use default import for dompurify instead of namespace import#908
Merged
Conversation
* Expose RelayMon trust observations as Nostr assertions RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses. The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture. Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring Confidence: high Scope-risk: moderate Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test . Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check * Keep docs lint focused on checked-in docs Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs. Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface Confidence: high Scope-risk: narrow Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md' Not-tested: Full GitHub Actions matrix before push * Keep RelayMon TRA assertions monitor-local RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data. Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events Constraint: monitors that do not publish TRA must not write detailed TRA observation history Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon Confidence: high Scope-risk: moderate Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator Tested: deno task test:config-types Tested: deno task test:database Tested: deno task test:kind30385 Tested: deno task test:unit Tested: deno task compile Tested: git diff --check Tested: deno lint with existing noisy rules excluded on changed TypeScript files Not-tested: full deno check remains blocked by existing project-wide strict type errors --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended. Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active. Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence. Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events. Confidence: high Scope-risk: narrow Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout Co-authored-by: sandwich <dskvr@users.noreply.github.com>
…dwichfarm#926) * Let remediation deletions publish after warmup Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued. Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression. Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing. Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller. Confidence: high Scope-risk: narrow Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts Not-tested: live Fisherman deployment and relay ACK drain * fix(relaymon): move TRA per-check DB work off the check hot path Trusted Relay Assertion (kind 30385) processing ran inline on every relay check: recordTrustedRelayObservation + buildTrustedRelayAssertion do synchronous SQLite work, including a read-back of up to max_observations_per_relay history rows plus per-sample scoring. On Deno's single-threaded event loop (and a CPU-constrained host) those bursts starved in-flight WebSocket I/O and busted the tight check budgets (read/open timeouts), so healthy relays were falsely reported offline. This is what took Fisherman down once TRA was enabled there. Decouple TRA from the check hot path: - processRelay now calls enqueueTrustedRelayObservation(), a cheap guard + array push (no DB work). The queue is capped (drops oldest, warns) so it can't grow unbounded during warmup. - A lazily-started background processor drains the queue one relay at a time and yields the event loop (delay) between relays, so the synchronous per-relay work never sustains a block long enough to starve checks. Start is deferred via setTimeout so the first item never runs inside the check's call stack. - Throttle is configurable via trustedRelayAssertions.processing_throttle_ms (default 25ms). publishTrustedRelayAssertion (the per-item worker) is unchanged, so its record/build/gate/publish behavior and existing coverage are preserved. Tests: add coverage that enqueue defers work (nothing published synchronously; queue drains via processTrustedRelayQueueOnce), that it is a no-op when TRA is disabled, and that the queue caps memory by dropping oldest. Full unit suite: 369 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…chfarm#928) Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml workflows (whose `<pkg>-v*` tag triggers never matched the release's actual `<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a single matrix-driven workflow covering every package that produces a Docker image: rstate, trawler, relaymon (nocapd excluded — deprecated). Behaviour: - push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for ALL matrix packages. - release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest` when it is the highest non-prerelease version (computed from git tags). - both paths runnable via workflow_dispatch (mode dev|tagged, package, version, update_latest auto|true|false). - every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR (ghcr.io/sandwichfarm/<app>). The build plan (which images, which tags, latest-or-not) lives in scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so the workflow logic is verifiable outside CI. Build contexts match each app's Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root). Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the built-in GITHUB_TOKEN (packages: write). Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hfarm#922) * Prevent stale rstate relay assertion cache hits The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks. This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only. Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind. Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next. Confidence: high Scope-risk: narrow Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases. Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing). Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce. Not-tested: full workspace recursive lint/build/test. * Publish trusted relay assertions from computed rstate output rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached. Constraint: must not ingest kind 1066 for this slice Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity Confidence: high Scope-risk: moderate Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check Not-tested: live relay publish round-trip against a production signer/relay pair --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com>
…andwichfarm#932) On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored relay (~17k), because the "already deleted" guard was an in-memory Set that resets each process. The deletion only needs to be sent once per ignore episode. Persist the guard: - relay_status gains a `deletion_published_at` column (idempotent migration). - deleteRelayCheckEvent skips when the durable marker is set (or the in-memory fast-path is) and stamps it on successful publish — so the once-only guarantee survives restarts. - IgnoreListSync.publishDeletions now only iterates ignored relays that are actually pending (deletion_published_at = 0) instead of all of them. - The marker is cleared whenever a relay becomes unignored (markRelayUnignored, persistResult, dedup re-evaluation), so a later re-ignore republishes exactly once (a prior deletion only covered the earlier 30166). Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip, cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite: 374 passed. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sandwichfarm#931) A prior merge committed unresolved conflict markers into apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content only — no version/release change. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(quick/260503-kc3): add failing regression tests for remaining XSS vectors
- operators.formatters.test.ts: name/about/reference/filterFormatters
- monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters
- relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy
- DataTableFallback.test.ts: structural regression for {@html row[column.key]}
* fix(gui/security): sanitize remaining XSS sinks in dataTable formatters
- operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters
- monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo
- relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software
- softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit)
- test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver
* fix(gui/security): bind cell + filter fallbacks as text, not HTML
Replace the {@html} fallbacks in 4 components with split-conditional text
bindings. Registered formatters keep their {@html ...} branch (output is
sanitized as of the previous commit). Unregistered columns now render as
plain text — closes the silent regression vector where any column without
a formatter would re-introduce XSS.
- data-view/table/DataTable.svelte: fallback row[column.key] now text-bound
- lists/table/DataTable.svelte: same
- lists/table/Filters.svelte: filter button fallback now text-bound
- data-view/filters/DataViewFilters.svelte: same
* fix build failures
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
…wichfarm#933) Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7. - [Release notes](https://github.com/chimurai/http-proxy-middleware/releases) - [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md) - [Commits](chimurai/http-proxy-middleware@v3.0.5...v3.0.7) --- updated-dependencies: - dependency-name: http-proxy-middleware dependency-version: 3.0.7 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](cure53/DOMPurify@3.4.2...3.4.11) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.11 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](nodeca/js-yaml@4.1.1...4.2.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* test(quick/260503-kc3): add failing regression tests for remaining XSS vectors
- operators.formatters.test.ts: name/about/reference/filterFormatters
- monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters
- relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy
- DataTableFallback.test.ts: structural regression for {@html row[column.key]}
* fix(gui/security): sanitize remaining XSS sinks in dataTable formatters
- operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters
- monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo
- relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software
- softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit)
- test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver
* fix(gui/security): bind cell + filter fallbacks as text, not HTML
Replace the {@html} fallbacks in 4 components with split-conditional text
bindings. Registered formatters keep their {@html ...} branch (output is
sanitized as of the previous commit). Unregistered columns now render as
plain text — closes the silent regression vector where any column without
a formatter would re-introduce XSS.
- data-view/table/DataTable.svelte: fallback row[column.key] now text-bound
- lists/table/DataTable.svelte: same
- lists/table/Filters.svelte: filter button fallback now text-bound
- data-view/filters/DataViewFilters.svelte: same
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
sandwichfarm#920) * chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.1 to 3.2.6. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 3.2.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * Add RelayMon trusted relay assertion publishing (sandwichfarm#923) * Expose RelayMon trust observations as Nostr assertions RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses. The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture. Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring Confidence: high Scope-risk: moderate Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test . Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check * Keep docs lint focused on checked-in docs Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs. Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface Confidence: high Scope-risk: narrow Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md' Not-tested: Full GitHub Actions matrix before push * Keep RelayMon TRA assertions monitor-local RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data. Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events Constraint: monitors that do not publish TRA must not write detailed TRA observation history Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon Confidence: high Scope-risk: moderate Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator Tested: deno task test:config-types Tested: deno task test:database Tested: deno task test:kind30385 Tested: deno task test:unit Tested: deno task compile Tested: git diff --check Tested: deno lint with existing noisy rules excluded on changed TypeScript files Not-tested: full deno check remains blocked by existing project-wide strict type errors --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * Allow RelayMon TRA publication during warmup (sandwichfarm#925) Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended. Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active. Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence. Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events. Confidence: high Scope-risk: narrow Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): move TRA per-check DB work off the check hot path (sandwichfarm#926) * Let remediation deletions publish after warmup Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued. Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression. Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing. Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller. Confidence: high Scope-risk: narrow Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts Not-tested: live Fisherman deployment and relay ACK drain * fix(relaymon): move TRA per-check DB work off the check hot path Trusted Relay Assertion (kind 30385) processing ran inline on every relay check: recordTrustedRelayObservation + buildTrustedRelayAssertion do synchronous SQLite work, including a read-back of up to max_observations_per_relay history rows plus per-sample scoring. On Deno's single-threaded event loop (and a CPU-constrained host) those bursts starved in-flight WebSocket I/O and busted the tight check budgets (read/open timeouts), so healthy relays were falsely reported offline. This is what took Fisherman down once TRA was enabled there. Decouple TRA from the check hot path: - processRelay now calls enqueueTrustedRelayObservation(), a cheap guard + array push (no DB work). The queue is capped (drops oldest, warns) so it can't grow unbounded during warmup. - A lazily-started background processor drains the queue one relay at a time and yields the event loop (delay) between relays, so the synchronous per-relay work never sustains a block long enough to starve checks. Start is deferred via setTimeout so the first item never runs inside the check's call stack. - Throttle is configurable via trustedRelayAssertions.processing_throttle_ms (default 25ms). publishTrustedRelayAssertion (the per-item worker) is unchanged, so its record/build/gate/publish behavior and existing coverage are preserved. Tests: add coverage that enqueue defers work (nothing published synchronously; queue drains via processTrustedRelayQueueOnce), that it is a no-op when TRA is disabled, and that the queue caps memory by dropping oldest. Full unit suite: 369 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (sandwichfarm#928) Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml workflows (whose `<pkg>-v*` tag triggers never matched the release's actual `<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a single matrix-driven workflow covering every package that produces a Docker image: rstate, trawler, relaymon (nocapd excluded — deprecated). Behaviour: - push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for ALL matrix packages. - release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest` when it is the highest non-prerelease version (computed from git tags). - both paths runnable via workflow_dispatch (mode dev|tagged, package, version, update_latest auto|true|false). - every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR (ghcr.io/sandwichfarm/<app>). The build plan (which images, which tags, latest-or-not) lives in scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so the workflow logic is verifiable outside CI. Build contexts match each app's Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root). Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the built-in GITHUB_TOKEN (packages: write). Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * rstate: publish trusted relay assertions from computed state (sandwichfarm#922) * Prevent stale rstate relay assertion cache hits The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks. This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only. Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind. Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next. Confidence: high Scope-risk: narrow Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases. Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing). Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce. Not-tested: full workspace recursive lint/build/test. * Publish trusted relay assertions from computed rstate output rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached. Constraint: must not ingest kind 1066 for this slice Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity Confidence: high Scope-risk: moderate Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check Not-tested: live relay publish round-trip against a production signer/relay pair --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): publish each relay deletion once, not on every boot (sandwichfarm#932) On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored relay (~17k), because the "already deleted" guard was an in-memory Set that resets each process. The deletion only needs to be sent once per ignore episode. Persist the guard: - relay_status gains a `deletion_published_at` column (idempotent migration). - deleteRelayCheckEvent skips when the durable marker is set (or the in-memory fast-path is) and stamps it on successful publish — so the once-only guarantee survives restarts. - IgnoreListSync.publishDeletions now only iterates ignored relays that are actually pending (deletion_published_at = 0) instead of all of them. - The marker is cleared whenever a relay becomes unignored (markRelayUnignored, persistResult, dedup re-evaluation), so a later re-ignore republishes exactly once (a prior deletion only covered the earlier 30166). Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip, cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite: 374 passed. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(trawler): remove committed merge-conflict markers from CHANGELOG (sandwichfarm#931) A prior merge committed unresolved conflict markers into apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content only — no version/release change. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix build failures (sandwichfarm#937) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same * fix build failures --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (sandwichfarm#933) Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7. - [Release notes](https://github.com/chimurai/http-proxy-middleware/releases) - [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md) - [Commits](chimurai/http-proxy-middleware@v3.0.5...v3.0.7) --- updated-dependencies: - dependency-name: http-proxy-middleware dependency-version: 3.0.7 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump dompurify from 3.4.2 to 3.4.11 (sandwichfarm#934) Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](cure53/DOMPurify@3.4.2...3.4.11) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.11 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (sandwichfarm#935) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](nodeca/js-yaml@4.1.1...4.2.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <299465+dskvr@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ta-js-ajv (sandwichfarm#921) * chore(deps-dev): bump vitest in /libraries/schemata-js-ajv Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.2 to 3.2.6. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 3.2.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * Add RelayMon trusted relay assertion publishing (sandwichfarm#923) * Expose RelayMon trust observations as Nostr assertions RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses. The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture. Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring Confidence: high Scope-risk: moderate Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test . Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check * Keep docs lint focused on checked-in docs Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs. Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface Confidence: high Scope-risk: narrow Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md' Not-tested: Full GitHub Actions matrix before push * Keep RelayMon TRA assertions monitor-local RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data. Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events Constraint: monitors that do not publish TRA must not write detailed TRA observation history Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon Confidence: high Scope-risk: moderate Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator Tested: deno task test:config-types Tested: deno task test:database Tested: deno task test:kind30385 Tested: deno task test:unit Tested: deno task compile Tested: git diff --check Tested: deno lint with existing noisy rules excluded on changed TypeScript files Not-tested: full deno check remains blocked by existing project-wide strict type errors --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * Allow RelayMon TRA publication during warmup (sandwichfarm#925) Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended. Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active. Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence. Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events. Confidence: high Scope-risk: narrow Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): move TRA per-check DB work off the check hot path (sandwichfarm#926) * Let remediation deletions publish after warmup Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued. Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression. Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing. Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller. Confidence: high Scope-risk: narrow Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts Not-tested: live Fisherman deployment and relay ACK drain * fix(relaymon): move TRA per-check DB work off the check hot path Trusted Relay Assertion (kind 30385) processing ran inline on every relay check: recordTrustedRelayObservation + buildTrustedRelayAssertion do synchronous SQLite work, including a read-back of up to max_observations_per_relay history rows plus per-sample scoring. On Deno's single-threaded event loop (and a CPU-constrained host) those bursts starved in-flight WebSocket I/O and busted the tight check budgets (read/open timeouts), so healthy relays were falsely reported offline. This is what took Fisherman down once TRA was enabled there. Decouple TRA from the check hot path: - processRelay now calls enqueueTrustedRelayObservation(), a cheap guard + array push (no DB work). The queue is capped (drops oldest, warns) so it can't grow unbounded during warmup. - A lazily-started background processor drains the queue one relay at a time and yields the event loop (delay) between relays, so the synchronous per-relay work never sustains a block long enough to starve checks. Start is deferred via setTimeout so the first item never runs inside the check's call stack. - Throttle is configurable via trustedRelayAssertions.processing_throttle_ms (default 25ms). publishTrustedRelayAssertion (the per-item worker) is unchanged, so its record/build/gate/publish behavior and existing coverage are preserved. Tests: add coverage that enqueue defers work (nothing published synchronously; queue drains via processTrustedRelayQueueOnce), that it is a no-op when TRA is disabled, and that the queue caps memory by dropping oldest. Full unit suite: 369 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (sandwichfarm#928) Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml workflows (whose `<pkg>-v*` tag triggers never matched the release's actual `<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a single matrix-driven workflow covering every package that produces a Docker image: rstate, trawler, relaymon (nocapd excluded — deprecated). Behaviour: - push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for ALL matrix packages. - release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest` when it is the highest non-prerelease version (computed from git tags). - both paths runnable via workflow_dispatch (mode dev|tagged, package, version, update_latest auto|true|false). - every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR (ghcr.io/sandwichfarm/<app>). The build plan (which images, which tags, latest-or-not) lives in scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so the workflow logic is verifiable outside CI. Build contexts match each app's Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root). Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the built-in GITHUB_TOKEN (packages: write). Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * rstate: publish trusted relay assertions from computed state (sandwichfarm#922) * Prevent stale rstate relay assertion cache hits The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks. This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only. Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind. Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next. Confidence: high Scope-risk: narrow Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases. Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing). Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce. Not-tested: full workspace recursive lint/build/test. * Publish trusted relay assertions from computed rstate output rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached. Constraint: must not ingest kind 1066 for this slice Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity Confidence: high Scope-risk: moderate Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check Not-tested: live relay publish round-trip against a production signer/relay pair --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): publish each relay deletion once, not on every boot (sandwichfarm#932) On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored relay (~17k), because the "already deleted" guard was an in-memory Set that resets each process. The deletion only needs to be sent once per ignore episode. Persist the guard: - relay_status gains a `deletion_published_at` column (idempotent migration). - deleteRelayCheckEvent skips when the durable marker is set (or the in-memory fast-path is) and stamps it on successful publish — so the once-only guarantee survives restarts. - IgnoreListSync.publishDeletions now only iterates ignored relays that are actually pending (deletion_published_at = 0) instead of all of them. - The marker is cleared whenever a relay becomes unignored (markRelayUnignored, persistResult, dedup re-evaluation), so a later re-ignore republishes exactly once (a prior deletion only covered the earlier 30166). Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip, cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite: 374 passed. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(trawler): remove committed merge-conflict markers from CHANGELOG (sandwichfarm#931) A prior merge committed unresolved conflict markers into apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content only — no version/release change. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix build failures (sandwichfarm#937) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same * fix build failures --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (sandwichfarm#933) Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7. - [Release notes](https://github.com/chimurai/http-proxy-middleware/releases) - [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md) - [Commits](chimurai/http-proxy-middleware@v3.0.5...v3.0.7) --- updated-dependencies: - dependency-name: http-proxy-middleware dependency-version: 3.0.7 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump dompurify from 3.4.2 to 3.4.11 (sandwichfarm#934) Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](cure53/DOMPurify@3.4.2...3.4.11) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.11 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (sandwichfarm#935) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](nodeca/js-yaml@4.1.1...4.2.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(gui/security): close remaining XSS vectors (sandwichfarm#938) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor (sandwichfarm#920) * chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.1 to 3.2.6. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 3.2.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * Add RelayMon trusted relay assertion publishing (sandwichfarm#923) * Expose RelayMon trust observations as Nostr assertions RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses. The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture. Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring Confidence: high Scope-risk: moderate Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test . Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check * Keep docs lint focused on checked-in docs Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs. Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface Confidence: high Scope-risk: narrow Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md' Not-tested: Full GitHub Actions matrix before push * Keep RelayMon TRA assertions monitor-local RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data. Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events Constraint: monitors that do not publish TRA must not write detailed TRA observation history Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon Confidence: high Scope-risk: moderate Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator Tested: deno task test:config-types Tested: deno task test:database Tested: deno task test:kind30385 Tested: deno task test:unit Tested: deno task compile Tested: git diff --check Tested: deno lint with existing noisy rules excluded on changed TypeScript files Not-tested: full deno check remains blocked by existing project-wide strict type errors --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * Allow RelayMon TRA publication during warmup (sandwichfarm#925) Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended. Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active. Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence. Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events. Confidence: high Scope-risk: narrow Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): move TRA per-check DB work off the check hot path (sandwichfarm#926) * Let remediation deletions publish after warmup Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued. Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression. Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing. Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller. Confidence: high Scope-risk: narrow Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts Not-tested: live Fisherman deployment and relay ACK drain * fix(relaymon): move TRA per-check DB work off the check hot path Trusted Relay Assertion (kind 30385) processing ran inline on every relay check: recordTrustedRelayObservation + buildTrustedRelayAssertion do synchronous SQLite work, including a read-back of up to max_observations_per_relay history rows plus per-sample scoring. On Deno's single-threaded event loop (and a CPU-constrained host) those bursts starved in-flight WebSocket I/O and busted the tight check budgets (read/open timeouts), so healthy relays were falsely reported offline. This is what took Fisherman down once TRA was enabled there. Decouple TRA from the check hot path: - processRelay now calls enqueueTrustedRelayObservation(), a cheap guard + array push (no DB work). The queue is capped (drops oldest, warns) so it can't grow unbounded during warmup. - A lazily-started background processor drains the queue one relay at a time and yields the event loop (delay) between relays, so the synchronous per-relay work never sustains a block long enough to starve checks. Start is deferred via setTimeout so the first item never runs inside the check's call stack. - Throttle is configurable via trustedRelayAssertions.processing_throttle_ms (default 25ms). publishTrustedRelayAssertion (the per-item worker) is unchanged, so its record/build/gate/publish behavior and existing coverage are preserved. Tests: add coverage that enqueue defers work (nothing published synchronously; queue drains via processTrustedRelayQueueOnce), that it is a no-op when TRA is disabled, and that the queue caps memory by dropping oldest. Full unit suite: 369 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (sandwichfarm#928) Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml workflows (whose `<pkg>-v*` tag triggers never matched the release's actual `<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a single matrix-driven workflow covering every package that produces a Docker image: rstate, trawler, relaymon (nocapd excluded — deprecated). Behaviour: - push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for ALL matrix packages. - release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest` when it is the highest non-prerelease version (computed from git tags). - both paths runnable via workflow_dispatch (mode dev|tagged, package, version, update_latest auto|true|false). - every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR (ghcr.io/sandwichfarm/<app>). The build plan (which images, which tags, latest-or-not) lives in scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so the workflow logic is verifiable outside CI. Build contexts match each app's Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root). Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the built-in GITHUB_TOKEN (packages: write). Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * rstate: publish trusted relay assertions from computed state (sandwichfarm#922) * Prevent stale rstate relay assertion cache hits The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks. This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only. Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind. Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next. Confidence: high Scope-risk: narrow Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases. Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing). Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce. Not-tested: full workspace recursive lint/build/test. * Publish trusted relay assertions from computed rstate output rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached. Constraint: must not ingest kind 1066 for this slice Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity Confidence: high Scope-risk: moderate Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check Not-tested: live relay publish round-trip against a production signer/relay pair --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): publish each relay deletion once, not on every boot (sandwichfarm#932) On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored relay (~17k), because the "already deleted" guard was an in-memory Set that resets each process. The deletion only needs to be sent once per ignore episode. Persist the guard: - relay_status gains a `deletion_published_at` column (idempotent migration). - deleteRelayCheckEvent skips when the durable marker is set (or the in-memory fast-path is) and stamps it on successful publish — so the once-only guarantee survives restarts. - IgnoreListSync.publishDeletions now only iterates ignored relays that are actually pending (deletion_published_at = 0) instead of all of them. - The marker is cleared whenever a relay becomes unignored (markRelayUnignored, persistResult, dedup re-evaluation), so a later re-ignore republishes exactly once (a prior deletion only covered the earlier 30166). Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip, cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite: 374 passed. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(trawler): remove committed merge-conflict markers from CHANGELOG (sandwichfarm#931) A prior merge committed unresolved conflict markers into apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content only — no version/release change. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix build failures (sandwichfarm#937) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same * fix build failures --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (sandwichfarm#933) Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7. - [Release notes](https://github.com/chimurai/http-proxy-middleware/releases) - [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md) - [Commits](chimurai/http-proxy-middleware@v3.0.5...v3.0.7) --- updated-dependencies: - dependency-name: http-proxy-middleware dependency-version: 3.0.7 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump dompurify from 3.4.2 to 3.4.11 (sandwichfarm#934) Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](cure53/DOMPurify@3.4.2...3.4.11) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.11 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (sandwichfarm#935) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](nodeca/js-yaml@4.1.1...4.2.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <299465+dskvr@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <299465+dskvr@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rs/default/WebsocketAdapterDefault (sandwichfarm#913) * chore(deps): bump ws Bumps [ws](https://github.com/websockets/ws) from 8.18.0 to 8.20.1. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/8.18.0...8.20.1) --- updated-dependencies: - dependency-name: ws dependency-version: 8.20.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> * Add RelayMon trusted relay assertion publishing (#923) * Expose RelayMon trust observations as Nostr assertions RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses. The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture. Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring Confidence: high Scope-risk: moderate Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test . Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check * Keep docs lint focused on checked-in docs Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs. Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface Confidence: high Scope-risk: narrow Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md' Not-tested: Full GitHub Actions matrix before push * Keep RelayMon TRA assertions monitor-local RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data. Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events Constraint: monitors that do not publish TRA must not write detailed TRA observation history Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon Confidence: high Scope-risk: moderate Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator Tested: deno task test:config-types Tested: deno task test:database Tested: deno task test:kind30385 Tested: deno task test:unit Tested: deno task compile Tested: git diff --check Tested: deno lint with existing noisy rules excluded on changed TypeScript files Not-tested: full deno check remains blocked by existing project-wide strict type errors --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * Allow RelayMon TRA publication during warmup (#925) Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended. Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active. Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence. Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events. Confidence: high Scope-risk: narrow Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): move TRA per-check DB work off the check hot path (#926) * Let remediation deletions publish after warmup Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued. Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression. Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing. Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller. Confidence: high Scope-risk: narrow Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts Not-tested: live Fisherman deployment and relay ACK drain * fix(relaymon): move TRA per-check DB work off the check hot path Trusted Relay Assertion (kind 30385) processing ran inline on every relay check: recordTrustedRelayObservation + buildTrustedRelayAssertion do synchronous SQLite work, including a read-back of up to max_observations_per_relay history rows plus per-sample scoring. On Deno's single-threaded event loop (and a CPU-constrained host) those bursts starved in-flight WebSocket I/O and busted the tight check budgets (read/open timeouts), so healthy relays were falsely reported offline. This is what took Fisherman down once TRA was enabled there. Decouple TRA from the check hot path: - processRelay now calls enqueueTrustedRelayObservation(), a cheap guard + array push (no DB work). The queue is capped (drops oldest, warns) so it can't grow unbounded during warmup. - A lazily-started background processor drains the queue one relay at a time and yields the event loop (delay) between relays, so the synchronous per-relay work never sustains a block long enough to starve checks. Start is deferred via setTimeout so the first item never runs inside the check's call stack. - Throttle is configurable via trustedRelayAssertions.processing_throttle_ms (default 25ms). publishTrustedRelayAssertion (the per-item worker) is unchanged, so its record/build/gate/publish behavior and existing coverage are preserved. Tests: add coverage that enqueue defers work (nothing published synchronously; queue drains via processTrustedRelayQueueOnce), that it is a no-op when TRA is disabled, and that the queue caps memory by dropping oldest. Full unit suite: 369 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (#928) Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml workflows (whose `<pkg>-v*` tag triggers never matched the release's actual `<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a single matrix-driven workflow covering every package that produces a Docker image: rstate, trawler, relaymon (nocapd excluded — deprecated). Behaviour: - push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for ALL matrix packages. - release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest` when it is the highest non-prerelease version (computed from git tags). - both paths runnable via workflow_dispatch (mode dev|tagged, package, version, update_latest auto|true|false). - every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR (ghcr.io/sandwichfarm/<app>). The build plan (which images, which tags, latest-or-not) lives in scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so the workflow logic is verifiable outside CI. Build contexts match each app's Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root). Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the built-in GITHUB_TOKEN (packages: write). Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * rstate: publish trusted relay assertions from computed state (#922) * Prevent stale rstate relay assertion cache hits The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks. This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only. Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind. Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next. Confidence: high Scope-risk: narrow Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases. Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing). Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce. Not-tested: full workspace recursive lint/build/test. * Publish trusted relay assertions from computed rstate output rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached. Constraint: must not ingest kind 1066 for this slice Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity Confidence: high Scope-risk: moderate Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check Not-tested: live relay publish round-trip against a production signer/relay pair --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): publish each relay deletion once, not on every boot (#932) On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored relay (~17k), because the "already deleted" guard was an in-memory Set that resets each process. The deletion only needs to be sent once per ignore episode. Persist the guard: - relay_status gains a `deletion_published_at` column (idempotent migration). - deleteRelayCheckEvent skips when the durable marker is set (or the in-memory fast-path is) and stamps it on successful publish — so the once-only guarantee survives restarts. - IgnoreListSync.publishDeletions now only iterates ignored relays that are actually pending (deletion_published_at = 0) instead of all of them. - The marker is cleared whenever a relay becomes unignored (markRelayUnignored, persistResult, dedup re-evaluation), so a later re-ignore republishes exactly once (a prior deletion only covered the earlier 30166). Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip, cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite: 374 passed. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(trawler): remove committed merge-conflict markers from CHANGELOG (#931) A prior merge committed unresolved conflict markers into apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content only — no version/release change. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix build failures (#937) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same * fix build failures --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (#933) Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7. - [Release notes](https://github.com/chimurai/http-proxy-middleware/releases) - [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md) - [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v3.0.5...v3.0.7) --- updated-dependencies: - dependency-name: http-proxy-middleware dependency-version: 3.0.7 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump dompurify from 3.4.2 to 3.4.11 (#934) Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.11) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.11 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (#935) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(gui/security): close remaining XSS vectors (#938) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor (#920) * chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.1 to 3.2.6. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 3.2.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * Add RelayMon trusted relay assertion publishing (#923) * Expose RelayMon trust observations as Nostr assertions RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses. The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture. Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring Confidence: high Scope-risk: moderate Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test . Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check * Keep docs lint focused on checked-in docs Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs. Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface Confidence: high Scope-risk: narrow Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md' Not-tested: Full GitHub Actions matrix before push * Keep RelayMon TRA assertions monitor-local RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data. Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events Constraint: monitors that do not publish TRA must not write detailed TRA observation history Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon Confidence: high Scope-risk: moderate Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator Tested: deno task test:config-types Tested: deno task test:database Tested: deno task test:kind30385 Tested: deno task test:unit Tested: deno task compile Tested: git diff --check Tested: deno lint with existing noisy rules excluded on changed TypeScript files Not-tested: full deno check remains blocked by existing project-wide strict type errors --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * Allow RelayMon TRA publication during warmup (#925) Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended. Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active. Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence. Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events. Confidence: high Scope-risk: narrow Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): move TRA per-check DB work off the check hot path (#926) * Let remediation deletions publish after warmup Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued. Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression. Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing. Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller. Confidence: high Scope-risk: narrow Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts Not-tested: live Fisherman deployment and relay ACK drain * fix(relaymon): move TRA per-check DB work off the check hot path Trusted Relay Assertion (kind 30385) processing ran inline on every relay check: recordTrustedRelayObservation + buildTrustedRelayAssertion do synchronous SQLite work, including a read-back of up to max_observations_per_relay history rows plus per-sample scoring. On Deno's single-threaded event loop (and a CPU-constrained host) those bursts starved in-flight WebSocket I/O and busted the tight check budgets (read/open timeouts), so healthy relays were falsely reported offline. This is what took Fisherman down once TRA was enabled there. Decouple TRA from the check hot path: - processRelay now calls enqueueTrustedRelayObservation(), a cheap guard + array push (no DB work). The queue is capped (drops oldest, warns) so it can't grow unbounded during warmup. - A lazily-started background processor drains the queue one relay at a time and yields the event loop (delay) between relays, so the synchronous per-relay work never sustains a block long enough to starve checks. Start is deferred via setTimeout so the first item never runs inside the check's call stack. - Throttle is configurable via trustedRelayAssertions.processing_throttle_ms (default 25ms). publishTrustedRelayAssertion (the per-item worker) is unchanged, so its record/build/gate/publish behavior and existing coverage are preserved. Tests: add coverage that enqueue defers work (nothing published synchronously; queue drains via processTrustedRelayQueueOnce), that it is a no-op when TRA is disabled, and that the queue caps memory by dropping oldest. Full unit suite: 369 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (#928) Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml workflows (whose `<pkg>-v*` tag triggers never matched the release's actual `<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a single matrix-driven workflow covering every package that produces a Docker image: rstate, trawler, relaymon (nocapd excluded — deprecated). Behaviour: - push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for ALL matrix packages. - release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest` when it is the highest non-prerelease version (computed from git tags). - both paths runnable via workflow_dispatch (mode dev|tagged, package, version, update_latest auto|true|false). - every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR (ghcr.io/sandwichfarm/<app>). The build plan (which images, which tags, latest-or-not) lives in scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so the workflow logic is verifiable outside CI. Build contexts match each app's Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root). Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the built-in GITHUB_TOKEN (packages: write). Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * rstate: publish trusted relay assertions from computed state (#922) * Prevent stale rstate relay assertion cache hits The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks. This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only. Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind. Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next. Confidence: high Scope-risk: narrow Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases. Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing). Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce. Not-tested: full workspace recursive lint/build/test. * Publish trusted relay assertions from computed rstate output rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached. Constraint: must not ingest kind 1066 for this slice Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity Confidence: high Scope-risk: moderate Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check Not-tested: live relay publish round-trip against a production signer/relay pair --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): publish each relay deletion once, not on every boot (#932) On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored relay (~17k), because the "already deleted" guard was an in-memory Set that resets each process. The deletion only needs to be sent once per ignore episode. Persist the guard: - relay_status gains a `deletion_published_at` column (idempotent migration). - deleteRelayCheckEvent skips when the durable marker is set (or the in-memory fast-path is) and stamps it on successful publish — so the once-only guarantee survives restarts. - IgnoreListSync.publishDeletions now only iterates ignored relays that are actually pending (deletion_published_at = 0) instead of all of them. - The marker is cleared whenever a relay becomes unignored (markRelayUnignored, persistResult, dedup re-evaluation), so a later re-ignore republishes exactly once (a prior deletion only covered the earlier 30166). Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip, cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite: 374 passed. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(trawler): remove committed merge-conflict markers from CHANGELOG (#931) A prior merge committed unresolved conflict markers into apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content only — no version/release change. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix build failures (#937) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same * fix build failures --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (#933) Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7. - [Release notes](https://github.com/chimurai/http-proxy-middleware/releases) - [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md) - [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v3.0.5...v3.0.7) --- updated-dependencies: - dependency-name: http-proxy-middleware dependency-version: 3.0.7 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump dompurify from 3.4.2 to 3.4.11 (#934) Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.11) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.11 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (#935) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <299465+dskvr@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(deps-dev): bump vitest from 3.1.2 to 3.2.6 in /libraries/schemata-js-ajv (#921) * chore(deps-dev): bump vitest in /libraries/schemata-js-ajv Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.2 to 3.2.6. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 3.2.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * Add RelayMon trusted relay assertion publishing (#923) * Expose RelayMon trust observations as Nostr assertions RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses. The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture. Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring Confidence: high Scope-risk: moderate Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test . Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check * Keep docs lint focused on checked-in docs Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs. Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface Confidence: high Scope-risk: narrow Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md' Not-tested: Full GitHub Actions matrix before push * Keep RelayMon TRA assertions monitor-local RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data. Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events Constraint: monitors that do not publish TRA must not write detailed TRA observation history Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon Confidence: high Scope-risk: moderate Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator Tested: deno task test:config-types Tested: deno task test:database Tested: deno task test:kind30385 Tested: deno task test:unit Tested: deno task compile Tested: git diff --check Tested: deno lint with existing noisy rules excluded on changed TypeScript files Not-tested: full deno check remains blocked by existing project-wide strict type errors --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * Allow RelayMon TRA publication during warmup (#925) Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended. Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active. Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence. Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events. Confidence: high Scope-risk: narrow Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): move TRA per-check DB work off the check hot path (#926) * Let remediation deletions publish after warmup Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued. Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression. Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing. Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller. Confidence: high Scope-risk: narrow Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts Not-tested: live Fisherman deployment and relay ACK drain * fix(relaymon): move TRA per-check DB work off the check hot path Trusted Relay Assertion (kind 30385) processing ran inline on every relay check: recordTrustedRelayObservation + buildTrustedRelayAssertion do synchronous SQLite work, including a read-back of up to max_observations_per_relay history rows plus per-sample scoring. On Deno's single-threaded event loop (and a CPU-constrained host) those bursts starved in-flight WebSocket I/O and busted the tight check budgets (read/open timeouts), so healthy relays were falsely reported offline. This is what took Fisherman down once TRA was enabled there. Decouple TRA from the check hot path: - processRelay now calls enqueueTrustedRelayObservation(), a cheap guard + array push (no DB work). The queue is capped (drops oldest, warns) so it can't grow unbounded during warmup. - A lazily-started background processor drains the queue one relay at a time and yields the event loop (delay) between relays, so the synchronous per-relay work never sustains a block long enough to starve checks. Start is deferred via setTimeout so the first item never runs inside the check's call stack. - Throttle is configurable via trustedRelayAssertions.processing_throttle_ms (default 25ms). publishTrustedRelayAssertion (the per-item worker) is unchanged, so its record/build/gate/publish behavior and existing coverage are preserved. Tests: add coverage that enqueue defers work (nothing published synchronously; queue drains via processTrustedRelayQueueOnce), that it is a no-op when TRA is disabled, and that the queue caps memory by dropping oldest. Full unit suite: 369 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (#928) Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml workflows (whose `<pkg>-v*` tag triggers never matched the release's actual `<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a single matrix-driven workflow covering every package that produces a Docker image: rstate, trawler, relaymon (nocapd excluded — deprecated). Behaviour: - push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for ALL matrix packages. - release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest` when it is the highest non-prerelease version (computed from git tags). - both paths runnable via workflow_dispatch (mode dev|tagged, package, version, update_latest auto|true|false). - every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR (ghcr.io/sandwichfarm/<app>). The build plan (which images, which tags, latest-or-not) lives in scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so the workflow logic is verifiable outside CI. Build contexts match each app's Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root). Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the built-in GITHUB_TOKEN (packages: write). Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * rstate: publish trusted relay assertions from computed state (#922) * Prevent stale rstate relay assertion cache hits The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks. This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only. Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind. Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next. Confidence: high Scope-risk: narrow Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases. Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing). Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce. Not-tested: full workspace recursive lint/build/test. * Publish trusted relay assertions from computed rstate output rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached. Constraint: must not ingest kind 1066 for this slice Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity Confidence: high Scope-risk: moderate Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check Not-tested: live relay publish round-trip against a production signer/relay pair --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): publish each relay deletion once, not on every boot (#932) On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored relay (~17k), because the "already deleted" guard was an in-memory Set that resets each process. The deletion only needs to be sent once per ignore episode. Persist the guard: - relay_status gains a `deletion_published_at` column (idempotent migration). - deleteRelayCheckEvent skips when the durable marker is set (or the in-memory fast-path is) and stamps it on successful publish — so the once-only guarantee survives restarts. - IgnoreListSync.publishDeletions now only iterates ignored relays that are actually pending (deletion_published_at = 0) instead of all of them. - The marker is cleared whenever a relay becomes unignored (markRelayUnignored, persistResult, dedup re-evaluation), so a later re-ignore republishes exactly once (a prior deletion only covered the earlier 30166). Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip, cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite: 374 passed. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(trawler): remove committed merge-conflict markers from CHANGELOG (#931) A prior merge committed unresolved conflict markers into apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content only — no version/release change. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix build failures (#937) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same * fix build failures --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (#933) Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7. - [Release notes](https://github.com/chimurai/http-proxy-middleware/releases) - [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md) - [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v3.0.5...v3.0.7) --- updated-dependencies: - dependency-name: http-proxy-middleware dependency-version: 3.0.7 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump dompurify from 3.4.2 to 3.4.11 (#934) Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.11) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.11 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (#935) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(gui/security): close remaining XSS vectors (#938) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor (#920) * chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.1 to 3.2.6. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 3.2.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * Add RelayMon trusted relay assertion publishing (#923) * Expose RelayMon trust observations as Nostr assertions RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses. The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture. Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring Confidence: high Scope-risk: moderate Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test . Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check * Keep docs lint focused on checked-in docs Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs. Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface Confidence: high Scope-risk: narrow Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md' Not-tested: Full GitHub Actions matrix before push * Keep RelayMon TRA assertions monitor-local RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data. Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events Constraint: monitors that do not publish TRA must not write detailed TRA observation history Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon Confidence: high Scope-risk: moderate Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator Tested: deno task test:config-types Tested: deno t…
…ichfarm#911) * chore(deps): bump ws from 8.17.1 to 8.20.1 in /libraries/nocap Bumps [ws](https://github.com/websockets/ws) from 8.17.1 to 8.20.1. - [Release notes](https://github.com/websockets/ws/releases) - [Commits](https://github.com/websockets/ws/compare/8.17.1...8.20.1) --- updated-dependencies: - dependency-name: ws dependency-version: 8.20.1 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> * Add RelayMon trusted relay assertion publishing (#923) * Expose RelayMon trust observations as Nostr assertions RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses. The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture. Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring Confidence: high Scope-risk: moderate Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test . Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check * Keep docs lint focused on checked-in docs Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs. Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface Confidence: high Scope-risk: narrow Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md' Not-tested: Full GitHub Actions matrix before push * Keep RelayMon TRA assertions monitor-local RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data. Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events Constraint: monitors that do not publish TRA must not write detailed TRA observation history Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon Confidence: high Scope-risk: moderate Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator Tested: deno task test:config-types Tested: deno task test:database Tested: deno task test:kind30385 Tested: deno task test:unit Tested: deno task compile Tested: git diff --check Tested: deno lint with existing noisy rules excluded on changed TypeScript files Not-tested: full deno check remains blocked by existing project-wide strict type errors --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * Allow RelayMon TRA publication during warmup (#925) Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended. Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active. Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence. Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events. Confidence: high Scope-risk: narrow Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): move TRA per-check DB work off the check hot path (#926) * Let remediation deletions publish after warmup Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued. Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression. Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing. Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller. Confidence: high Scope-risk: narrow Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts Not-tested: live Fisherman deployment and relay ACK drain * fix(relaymon): move TRA per-check DB work off the check hot path Trusted Relay Assertion (kind 30385) processing ran inline on every relay check: recordTrustedRelayObservation + buildTrustedRelayAssertion do synchronous SQLite work, including a read-back of up to max_observations_per_relay history rows plus per-sample scoring. On Deno's single-threaded event loop (and a CPU-constrained host) those bursts starved in-flight WebSocket I/O and busted the tight check budgets (read/open timeouts), so healthy relays were falsely reported offline. This is what took Fisherman down once TRA was enabled there. Decouple TRA from the check hot path: - processRelay now calls enqueueTrustedRelayObservation(), a cheap guard + array push (no DB work). The queue is capped (drops oldest, warns) so it can't grow unbounded during warmup. - A lazily-started background processor drains the queue one relay at a time and yields the event loop (delay) between relays, so the synchronous per-relay work never sustains a block long enough to starve checks. Start is deferred via setTimeout so the first item never runs inside the check's call stack. - Throttle is configurable via trustedRelayAssertions.processing_throttle_ms (default 25ms). publishTrustedRelayAssertion (the per-item worker) is unchanged, so its record/build/gate/publish behavior and existing coverage are preserved. Tests: add coverage that enqueue defers work (nothing published synchronously; queue drains via processTrustedRelayQueueOnce), that it is a no-op when TRA is disabled, and that the queue caps memory by dropping oldest. Full unit suite: 369 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (#928) Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml workflows (whose `<pkg>-v*` tag triggers never matched the release's actual `<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a single matrix-driven workflow covering every package that produces a Docker image: rstate, trawler, relaymon (nocapd excluded — deprecated). Behaviour: - push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for ALL matrix packages. - release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest` when it is the highest non-prerelease version (computed from git tags). - both paths runnable via workflow_dispatch (mode dev|tagged, package, version, update_latest auto|true|false). - every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR (ghcr.io/sandwichfarm/<app>). The build plan (which images, which tags, latest-or-not) lives in scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so the workflow logic is verifiable outside CI. Build contexts match each app's Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root). Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the built-in GITHUB_TOKEN (packages: write). Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * rstate: publish trusted relay assertions from computed state (#922) * Prevent stale rstate relay assertion cache hits The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks. This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only. Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind. Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next. Confidence: high Scope-risk: narrow Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases. Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing). Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce. Not-tested: full workspace recursive lint/build/test. * Publish trusted relay assertions from computed rstate output rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached. Constraint: must not ingest kind 1066 for this slice Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity Confidence: high Scope-risk: moderate Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check Not-tested: live relay publish round-trip against a production signer/relay pair --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): publish each relay deletion once, not on every boot (#932) On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored relay (~17k), because the "already deleted" guard was an in-memory Set that resets each process. The deletion only needs to be sent once per ignore episode. Persist the guard: - relay_status gains a `deletion_published_at` column (idempotent migration). - deleteRelayCheckEvent skips when the durable marker is set (or the in-memory fast-path is) and stamps it on successful publish — so the once-only guarantee survives restarts. - IgnoreListSync.publishDeletions now only iterates ignored relays that are actually pending (deletion_published_at = 0) instead of all of them. - The marker is cleared whenever a relay becomes unignored (markRelayUnignored, persistResult, dedup re-evaluation), so a later re-ignore republishes exactly once (a prior deletion only covered the earlier 30166). Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip, cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite: 374 passed. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(trawler): remove committed merge-conflict markers from CHANGELOG (#931) A prior merge committed unresolved conflict markers into apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content only — no version/release change. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix build failures (#937) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same * fix build failures --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (#933) Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7. - [Release notes](https://github.com/chimurai/http-proxy-middleware/releases) - [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md) - [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v3.0.5...v3.0.7) --- updated-dependencies: - dependency-name: http-proxy-middleware dependency-version: 3.0.7 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump dompurify from 3.4.2 to 3.4.11 (#934) Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.11) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.11 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (#935) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(gui/security): close remaining XSS vectors (#938) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor (#920) * chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.1 to 3.2.6. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 3.2.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * Add RelayMon trusted relay assertion publishing (#923) * Expose RelayMon trust observations as Nostr assertions RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses. The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture. Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring Confidence: high Scope-risk: moderate Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test . Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check * Keep docs lint focused on checked-in docs Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs. Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface Confidence: high Scope-risk: narrow Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md' Not-tested: Full GitHub Actions matrix before push * Keep RelayMon TRA assertions monitor-local RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data. Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events Constraint: monitors that do not publish TRA must not write detailed TRA observation history Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon Confidence: high Scope-risk: moderate Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator Tested: deno task test:config-types Tested: deno task test:database Tested: deno task test:kind30385 Tested: deno task test:unit Tested: deno task compile Tested: git diff --check Tested: deno lint with existing noisy rules excluded on changed TypeScript files Not-tested: full deno check remains blocked by existing project-wide strict type errors --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * Allow RelayMon TRA publication during warmup (#925) Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended. Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active. Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence. Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events. Confidence: high Scope-risk: narrow Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): move TRA per-check DB work off the check hot path (#926) * Let remediation deletions publish after warmup Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued. Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression. Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing. Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller. Confidence: high Scope-risk: narrow Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts Not-tested: live Fisherman deployment and relay ACK drain * fix(relaymon): move TRA per-check DB work off the check hot path Trusted Relay Assertion (kind 30385) processing ran inline on every relay check: recordTrustedRelayObservation + buildTrustedRelayAssertion do synchronous SQLite work, including a read-back of up to max_observations_per_relay history rows plus per-sample scoring. On Deno's single-threaded event loop (and a CPU-constrained host) those bursts starved in-flight WebSocket I/O and busted the tight check budgets (read/open timeouts), so healthy relays were falsely reported offline. This is what took Fisherman down once TRA was enabled there. Decouple TRA from the check hot path: - processRelay now calls enqueueTrustedRelayObservation(), a cheap guard + array push (no DB work). The queue is capped (drops oldest, warns) so it can't grow unbounded during warmup. - A lazily-started background processor drains the queue one relay at a time and yields the event loop (delay) between relays, so the synchronous per-relay work never sustains a block long enough to starve checks. Start is deferred via setTimeout so the first item never runs inside the check's call stack. - Throttle is configurable via trustedRelayAssertions.processing_throttle_ms (default 25ms). publishTrustedRelayAssertion (the per-item worker) is unchanged, so its record/build/gate/publish behavior and existing coverage are preserved. Tests: add coverage that enqueue defers work (nothing published synchronously; queue drains via processTrustedRelayQueueOnce), that it is a no-op when TRA is disabled, and that the queue caps memory by dropping oldest. Full unit suite: 369 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (#928) Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml workflows (whose `<pkg>-v*` tag triggers never matched the release's actual `<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a single matrix-driven workflow covering every package that produces a Docker image: rstate, trawler, relaymon (nocapd excluded — deprecated). Behaviour: - push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for ALL matrix packages. - release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest` when it is the highest non-prerelease version (computed from git tags). - both paths runnable via workflow_dispatch (mode dev|tagged, package, version, update_latest auto|true|false). - every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR (ghcr.io/sandwichfarm/<app>). The build plan (which images, which tags, latest-or-not) lives in scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so the workflow logic is verifiable outside CI. Build contexts match each app's Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root). Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the built-in GITHUB_TOKEN (packages: write). Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * rstate: publish trusted relay assertions from computed state (#922) * Prevent stale rstate relay assertion cache hits The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks. This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only. Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind. Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next. Confidence: high Scope-risk: narrow Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases. Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing). Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce. Not-tested: full workspace recursive lint/build/test. * Publish trusted relay assertions from computed rstate output rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached. Constraint: must not ingest kind 1066 for this slice Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity Confidence: high Scope-risk: moderate Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check Not-tested: live relay publish round-trip against a production signer/relay pair --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): publish each relay deletion once, not on every boot (#932) On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored relay (~17k), because the "already deleted" guard was an in-memory Set that resets each process. The deletion only needs to be sent once per ignore episode. Persist the guard: - relay_status gains a `deletion_published_at` column (idempotent migration). - deleteRelayCheckEvent skips when the durable marker is set (or the in-memory fast-path is) and stamps it on successful publish — so the once-only guarantee survives restarts. - IgnoreListSync.publishDeletions now only iterates ignored relays that are actually pending (deletion_published_at = 0) instead of all of them. - The marker is cleared whenever a relay becomes unignored (markRelayUnignored, persistResult, dedup re-evaluation), so a later re-ignore republishes exactly once (a prior deletion only covered the earlier 30166). Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip, cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite: 374 passed. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(trawler): remove committed merge-conflict markers from CHANGELOG (#931) A prior merge committed unresolved conflict markers into apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content only — no version/release change. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix build failures (#937) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same * fix build failures --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (#933) Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7. - [Release notes](https://github.com/chimurai/http-proxy-middleware/releases) - [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md) - [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v3.0.5...v3.0.7) --- updated-dependencies: - dependency-name: http-proxy-middleware dependency-version: 3.0.7 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump dompurify from 3.4.2 to 3.4.11 (#934) Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.11) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.11 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (#935) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <299465+dskvr@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(deps-dev): bump vitest from 3.1.2 to 3.2.6 in /libraries/schemata-js-ajv (#921) * chore(deps-dev): bump vitest in /libraries/schemata-js-ajv Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.2 to 3.2.6. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 3.2.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * Add RelayMon trusted relay assertion publishing (#923) * Expose RelayMon trust observations as Nostr assertions RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses. The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture. Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring Confidence: high Scope-risk: moderate Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test . Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check * Keep docs lint focused on checked-in docs Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs. Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface Confidence: high Scope-risk: narrow Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md' Not-tested: Full GitHub Actions matrix before push * Keep RelayMon TRA assertions monitor-local RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data. Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events Constraint: monitors that do not publish TRA must not write detailed TRA observation history Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon Confidence: high Scope-risk: moderate Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator Tested: deno task test:config-types Tested: deno task test:database Tested: deno task test:kind30385 Tested: deno task test:unit Tested: deno task compile Tested: git diff --check Tested: deno lint with existing noisy rules excluded on changed TypeScript files Not-tested: full deno check remains blocked by existing project-wide strict type errors --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * Allow RelayMon TRA publication during warmup (#925) Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended. Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active. Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence. Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events. Confidence: high Scope-risk: narrow Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): move TRA per-check DB work off the check hot path (#926) * Let remediation deletions publish after warmup Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued. Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression. Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing. Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller. Confidence: high Scope-risk: narrow Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts Not-tested: live Fisherman deployment and relay ACK drain * fix(relaymon): move TRA per-check DB work off the check hot path Trusted Relay Assertion (kind 30385) processing ran inline on every relay check: recordTrustedRelayObservation + buildTrustedRelayAssertion do synchronous SQLite work, including a read-back of up to max_observations_per_relay history rows plus per-sample scoring. On Deno's single-threaded event loop (and a CPU-constrained host) those bursts starved in-flight WebSocket I/O and busted the tight check budgets (read/open timeouts), so healthy relays were falsely reported offline. This is what took Fisherman down once TRA was enabled there. Decouple TRA from the check hot path: - processRelay now calls enqueueTrustedRelayObservation(), a cheap guard + array push (no DB work). The queue is capped (drops oldest, warns) so it can't grow unbounded during warmup. - A lazily-started background processor drains the queue one relay at a time and yields the event loop (delay) between relays, so the synchronous per-relay work never sustains a block long enough to starve checks. Start is deferred via setTimeout so the first item never runs inside the check's call stack. - Throttle is configurable via trustedRelayAssertions.processing_throttle_ms (default 25ms). publishTrustedRelayAssertion (the per-item worker) is unchanged, so its record/build/gate/publish behavior and existing coverage are preserved. Tests: add coverage that enqueue defers work (nothing published synchronously; queue drains via processTrustedRelayQueueOnce), that it is a no-op when TRA is disabled, and that the queue caps memory by dropping oldest. Full unit suite: 369 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (#928) Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml workflows (whose `<pkg>-v*` tag triggers never matched the release's actual `<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a single matrix-driven workflow covering every package that produces a Docker image: rstate, trawler, relaymon (nocapd excluded — deprecated). Behaviour: - push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for ALL matrix packages. - release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest` when it is the highest non-prerelease version (computed from git tags). - both paths runnable via workflow_dispatch (mode dev|tagged, package, version, update_latest auto|true|false). - every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR (ghcr.io/sandwichfarm/<app>). The build plan (which images, which tags, latest-or-not) lives in scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so the workflow logic is verifiable outside CI. Build contexts match each app's Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root). Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the built-in GITHUB_TOKEN (packages: write). Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * rstate: publish trusted relay assertions from computed state (#922) * Prevent stale rstate relay assertion cache hits The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks. This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only. Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind. Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next. Confidence: high Scope-risk: narrow Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases. Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing). Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce. Not-tested: full workspace recursive lint/build/test. * Publish trusted relay assertions from computed rstate output rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached. Constraint: must not ingest kind 1066 for this slice Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity Confidence: high Scope-risk: moderate Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check Not-tested: live relay publish round-trip against a production signer/relay pair --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): publish each relay deletion once, not on every boot (#932) On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored relay (~17k), because the "already deleted" guard was an in-memory Set that resets each process. The deletion only needs to be sent once per ignore episode. Persist the guard: - relay_status gains a `deletion_published_at` column (idempotent migration). - deleteRelayCheckEvent skips when the durable marker is set (or the in-memory fast-path is) and stamps it on successful publish — so the once-only guarantee survives restarts. - IgnoreListSync.publishDeletions now only iterates ignored relays that are actually pending (deletion_published_at = 0) instead of all of them. - The marker is cleared whenever a relay becomes unignored (markRelayUnignored, persistResult, dedup re-evaluation), so a later re-ignore republishes exactly once (a prior deletion only covered the earlier 30166). Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip, cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite: 374 passed. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(trawler): remove committed merge-conflict markers from CHANGELOG (#931) A prior merge committed unresolved conflict markers into apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content only — no version/release change. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix build failures (#937) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same * fix build failures --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (#933) Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7. - [Release notes](https://github.com/chimurai/http-proxy-middleware/releases) - [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md) - [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v3.0.5...v3.0.7) --- updated-dependencies: - dependency-name: http-proxy-middleware dependency-version: 3.0.7 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump dompurify from 3.4.2 to 3.4.11 (#934) Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.11) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.11 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (#935) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(gui/security): close remaining XSS vectors (#938) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor (#920) * chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.1 to 3.2.6. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 3.2.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * Add RelayMon trusted relay assertion publishing (#923) * Expose RelayMon trust observations as Nostr assertions RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses. The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture. Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring Confidence: high Scope-risk: moderate Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test . Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check * Keep docs lint focused on checked-in docs Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs. Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface Confidence: high Scope-risk: narrow Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md' Not-tested: Full GitHub Actions matrix before push * Keep RelayMon TRA assertions monitor-local RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data. Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events Constraint: monitors that do not publish TRA must not write detailed TRA observation history Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon Confidence: high Scope-risk: moderate Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator Tested: deno task test:config-types Tested: deno …
* chore(deps): bump ws from 8.18.0 to 8.20.1
Bumps [ws](https://github.com/websockets/ws) from 8.18.0 to 8.20.1.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.18.0...8.20.1)
---
updated-dependencies:
- dependency-name: ws
dependency-version: 8.20.1
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
* Add RelayMon trusted relay assertion publishing (#923)
* Expose RelayMon trust observations as Nostr assertions
RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses.
The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture.
Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags
Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks
Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating
Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring
Confidence: high
Scope-risk: moderate
Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults
Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test .
Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check
* Keep docs lint focused on checked-in docs
Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs.
Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request
Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface
Confidence: high
Scope-risk: narrow
Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md'
Not-tested: Full GitHub Actions matrix before push
* Keep RelayMon TRA assertions monitor-local
RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data.
Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events
Constraint: monitors that do not publish TRA must not write detailed TRA observation history
Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity
Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon
Confidence: high
Scope-risk: moderate
Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator
Tested: deno task test:config-types
Tested: deno task test:database
Tested: deno task test:kind30385
Tested: deno task test:unit
Tested: deno task compile
Tested: git diff --check
Tested: deno lint with existing noisy rules excluded on changed TypeScript files
Not-tested: full deno check remains blocked by existing project-wide strict type errors
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* Allow RelayMon TRA publication during warmup (#925)
Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended.
Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active.
Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence.
Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events.
Confidence: high
Scope-risk: narrow
Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder.
Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts
Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts
Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts
Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* fix(relaymon): move TRA per-check DB work off the check hot path (#926)
* Let remediation deletions publish after warmup
Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued.
Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression.
Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing.
Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller.
Confidence: high
Scope-risk: narrow
Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics.
Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts
Tested: git diff --check -- apps/relaymon/src/core/daemon.ts
Not-tested: live Fisherman deployment and relay ACK drain
* fix(relaymon): move TRA per-check DB work off the check hot path
Trusted Relay Assertion (kind 30385) processing ran inline on every relay
check: recordTrustedRelayObservation + buildTrustedRelayAssertion do
synchronous SQLite work, including a read-back of up to
max_observations_per_relay history rows plus per-sample scoring. On Deno's
single-threaded event loop (and a CPU-constrained host) those bursts
starved in-flight WebSocket I/O and busted the tight check budgets
(read/open timeouts), so healthy relays were falsely reported offline.
This is what took Fisherman down once TRA was enabled there.
Decouple TRA from the check hot path:
- processRelay now calls enqueueTrustedRelayObservation(), a cheap guard +
array push (no DB work). The queue is capped (drops oldest, warns) so it
can't grow unbounded during warmup.
- A lazily-started background processor drains the queue one relay at a
time and yields the event loop (delay) between relays, so the synchronous
per-relay work never sustains a block long enough to starve checks. Start
is deferred via setTimeout so the first item never runs inside the check's
call stack.
- Throttle is configurable via trustedRelayAssertions.processing_throttle_ms
(default 25ms).
publishTrustedRelayAssertion (the per-item worker) is unchanged, so its
record/build/gate/publish behavior and existing coverage are preserved.
Tests: add coverage that enqueue defers work (nothing published
synchronously; queue drains via processTrustedRelayQueueOnce), that it is a
no-op when TRA is disabled, and that the queue caps memory by dropping
oldest. Full unit suite: 369 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (#928)
Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml
workflows (whose `<pkg>-v*` tag triggers never matched the release's actual
`<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a
single matrix-driven workflow covering every package that produces a Docker
image: rstate, trawler, relaymon (nocapd excluded — deprecated).
Behaviour:
- push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for
ALL matrix packages.
- release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest`
when it is the highest non-prerelease version (computed from git tags).
- both paths runnable via workflow_dispatch (mode dev|tagged, package, version,
update_latest auto|true|false).
- every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR
(ghcr.io/sandwichfarm/<app>).
The build plan (which images, which tags, latest-or-not) lives in
scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so
the workflow logic is verifiable outside CI. Build contexts match each app's
Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root).
Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the
built-in GITHUB_TOKEN (packages: write).
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* rstate: publish trusted relay assertions from computed state (#922)
* Prevent stale rstate relay assertion cache hits
The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks.
This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only.
Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind.
Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next.
Confidence: high
Scope-risk: narrow
Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases.
Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit
Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing).
Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce.
Not-tested: full workspace recursive lint/build/test.
* Publish trusted relay assertions from computed rstate output
rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached.
Constraint: must not ingest kind 1066 for this slice
Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity
Confidence: high
Scope-risk: moderate
Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here
Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check
Not-tested: live relay publish round-trip against a production signer/relay pair
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* fix(relaymon): publish each relay deletion once, not on every boot (#932)
On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored
relay (~17k), because the "already deleted" guard was an in-memory Set that
resets each process. The deletion only needs to be sent once per ignore episode.
Persist the guard:
- relay_status gains a `deletion_published_at` column (idempotent migration).
- deleteRelayCheckEvent skips when the durable marker is set (or the in-memory
fast-path is) and stamps it on successful publish — so the once-only guarantee
survives restarts.
- IgnoreListSync.publishDeletions now only iterates ignored relays that are
actually pending (deletion_published_at = 0) instead of all of them.
- The marker is cleared whenever a relay becomes unignored (markRelayUnignored,
persistResult, dedup re-evaluation), so a later re-ignore republishes exactly
once (a prior deletion only covered the earlier 30166).
Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip,
cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite:
374 passed.
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(trawler): remove committed merge-conflict markers from CHANGELOG (#931)
A prior merge committed unresolved conflict markers into
apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side
held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content
only — no version/release change.
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix build failures (#937)
* test(quick/260503-kc3): add failing regression tests for remaining XSS vectors
- operators.formatters.test.ts: name/about/reference/filterFormatters
- monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters
- relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy
- DataTableFallback.test.ts: structural regression for {@html row[column.key]}
* fix(gui/security): sanitize remaining XSS sinks in dataTable formatters
- operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters
- monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo
- relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software
- softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit)
- test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver
* fix(gui/security): bind cell + filter fallbacks as text, not HTML
Replace the {@html} fallbacks in 4 components with split-conditional text
bindings. Registered formatters keep their {@html ...} branch (output is
sanitized as of the previous commit). Unregistered columns now render as
plain text — closes the silent regression vector where any column without
a formatter would re-introduce XSS.
- data-view/table/DataTable.svelte: fallback row[column.key] now text-bound
- lists/table/DataTable.svelte: same
- lists/table/Filters.svelte: filter button fallback now text-bound
- data-view/filters/DataViewFilters.svelte: same
* fix build failures
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (#933)
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v3.0.5...v3.0.7)
---
updated-dependencies:
- dependency-name: http-proxy-middleware
dependency-version: 3.0.7
dependency-type: direct:development
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore(deps): bump dompurify from 3.4.2 to 3.4.11 (#934)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.11)
---
updated-dependencies:
- dependency-name: dompurify
dependency-version: 3.4.11
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (#935)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0)
---
updated-dependencies:
- dependency-name: js-yaml
dependency-version: 4.2.0
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* fix(gui/security): close remaining XSS vectors (#938)
* test(quick/260503-kc3): add failing regression tests for remaining XSS vectors
- operators.formatters.test.ts: name/about/reference/filterFormatters
- monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters
- relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy
- DataTableFallback.test.ts: structural regression for {@html row[column.key]}
* fix(gui/security): sanitize remaining XSS sinks in dataTable formatters
- operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters
- monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo
- relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software
- softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit)
- test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver
* fix(gui/security): bind cell + filter fallbacks as text, not HTML
Replace the {@html} fallbacks in 4 components with split-conditional text
bindings. Registered formatters keep their {@html ...} branch (output is
sanitized as of the previous commit). Unregistered columns now render as
plain text — closes the silent regression vector where any column without
a formatter would re-introduce XSS.
- data-view/table/DataTable.svelte: fallback row[column.key] now text-bound
- lists/table/DataTable.svelte: same
- lists/table/Filters.svelte: filter button fallback now text-bound
- data-view/filters/DataViewFilters.svelte: same
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor (#920)
* chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.1 to 3.2.6.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest)
---
updated-dependencies:
- dependency-name: vitest
dependency-version: 3.2.6
dependency-type: direct:development
...
Signed-off-by: dependabot[bot] <support@github.com>
* Add RelayMon trusted relay assertion publishing (#923)
* Expose RelayMon trust observations as Nostr assertions
RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses.
The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture.
Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags
Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks
Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating
Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring
Confidence: high
Scope-risk: moderate
Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults
Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test .
Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check
* Keep docs lint focused on checked-in docs
Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs.
Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request
Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface
Confidence: high
Scope-risk: narrow
Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md'
Not-tested: Full GitHub Actions matrix before push
* Keep RelayMon TRA assertions monitor-local
RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data.
Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events
Constraint: monitors that do not publish TRA must not write detailed TRA observation history
Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity
Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon
Confidence: high
Scope-risk: moderate
Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator
Tested: deno task test:config-types
Tested: deno task test:database
Tested: deno task test:kind30385
Tested: deno task test:unit
Tested: deno task compile
Tested: git diff --check
Tested: deno lint with existing noisy rules excluded on changed TypeScript files
Not-tested: full deno check remains blocked by existing project-wide strict type errors
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* Allow RelayMon TRA publication during warmup (#925)
Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended.
Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active.
Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence.
Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events.
Confidence: high
Scope-risk: narrow
Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder.
Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts
Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts
Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts
Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* fix(relaymon): move TRA per-check DB work off the check hot path (#926)
* Let remediation deletions publish after warmup
Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued.
Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression.
Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing.
Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller.
Confidence: high
Scope-risk: narrow
Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics.
Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts
Tested: git diff --check -- apps/relaymon/src/core/daemon.ts
Not-tested: live Fisherman deployment and relay ACK drain
* fix(relaymon): move TRA per-check DB work off the check hot path
Trusted Relay Assertion (kind 30385) processing ran inline on every relay
check: recordTrustedRelayObservation + buildTrustedRelayAssertion do
synchronous SQLite work, including a read-back of up to
max_observations_per_relay history rows plus per-sample scoring. On Deno's
single-threaded event loop (and a CPU-constrained host) those bursts
starved in-flight WebSocket I/O and busted the tight check budgets
(read/open timeouts), so healthy relays were falsely reported offline.
This is what took Fisherman down once TRA was enabled there.
Decouple TRA from the check hot path:
- processRelay now calls enqueueTrustedRelayObservation(), a cheap guard +
array push (no DB work). The queue is capped (drops oldest, warns) so it
can't grow unbounded during warmup.
- A lazily-started background processor drains the queue one relay at a
time and yields the event loop (delay) between relays, so the synchronous
per-relay work never sustains a block long enough to starve checks. Start
is deferred via setTimeout so the first item never runs inside the check's
call stack.
- Throttle is configurable via trustedRelayAssertions.processing_throttle_ms
(default 25ms).
publishTrustedRelayAssertion (the per-item worker) is unchanged, so its
record/build/gate/publish behavior and existing coverage are preserved.
Tests: add coverage that enqueue defers work (nothing published
synchronously; queue drains via processTrustedRelayQueueOnce), that it is a
no-op when TRA is disabled, and that the queue caps memory by dropping
oldest. Full unit suite: 369 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (#928)
Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml
workflows (whose `<pkg>-v*` tag triggers never matched the release's actual
`<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a
single matrix-driven workflow covering every package that produces a Docker
image: rstate, trawler, relaymon (nocapd excluded — deprecated).
Behaviour:
- push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for
ALL matrix packages.
- release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest`
when it is the highest non-prerelease version (computed from git tags).
- both paths runnable via workflow_dispatch (mode dev|tagged, package, version,
update_latest auto|true|false).
- every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR
(ghcr.io/sandwichfarm/<app>).
The build plan (which images, which tags, latest-or-not) lives in
scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so
the workflow logic is verifiable outside CI. Build contexts match each app's
Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root).
Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the
built-in GITHUB_TOKEN (packages: write).
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* rstate: publish trusted relay assertions from computed state (#922)
* Prevent stale rstate relay assertion cache hits
The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks.
This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only.
Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind.
Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next.
Confidence: high
Scope-risk: narrow
Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases.
Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit
Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing).
Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce.
Not-tested: full workspace recursive lint/build/test.
* Publish trusted relay assertions from computed rstate output
rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached.
Constraint: must not ingest kind 1066 for this slice
Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity
Confidence: high
Scope-risk: moderate
Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here
Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check
Not-tested: live relay publish round-trip against a production signer/relay pair
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* fix(relaymon): publish each relay deletion once, not on every boot (#932)
On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored
relay (~17k), because the "already deleted" guard was an in-memory Set that
resets each process. The deletion only needs to be sent once per ignore episode.
Persist the guard:
- relay_status gains a `deletion_published_at` column (idempotent migration).
- deleteRelayCheckEvent skips when the durable marker is set (or the in-memory
fast-path is) and stamps it on successful publish — so the once-only guarantee
survives restarts.
- IgnoreListSync.publishDeletions now only iterates ignored relays that are
actually pending (deletion_published_at = 0) instead of all of them.
- The marker is cleared whenever a relay becomes unignored (markRelayUnignored,
persistResult, dedup re-evaluation), so a later re-ignore republishes exactly
once (a prior deletion only covered the earlier 30166).
Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip,
cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite:
374 passed.
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(trawler): remove committed merge-conflict markers from CHANGELOG (#931)
A prior merge committed unresolved conflict markers into
apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side
held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content
only — no version/release change.
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix build failures (#937)
* test(quick/260503-kc3): add failing regression tests for remaining XSS vectors
- operators.formatters.test.ts: name/about/reference/filterFormatters
- monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters
- relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy
- DataTableFallback.test.ts: structural regression for {@html row[column.key]}
* fix(gui/security): sanitize remaining XSS sinks in dataTable formatters
- operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters
- monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo
- relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software
- softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit)
- test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver
* fix(gui/security): bind cell + filter fallbacks as text, not HTML
Replace the {@html} fallbacks in 4 components with split-conditional text
bindings. Registered formatters keep their {@html ...} branch (output is
sanitized as of the previous commit). Unregistered columns now render as
plain text — closes the silent regression vector where any column without
a formatter would re-introduce XSS.
- data-view/table/DataTable.svelte: fallback row[column.key] now text-bound
- lists/table/DataTable.svelte: same
- lists/table/Filters.svelte: filter button fallback now text-bound
- data-view/filters/DataViewFilters.svelte: same
* fix build failures
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (#933)
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v3.0.5...v3.0.7)
---
updated-dependencies:
- dependency-name: http-proxy-middleware
dependency-version: 3.0.7
dependency-type: direct:development
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore(deps): bump dompurify from 3.4.2 to 3.4.11 (#934)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.11)
---
updated-dependencies:
- dependency-name: dompurify
dependency-version: 3.4.11
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (#935)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0)
---
updated-dependencies:
- dependency-name: js-yaml
dependency-version: 4.2.0
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: sandwich <299465+dskvr@users.noreply.github.com>
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(deps-dev): bump vitest from 3.1.2 to 3.2.6 in /libraries/schemata-js-ajv (#921)
* chore(deps-dev): bump vitest in /libraries/schemata-js-ajv
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.2 to 3.2.6.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest)
---
updated-dependencies:
- dependency-name: vitest
dependency-version: 3.2.6
dependency-type: direct:development
...
Signed-off-by: dependabot[bot] <support@github.com>
* Add RelayMon trusted relay assertion publishing (#923)
* Expose RelayMon trust observations as Nostr assertions
RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses.
The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture.
Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags
Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks
Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating
Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring
Confidence: high
Scope-risk: moderate
Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults
Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test .
Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check
* Keep docs lint focused on checked-in docs
Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs.
Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request
Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface
Confidence: high
Scope-risk: narrow
Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md'
Not-tested: Full GitHub Actions matrix before push
* Keep RelayMon TRA assertions monitor-local
RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data.
Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events
Constraint: monitors that do not publish TRA must not write detailed TRA observation history
Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity
Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon
Confidence: high
Scope-risk: moderate
Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator
Tested: deno task test:config-types
Tested: deno task test:database
Tested: deno task test:kind30385
Tested: deno task test:unit
Tested: deno task compile
Tested: git diff --check
Tested: deno lint with existing noisy rules excluded on changed TypeScript files
Not-tested: full deno check remains blocked by existing project-wide strict type errors
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* Allow RelayMon TRA publication during warmup (#925)
Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended.
Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active.
Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence.
Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events.
Confidence: high
Scope-risk: narrow
Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder.
Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts
Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts
Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts
Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* fix(relaymon): move TRA per-check DB work off the check hot path (#926)
* Let remediation deletions publish after warmup
Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued.
Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression.
Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing.
Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller.
Confidence: high
Scope-risk: narrow
Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics.
Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts
Tested: git diff --check -- apps/relaymon/src/core/daemon.ts
Not-tested: live Fisherman deployment and relay ACK drain
* fix(relaymon): move TRA per-check DB work off the check hot path
Trusted Relay Assertion (kind 30385) processing ran inline on every relay
check: recordTrustedRelayObservation + buildTrustedRelayAssertion do
synchronous SQLite work, including a read-back of up to
max_observations_per_relay history rows plus per-sample scoring. On Deno's
single-threaded event loop (and a CPU-constrained host) those bursts
starved in-flight WebSocket I/O and busted the tight check budgets
(read/open timeouts), so healthy relays were falsely reported offline.
This is what took Fisherman down once TRA was enabled there.
Decouple TRA from the check hot path:
- processRelay now calls enqueueTrustedRelayObservation(), a cheap guard +
array push (no DB work). The queue is capped (drops oldest, warns) so it
can't grow unbounded during warmup.
- A lazily-started background processor drains the queue one relay at a
time and yields the event loop (delay) between relays, so the synchronous
per-relay work never sustains a block long enough to starve checks. Start
is deferred via setTimeout so the first item never runs inside the check's
call stack.
- Throttle is configurable via trustedRelayAssertions.processing_throttle_ms
(default 25ms).
publishTrustedRelayAssertion (the per-item worker) is unchanged, so its
record/build/gate/publish behavior and existing coverage are preserved.
Tests: add coverage that enqueue defers work (nothing published
synchronously; queue drains via processTrustedRelayQueueOnce), that it is a
no-op when TRA is disabled, and that the queue caps memory by dropping
oldest. Full unit suite: 369 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (#928)
Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml
workflows (whose `<pkg>-v*` tag triggers never matched the release's actual
`<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a
single matrix-driven workflow covering every package that produces a Docker
image: rstate, trawler, relaymon (nocapd excluded — deprecated).
Behaviour:
- push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for
ALL matrix packages.
- release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest`
when it is the highest non-prerelease version (computed from git tags).
- both paths runnable via workflow_dispatch (mode dev|tagged, package, version,
update_latest auto|true|false).
- every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR
(ghcr.io/sandwichfarm/<app>).
The build plan (which images, which tags, latest-or-not) lives in
scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so
the workflow logic is verifiable outside CI. Build contexts match each app's
Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root).
Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the
built-in GITHUB_TOKEN (packages: write).
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* rstate: publish trusted relay assertions from computed state (#922)
* Prevent stale rstate relay assertion cache hits
The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks.
This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only.
Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind.
Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next.
Confidence: high
Scope-risk: narrow
Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases.
Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit
Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing).
Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce.
Not-tested: full workspace recursive lint/build/test.
* Publish trusted relay assertions from computed rstate output
rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached.
Constraint: must not ingest kind 1066 for this slice
Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity
Confidence: high
Scope-risk: moderate
Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here
Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check
Not-tested: live relay publish round-trip against a production signer/relay pair
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* fix(relaymon): publish each relay deletion once, not on every boot (#932)
On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored
relay (~17k), because the "already deleted" guard was an in-memory Set that
resets each process. The deletion only needs to be sent once per ignore episode.
Persist the guard:
- relay_status gains a `deletion_published_at` column (idempotent migration).
- deleteRelayCheckEvent skips when the durable marker is set (or the in-memory
fast-path is) and stamps it on successful publish — so the once-only guarantee
survives restarts.
- IgnoreListSync.publishDeletions now only iterates ignored relays that are
actually pending (deletion_published_at = 0) instead of all of them.
- The marker is cleared whenever a relay becomes unignored (markRelayUnignored,
persistResult, dedup re-evaluation), so a later re-ignore republishes exactly
once (a prior deletion only covered the earlier 30166).
Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip,
cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite:
374 passed.
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(trawler): remove committed merge-conflict markers from CHANGELOG (#931)
A prior merge committed unresolved conflict markers into
apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side
held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content
only — no version/release change.
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix build failures (#937)
* test(quick/260503-kc3): add failing regression tests for remaining XSS vectors
- operators.formatters.test.ts: name/about/reference/filterFormatters
- monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters
- relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy
- DataTableFallback.test.ts: structural regression for {@html row[column.key]}
* fix(gui/security): sanitize remaining XSS sinks in dataTable formatters
- operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters
- monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo
- relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software
- softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit)
- test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver
* fix(gui/security): bind cell + filter fallbacks as text, not HTML
Replace the {@html} fallbacks in 4 components with split-conditional text
bindings. Registered formatters keep their {@html ...} branch (output is
sanitized as of the previous commit). Unregistered columns now render as
plain text — closes the silent regression vector where any column without
a formatter would re-introduce XSS.
- data-view/table/DataTable.svelte: fallback row[column.key] now text-bound
- lists/table/DataTable.svelte: same
- lists/table/Filters.svelte: filter button fallback now text-bound
- data-view/filters/DataViewFilters.svelte: same
* fix build failures
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (#933)
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v3.0.5...v3.0.7)
---
updated-dependencies:
- dependency-name: http-proxy-middleware
dependency-version: 3.0.7
dependency-type: direct:development
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore(deps): bump dompurify from 3.4.2 to 3.4.11 (#934)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.11)
---
updated-dependencies:
- dependency-name: dompurify
dependency-version: 3.4.11
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (#935)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0)
---
updated-dependencies:
- dependency-name: js-yaml
dependency-version: 4.2.0
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* fix(gui/security): close remaining XSS vectors (#938)
* test(quick/260503-kc3): add failing regression tests for remaining XSS vectors
- operators.formatters.test.ts: name/about/reference/filterFormatters
- monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters
- relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy
- DataTableFallback.test.ts: structural regression for {@html row[column.key]}
* fix(gui/security): sanitize remaining XSS sinks in dataTable formatters
- operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters
- monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo
- relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software
- softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit)
- test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver
* fix(gui/security): bind cell + filter fallbacks as text, not HTML
Replace the {@html} fallbacks in 4 components with split-conditional text
bindings. Registered formatters keep their {@html ...} branch (output is
sanitized as of the previous commit). Unregistered columns now render as
plain text — closes the silent regression vector where any column without
a formatter would re-introduce XSS.
- data-view/table/DataTable.svelte: fallback row[column.key] now text-bound
- lists/table/DataTable.svelte: same
- lists/table/Filters.svelte: filter button fallback now text-bound
- data-view/filters/DataViewFilters.svelte: same
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor (#920)
* chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.1 to 3.2.6.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest)
---
updated-dependencies:
- dependency-name: vitest
dependency-version: 3.2.6
dependency-type: direct:development
...
Signed-off-by: dependabot[bot] <support@github.com>
* Add RelayMon trusted relay assertion publishing (#923)
* Expose RelayMon trust observations as Nostr assertions
RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses.
The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture.
Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags
Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks
Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating
Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring
Confidence: high
Scope-risk: moderate
Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults
Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test .
Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check
* Keep docs lint focused on checked-in docs
Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs.
Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request
Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface
Confidence: high
Scope-risk: narrow
Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md'
Not-tested: Full GitHub Actions matrix before push
* Keep RelayMon TRA assertions monitor-local
RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data.
Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events
Constraint: monitors that do not publish TRA must not write detailed TRA observation history
Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity
Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon
Confidence: high
Scope-risk: moderate
Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator
Tested: deno task test:config-types
Tested: deno task test:database
Tested: deno task te…
…rm#910) * chore(deps-dev): bump @sveltejs/kit from 2.59.0 to 2.60.1 Bumps [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit) from 2.59.0 to 2.60.1. - [Release notes](https://github.com/sveltejs/kit/releases) - [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md) - [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@2.60.1/packages/kit) --- updated-dependencies: - dependency-name: "@sveltejs/kit" dependency-version: 2.60.1 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * Add RelayMon trusted relay assertion publishing (#923) * Expose RelayMon trust observations as Nostr assertions RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses. The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture. Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring Confidence: high Scope-risk: moderate Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test . Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check * Keep docs lint focused on checked-in docs Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs. Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface Confidence: high Scope-risk: narrow Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md' Not-tested: Full GitHub Actions matrix before push * Keep RelayMon TRA assertions monitor-local RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data. Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events Constraint: monitors that do not publish TRA must not write detailed TRA observation history Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon Confidence: high Scope-risk: moderate Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator Tested: deno task test:config-types Tested: deno task test:database Tested: deno task test:kind30385 Tested: deno task test:unit Tested: deno task compile Tested: git diff --check Tested: deno lint with existing noisy rules excluded on changed TypeScript files Not-tested: full deno check remains blocked by existing project-wide strict type errors --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * Allow RelayMon TRA publication during warmup (#925) Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended. Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active. Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence. Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events. Confidence: high Scope-risk: narrow Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): move TRA per-check DB work off the check hot path (#926) * Let remediation deletions publish after warmup Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued. Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression. Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing. Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller. Confidence: high Scope-risk: narrow Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts Not-tested: live Fisherman deployment and relay ACK drain * fix(relaymon): move TRA per-check DB work off the check hot path Trusted Relay Assertion (kind 30385) processing ran inline on every relay check: recordTrustedRelayObservation + buildTrustedRelayAssertion do synchronous SQLite work, including a read-back of up to max_observations_per_relay history rows plus per-sample scoring. On Deno's single-threaded event loop (and a CPU-constrained host) those bursts starved in-flight WebSocket I/O and busted the tight check budgets (read/open timeouts), so healthy relays were falsely reported offline. This is what took Fisherman down once TRA was enabled there. Decouple TRA from the check hot path: - processRelay now calls enqueueTrustedRelayObservation(), a cheap guard + array push (no DB work). The queue is capped (drops oldest, warns) so it can't grow unbounded during warmup. - A lazily-started background processor drains the queue one relay at a time and yields the event loop (delay) between relays, so the synchronous per-relay work never sustains a block long enough to starve checks. Start is deferred via setTimeout so the first item never runs inside the check's call stack. - Throttle is configurable via trustedRelayAssertions.processing_throttle_ms (default 25ms). publishTrustedRelayAssertion (the per-item worker) is unchanged, so its record/build/gate/publish behavior and existing coverage are preserved. Tests: add coverage that enqueue defers work (nothing published synchronously; queue drains via processTrustedRelayQueueOnce), that it is a no-op when TRA is disabled, and that the queue caps memory by dropping oldest. Full unit suite: 369 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (#928) Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml workflows (whose `<pkg>-v*` tag triggers never matched the release's actual `<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a single matrix-driven workflow covering every package that produces a Docker image: rstate, trawler, relaymon (nocapd excluded — deprecated). Behaviour: - push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for ALL matrix packages. - release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest` when it is the highest non-prerelease version (computed from git tags). - both paths runnable via workflow_dispatch (mode dev|tagged, package, version, update_latest auto|true|false). - every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR (ghcr.io/sandwichfarm/<app>). The build plan (which images, which tags, latest-or-not) lives in scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so the workflow logic is verifiable outside CI. Build contexts match each app's Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root). Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the built-in GITHUB_TOKEN (packages: write). Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * rstate: publish trusted relay assertions from computed state (#922) * Prevent stale rstate relay assertion cache hits The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks. This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only. Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind. Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next. Confidence: high Scope-risk: narrow Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases. Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing). Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce. Not-tested: full workspace recursive lint/build/test. * Publish trusted relay assertions from computed rstate output rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached. Constraint: must not ingest kind 1066 for this slice Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity Confidence: high Scope-risk: moderate Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check Not-tested: live relay publish round-trip against a production signer/relay pair --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): publish each relay deletion once, not on every boot (#932) On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored relay (~17k), because the "already deleted" guard was an in-memory Set that resets each process. The deletion only needs to be sent once per ignore episode. Persist the guard: - relay_status gains a `deletion_published_at` column (idempotent migration). - deleteRelayCheckEvent skips when the durable marker is set (or the in-memory fast-path is) and stamps it on successful publish — so the once-only guarantee survives restarts. - IgnoreListSync.publishDeletions now only iterates ignored relays that are actually pending (deletion_published_at = 0) instead of all of them. - The marker is cleared whenever a relay becomes unignored (markRelayUnignored, persistResult, dedup re-evaluation), so a later re-ignore republishes exactly once (a prior deletion only covered the earlier 30166). Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip, cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite: 374 passed. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(trawler): remove committed merge-conflict markers from CHANGELOG (#931) A prior merge committed unresolved conflict markers into apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content only — no version/release change. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix build failures (#937) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same * fix build failures --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (#933) Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7. - [Release notes](https://github.com/chimurai/http-proxy-middleware/releases) - [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md) - [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v3.0.5...v3.0.7) --- updated-dependencies: - dependency-name: http-proxy-middleware dependency-version: 3.0.7 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump dompurify from 3.4.2 to 3.4.11 (#934) Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.11) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.11 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (#935) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(gui/security): close remaining XSS vectors (#938) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor (#920) * chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.1 to 3.2.6. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 3.2.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * Add RelayMon trusted relay assertion publishing (#923) * Expose RelayMon trust observations as Nostr assertions RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses. The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture. Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring Confidence: high Scope-risk: moderate Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test . Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check * Keep docs lint focused on checked-in docs Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs. Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface Confidence: high Scope-risk: narrow Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md' Not-tested: Full GitHub Actions matrix before push * Keep RelayMon TRA assertions monitor-local RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data. Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events Constraint: monitors that do not publish TRA must not write detailed TRA observation history Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon Confidence: high Scope-risk: moderate Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator Tested: deno task test:config-types Tested: deno task test:database Tested: deno task test:kind30385 Tested: deno task test:unit Tested: deno task compile Tested: git diff --check Tested: deno lint with existing noisy rules excluded on changed TypeScript files Not-tested: full deno check remains blocked by existing project-wide strict type errors --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * Allow RelayMon TRA publication during warmup (#925) Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended. Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active. Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence. Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events. Confidence: high Scope-risk: narrow Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): move TRA per-check DB work off the check hot path (#926) * Let remediation deletions publish after warmup Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued. Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression. Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing. Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller. Confidence: high Scope-risk: narrow Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts Not-tested: live Fisherman deployment and relay ACK drain * fix(relaymon): move TRA per-check DB work off the check hot path Trusted Relay Assertion (kind 30385) processing ran inline on every relay check: recordTrustedRelayObservation + buildTrustedRelayAssertion do synchronous SQLite work, including a read-back of up to max_observations_per_relay history rows plus per-sample scoring. On Deno's single-threaded event loop (and a CPU-constrained host) those bursts starved in-flight WebSocket I/O and busted the tight check budgets (read/open timeouts), so healthy relays were falsely reported offline. This is what took Fisherman down once TRA was enabled there. Decouple TRA from the check hot path: - processRelay now calls enqueueTrustedRelayObservation(), a cheap guard + array push (no DB work). The queue is capped (drops oldest, warns) so it can't grow unbounded during warmup. - A lazily-started background processor drains the queue one relay at a time and yields the event loop (delay) between relays, so the synchronous per-relay work never sustains a block long enough to starve checks. Start is deferred via setTimeout so the first item never runs inside the check's call stack. - Throttle is configurable via trustedRelayAssertions.processing_throttle_ms (default 25ms). publishTrustedRelayAssertion (the per-item worker) is unchanged, so its record/build/gate/publish behavior and existing coverage are preserved. Tests: add coverage that enqueue defers work (nothing published synchronously; queue drains via processTrustedRelayQueueOnce), that it is a no-op when TRA is disabled, and that the queue caps memory by dropping oldest. Full unit suite: 369 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (#928) Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml workflows (whose `<pkg>-v*` tag triggers never matched the release's actual `<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a single matrix-driven workflow covering every package that produces a Docker image: rstate, trawler, relaymon (nocapd excluded — deprecated). Behaviour: - push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for ALL matrix packages. - release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest` when it is the highest non-prerelease version (computed from git tags). - both paths runnable via workflow_dispatch (mode dev|tagged, package, version, update_latest auto|true|false). - every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR (ghcr.io/sandwichfarm/<app>). The build plan (which images, which tags, latest-or-not) lives in scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so the workflow logic is verifiable outside CI. Build contexts match each app's Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root). Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the built-in GITHUB_TOKEN (packages: write). Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * rstate: publish trusted relay assertions from computed state (#922) * Prevent stale rstate relay assertion cache hits The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks. This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only. Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind. Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next. Confidence: high Scope-risk: narrow Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases. Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing). Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce. Not-tested: full workspace recursive lint/build/test. * Publish trusted relay assertions from computed rstate output rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached. Constraint: must not ingest kind 1066 for this slice Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity Confidence: high Scope-risk: moderate Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check Not-tested: live relay publish round-trip against a production signer/relay pair --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): publish each relay deletion once, not on every boot (#932) On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored relay (~17k), because the "already deleted" guard was an in-memory Set that resets each process. The deletion only needs to be sent once per ignore episode. Persist the guard: - relay_status gains a `deletion_published_at` column (idempotent migration). - deleteRelayCheckEvent skips when the durable marker is set (or the in-memory fast-path is) and stamps it on successful publish — so the once-only guarantee survives restarts. - IgnoreListSync.publishDeletions now only iterates ignored relays that are actually pending (deletion_published_at = 0) instead of all of them. - The marker is cleared whenever a relay becomes unignored (markRelayUnignored, persistResult, dedup re-evaluation), so a later re-ignore republishes exactly once (a prior deletion only covered the earlier 30166). Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip, cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite: 374 passed. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(trawler): remove committed merge-conflict markers from CHANGELOG (#931) A prior merge committed unresolved conflict markers into apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content only — no version/release change. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix build failures (#937) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same * fix build failures --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (#933) Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7. - [Release notes](https://github.com/chimurai/http-proxy-middleware/releases) - [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md) - [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v3.0.5...v3.0.7) --- updated-dependencies: - dependency-name: http-proxy-middleware dependency-version: 3.0.7 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump dompurify from 3.4.2 to 3.4.11 (#934) Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.11) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.11 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (#935) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <299465+dskvr@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(deps-dev): bump vitest from 3.1.2 to 3.2.6 in /libraries/schemata-js-ajv (#921) * chore(deps-dev): bump vitest in /libraries/schemata-js-ajv Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.2 to 3.2.6. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 3.2.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * Add RelayMon trusted relay assertion publishing (#923) * Expose RelayMon trust observations as Nostr assertions RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses. The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture. Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring Confidence: high Scope-risk: moderate Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test . Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check * Keep docs lint focused on checked-in docs Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs. Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface Confidence: high Scope-risk: narrow Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md' Not-tested: Full GitHub Actions matrix before push * Keep RelayMon TRA assertions monitor-local RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data. Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events Constraint: monitors that do not publish TRA must not write detailed TRA observation history Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon Confidence: high Scope-risk: moderate Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator Tested: deno task test:config-types Tested: deno task test:database Tested: deno task test:kind30385 Tested: deno task test:unit Tested: deno task compile Tested: git diff --check Tested: deno lint with existing noisy rules excluded on changed TypeScript files Not-tested: full deno check remains blocked by existing project-wide strict type errors --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * Allow RelayMon TRA publication during warmup (#925) Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended. Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active. Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence. Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events. Confidence: high Scope-risk: narrow Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): move TRA per-check DB work off the check hot path (#926) * Let remediation deletions publish after warmup Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued. Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression. Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing. Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller. Confidence: high Scope-risk: narrow Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts Not-tested: live Fisherman deployment and relay ACK drain * fix(relaymon): move TRA per-check DB work off the check hot path Trusted Relay Assertion (kind 30385) processing ran inline on every relay check: recordTrustedRelayObservation + buildTrustedRelayAssertion do synchronous SQLite work, including a read-back of up to max_observations_per_relay history rows plus per-sample scoring. On Deno's single-threaded event loop (and a CPU-constrained host) those bursts starved in-flight WebSocket I/O and busted the tight check budgets (read/open timeouts), so healthy relays were falsely reported offline. This is what took Fisherman down once TRA was enabled there. Decouple TRA from the check hot path: - processRelay now calls enqueueTrustedRelayObservation(), a cheap guard + array push (no DB work). The queue is capped (drops oldest, warns) so it can't grow unbounded during warmup. - A lazily-started background processor drains the queue one relay at a time and yields the event loop (delay) between relays, so the synchronous per-relay work never sustains a block long enough to starve checks. Start is deferred via setTimeout so the first item never runs inside the check's call stack. - Throttle is configurable via trustedRelayAssertions.processing_throttle_ms (default 25ms). publishTrustedRelayAssertion (the per-item worker) is unchanged, so its record/build/gate/publish behavior and existing coverage are preserved. Tests: add coverage that enqueue defers work (nothing published synchronously; queue drains via processTrustedRelayQueueOnce), that it is a no-op when TRA is disabled, and that the queue caps memory by dropping oldest. Full unit suite: 369 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (#928) Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml workflows (whose `<pkg>-v*` tag triggers never matched the release's actual `<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a single matrix-driven workflow covering every package that produces a Docker image: rstate, trawler, relaymon (nocapd excluded — deprecated). Behaviour: - push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for ALL matrix packages. - release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest` when it is the highest non-prerelease version (computed from git tags). - both paths runnable via workflow_dispatch (mode dev|tagged, package, version, update_latest auto|true|false). - every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR (ghcr.io/sandwichfarm/<app>). The build plan (which images, which tags, latest-or-not) lives in scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so the workflow logic is verifiable outside CI. Build contexts match each app's Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root). Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the built-in GITHUB_TOKEN (packages: write). Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * rstate: publish trusted relay assertions from computed state (#922) * Prevent stale rstate relay assertion cache hits The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks. This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only. Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind. Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next. Confidence: high Scope-risk: narrow Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases. Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing). Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce. Not-tested: full workspace recursive lint/build/test. * Publish trusted relay assertions from computed rstate output rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached. Constraint: must not ingest kind 1066 for this slice Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity Confidence: high Scope-risk: moderate Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check Not-tested: live relay publish round-trip against a production signer/relay pair --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): publish each relay deletion once, not on every boot (#932) On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored relay (~17k), because the "already deleted" guard was an in-memory Set that resets each process. The deletion only needs to be sent once per ignore episode. Persist the guard: - relay_status gains a `deletion_published_at` column (idempotent migration). - deleteRelayCheckEvent skips when the durable marker is set (or the in-memory fast-path is) and stamps it on successful publish — so the once-only guarantee survives restarts. - IgnoreListSync.publishDeletions now only iterates ignored relays that are actually pending (deletion_published_at = 0) instead of all of them. - The marker is cleared whenever a relay becomes unignored (markRelayUnignored, persistResult, dedup re-evaluation), so a later re-ignore republishes exactly once (a prior deletion only covered the earlier 30166). Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip, cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite: 374 passed. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(trawler): remove committed merge-conflict markers from CHANGELOG (#931) A prior merge committed unresolved conflict markers into apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content only — no version/release change. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix build failures (#937) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same * fix build failures --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (#933) Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7. - [Release notes](https://github.com/chimurai/http-proxy-middleware/releases) - [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md) - [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v3.0.5...v3.0.7) --- updated-dependencies: - dependency-name: http-proxy-middleware dependency-version: 3.0.7 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump dompurify from 3.4.2 to 3.4.11 (#934) Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.11) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.11 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (#935) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(gui/security): close remaining XSS vectors (#938) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor (#920) * chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.1 to 3.2.6. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 3.2.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * Add RelayMon trusted relay assertion publishing (#923) * Expose RelayMon trust observations as Nostr assertions RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses. The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture. Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring Confidence: high Scope-risk: moderate Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test . Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check * Keep docs lint focused on checked-in docs Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs. Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface Confidence: high Scope-risk: narrow Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md' Not-tested: Full GitHub Actions matrix before push * Keep RelayMon TRA assertions monitor-local RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data. Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events Constraint: monitors that do not publish TRA must not write detailed TRA observation history Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon Confidence: high Scope-risk: moderate Directive: Keep RelayMon TRA scoring limited to this…
* chore(deps-dev): bump svelte from 5.53.5 to 5.55.7
Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.53.5 to 5.55.7.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.55.7/packages/svelte)
---
updated-dependencies:
- dependency-name: svelte
dependency-version: 5.55.7
dependency-type: direct:development
...
Signed-off-by: dependabot[bot] <support@github.com>
* Add RelayMon trusted relay assertion publishing (#923)
* Expose RelayMon trust observations as Nostr assertions
RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses.
The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture.
Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags
Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks
Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating
Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring
Confidence: high
Scope-risk: moderate
Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults
Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test .
Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check
* Keep docs lint focused on checked-in docs
Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs.
Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request
Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface
Confidence: high
Scope-risk: narrow
Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md'
Not-tested: Full GitHub Actions matrix before push
* Keep RelayMon TRA assertions monitor-local
RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data.
Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events
Constraint: monitors that do not publish TRA must not write detailed TRA observation history
Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity
Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon
Confidence: high
Scope-risk: moderate
Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator
Tested: deno task test:config-types
Tested: deno task test:database
Tested: deno task test:kind30385
Tested: deno task test:unit
Tested: deno task compile
Tested: git diff --check
Tested: deno lint with existing noisy rules excluded on changed TypeScript files
Not-tested: full deno check remains blocked by existing project-wide strict type errors
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* Allow RelayMon TRA publication during warmup (#925)
Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended.
Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active.
Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence.
Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events.
Confidence: high
Scope-risk: narrow
Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder.
Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts
Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts
Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts
Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* fix(relaymon): move TRA per-check DB work off the check hot path (#926)
* Let remediation deletions publish after warmup
Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued.
Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression.
Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing.
Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller.
Confidence: high
Scope-risk: narrow
Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics.
Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts
Tested: git diff --check -- apps/relaymon/src/core/daemon.ts
Not-tested: live Fisherman deployment and relay ACK drain
* fix(relaymon): move TRA per-check DB work off the check hot path
Trusted Relay Assertion (kind 30385) processing ran inline on every relay
check: recordTrustedRelayObservation + buildTrustedRelayAssertion do
synchronous SQLite work, including a read-back of up to
max_observations_per_relay history rows plus per-sample scoring. On Deno's
single-threaded event loop (and a CPU-constrained host) those bursts
starved in-flight WebSocket I/O and busted the tight check budgets
(read/open timeouts), so healthy relays were falsely reported offline.
This is what took Fisherman down once TRA was enabled there.
Decouple TRA from the check hot path:
- processRelay now calls enqueueTrustedRelayObservation(), a cheap guard +
array push (no DB work). The queue is capped (drops oldest, warns) so it
can't grow unbounded during warmup.
- A lazily-started background processor drains the queue one relay at a
time and yields the event loop (delay) between relays, so the synchronous
per-relay work never sustains a block long enough to starve checks. Start
is deferred via setTimeout so the first item never runs inside the check's
call stack.
- Throttle is configurable via trustedRelayAssertions.processing_throttle_ms
(default 25ms).
publishTrustedRelayAssertion (the per-item worker) is unchanged, so its
record/build/gate/publish behavior and existing coverage are preserved.
Tests: add coverage that enqueue defers work (nothing published
synchronously; queue drains via processTrustedRelayQueueOnce), that it is a
no-op when TRA is disabled, and that the queue caps memory by dropping
oldest. Full unit suite: 369 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (#928)
Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml
workflows (whose `<pkg>-v*` tag triggers never matched the release's actual
`<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a
single matrix-driven workflow covering every package that produces a Docker
image: rstate, trawler, relaymon (nocapd excluded — deprecated).
Behaviour:
- push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for
ALL matrix packages.
- release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest`
when it is the highest non-prerelease version (computed from git tags).
- both paths runnable via workflow_dispatch (mode dev|tagged, package, version,
update_latest auto|true|false).
- every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR
(ghcr.io/sandwichfarm/<app>).
The build plan (which images, which tags, latest-or-not) lives in
scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so
the workflow logic is verifiable outside CI. Build contexts match each app's
Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root).
Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the
built-in GITHUB_TOKEN (packages: write).
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* rstate: publish trusted relay assertions from computed state (#922)
* Prevent stale rstate relay assertion cache hits
The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks.
This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only.
Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind.
Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next.
Confidence: high
Scope-risk: narrow
Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases.
Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit
Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing).
Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce.
Not-tested: full workspace recursive lint/build/test.
* Publish trusted relay assertions from computed rstate output
rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached.
Constraint: must not ingest kind 1066 for this slice
Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity
Confidence: high
Scope-risk: moderate
Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here
Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check
Not-tested: live relay publish round-trip against a production signer/relay pair
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* fix(relaymon): publish each relay deletion once, not on every boot (#932)
On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored
relay (~17k), because the "already deleted" guard was an in-memory Set that
resets each process. The deletion only needs to be sent once per ignore episode.
Persist the guard:
- relay_status gains a `deletion_published_at` column (idempotent migration).
- deleteRelayCheckEvent skips when the durable marker is set (or the in-memory
fast-path is) and stamps it on successful publish — so the once-only guarantee
survives restarts.
- IgnoreListSync.publishDeletions now only iterates ignored relays that are
actually pending (deletion_published_at = 0) instead of all of them.
- The marker is cleared whenever a relay becomes unignored (markRelayUnignored,
persistResult, dedup re-evaluation), so a later re-ignore republishes exactly
once (a prior deletion only covered the earlier 30166).
Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip,
cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite:
374 passed.
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(trawler): remove committed merge-conflict markers from CHANGELOG (#931)
A prior merge committed unresolved conflict markers into
apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side
held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content
only — no version/release change.
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix build failures (#937)
* test(quick/260503-kc3): add failing regression tests for remaining XSS vectors
- operators.formatters.test.ts: name/about/reference/filterFormatters
- monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters
- relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy
- DataTableFallback.test.ts: structural regression for {@html row[column.key]}
* fix(gui/security): sanitize remaining XSS sinks in dataTable formatters
- operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters
- monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo
- relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software
- softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit)
- test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver
* fix(gui/security): bind cell + filter fallbacks as text, not HTML
Replace the {@html} fallbacks in 4 components with split-conditional text
bindings. Registered formatters keep their {@html ...} branch (output is
sanitized as of the previous commit). Unregistered columns now render as
plain text — closes the silent regression vector where any column without
a formatter would re-introduce XSS.
- data-view/table/DataTable.svelte: fallback row[column.key] now text-bound
- lists/table/DataTable.svelte: same
- lists/table/Filters.svelte: filter button fallback now text-bound
- data-view/filters/DataViewFilters.svelte: same
* fix build failures
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (#933)
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v3.0.5...v3.0.7)
---
updated-dependencies:
- dependency-name: http-proxy-middleware
dependency-version: 3.0.7
dependency-type: direct:development
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore(deps): bump dompurify from 3.4.2 to 3.4.11 (#934)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.11)
---
updated-dependencies:
- dependency-name: dompurify
dependency-version: 3.4.11
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (#935)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0)
---
updated-dependencies:
- dependency-name: js-yaml
dependency-version: 4.2.0
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* fix(gui/security): close remaining XSS vectors (#938)
* test(quick/260503-kc3): add failing regression tests for remaining XSS vectors
- operators.formatters.test.ts: name/about/reference/filterFormatters
- monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters
- relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy
- DataTableFallback.test.ts: structural regression for {@html row[column.key]}
* fix(gui/security): sanitize remaining XSS sinks in dataTable formatters
- operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters
- monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo
- relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software
- softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit)
- test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver
* fix(gui/security): bind cell + filter fallbacks as text, not HTML
Replace the {@html} fallbacks in 4 components with split-conditional text
bindings. Registered formatters keep their {@html ...} branch (output is
sanitized as of the previous commit). Unregistered columns now render as
plain text — closes the silent regression vector where any column without
a formatter would re-introduce XSS.
- data-view/table/DataTable.svelte: fallback row[column.key] now text-bound
- lists/table/DataTable.svelte: same
- lists/table/Filters.svelte: filter button fallback now text-bound
- data-view/filters/DataViewFilters.svelte: same
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor (#920)
* chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.1 to 3.2.6.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest)
---
updated-dependencies:
- dependency-name: vitest
dependency-version: 3.2.6
dependency-type: direct:development
...
Signed-off-by: dependabot[bot] <support@github.com>
* Add RelayMon trusted relay assertion publishing (#923)
* Expose RelayMon trust observations as Nostr assertions
RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses.
The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture.
Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags
Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks
Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating
Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring
Confidence: high
Scope-risk: moderate
Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults
Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test .
Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check
* Keep docs lint focused on checked-in docs
Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs.
Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request
Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface
Confidence: high
Scope-risk: narrow
Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md'
Not-tested: Full GitHub Actions matrix before push
* Keep RelayMon TRA assertions monitor-local
RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data.
Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events
Constraint: monitors that do not publish TRA must not write detailed TRA observation history
Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity
Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon
Confidence: high
Scope-risk: moderate
Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator
Tested: deno task test:config-types
Tested: deno task test:database
Tested: deno task test:kind30385
Tested: deno task test:unit
Tested: deno task compile
Tested: git diff --check
Tested: deno lint with existing noisy rules excluded on changed TypeScript files
Not-tested: full deno check remains blocked by existing project-wide strict type errors
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* Allow RelayMon TRA publication during warmup (#925)
Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended.
Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active.
Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence.
Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events.
Confidence: high
Scope-risk: narrow
Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder.
Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts
Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts
Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts
Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* fix(relaymon): move TRA per-check DB work off the check hot path (#926)
* Let remediation deletions publish after warmup
Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued.
Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression.
Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing.
Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller.
Confidence: high
Scope-risk: narrow
Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics.
Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts
Tested: git diff --check -- apps/relaymon/src/core/daemon.ts
Not-tested: live Fisherman deployment and relay ACK drain
* fix(relaymon): move TRA per-check DB work off the check hot path
Trusted Relay Assertion (kind 30385) processing ran inline on every relay
check: recordTrustedRelayObservation + buildTrustedRelayAssertion do
synchronous SQLite work, including a read-back of up to
max_observations_per_relay history rows plus per-sample scoring. On Deno's
single-threaded event loop (and a CPU-constrained host) those bursts
starved in-flight WebSocket I/O and busted the tight check budgets
(read/open timeouts), so healthy relays were falsely reported offline.
This is what took Fisherman down once TRA was enabled there.
Decouple TRA from the check hot path:
- processRelay now calls enqueueTrustedRelayObservation(), a cheap guard +
array push (no DB work). The queue is capped (drops oldest, warns) so it
can't grow unbounded during warmup.
- A lazily-started background processor drains the queue one relay at a
time and yields the event loop (delay) between relays, so the synchronous
per-relay work never sustains a block long enough to starve checks. Start
is deferred via setTimeout so the first item never runs inside the check's
call stack.
- Throttle is configurable via trustedRelayAssertions.processing_throttle_ms
(default 25ms).
publishTrustedRelayAssertion (the per-item worker) is unchanged, so its
record/build/gate/publish behavior and existing coverage are preserved.
Tests: add coverage that enqueue defers work (nothing published
synchronously; queue drains via processTrustedRelayQueueOnce), that it is a
no-op when TRA is disabled, and that the queue caps memory by dropping
oldest. Full unit suite: 369 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (#928)
Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml
workflows (whose `<pkg>-v*` tag triggers never matched the release's actual
`<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a
single matrix-driven workflow covering every package that produces a Docker
image: rstate, trawler, relaymon (nocapd excluded — deprecated).
Behaviour:
- push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for
ALL matrix packages.
- release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest`
when it is the highest non-prerelease version (computed from git tags).
- both paths runnable via workflow_dispatch (mode dev|tagged, package, version,
update_latest auto|true|false).
- every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR
(ghcr.io/sandwichfarm/<app>).
The build plan (which images, which tags, latest-or-not) lives in
scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so
the workflow logic is verifiable outside CI. Build contexts match each app's
Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root).
Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the
built-in GITHUB_TOKEN (packages: write).
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* rstate: publish trusted relay assertions from computed state (#922)
* Prevent stale rstate relay assertion cache hits
The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks.
This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only.
Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind.
Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next.
Confidence: high
Scope-risk: narrow
Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases.
Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit
Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing).
Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce.
Not-tested: full workspace recursive lint/build/test.
* Publish trusted relay assertions from computed rstate output
rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached.
Constraint: must not ingest kind 1066 for this slice
Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity
Confidence: high
Scope-risk: moderate
Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here
Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check
Not-tested: live relay publish round-trip against a production signer/relay pair
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* fix(relaymon): publish each relay deletion once, not on every boot (#932)
On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored
relay (~17k), because the "already deleted" guard was an in-memory Set that
resets each process. The deletion only needs to be sent once per ignore episode.
Persist the guard:
- relay_status gains a `deletion_published_at` column (idempotent migration).
- deleteRelayCheckEvent skips when the durable marker is set (or the in-memory
fast-path is) and stamps it on successful publish — so the once-only guarantee
survives restarts.
- IgnoreListSync.publishDeletions now only iterates ignored relays that are
actually pending (deletion_published_at = 0) instead of all of them.
- The marker is cleared whenever a relay becomes unignored (markRelayUnignored,
persistResult, dedup re-evaluation), so a later re-ignore republishes exactly
once (a prior deletion only covered the earlier 30166).
Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip,
cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite:
374 passed.
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(trawler): remove committed merge-conflict markers from CHANGELOG (#931)
A prior merge committed unresolved conflict markers into
apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side
held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content
only — no version/release change.
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix build failures (#937)
* test(quick/260503-kc3): add failing regression tests for remaining XSS vectors
- operators.formatters.test.ts: name/about/reference/filterFormatters
- monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters
- relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy
- DataTableFallback.test.ts: structural regression for {@html row[column.key]}
* fix(gui/security): sanitize remaining XSS sinks in dataTable formatters
- operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters
- monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo
- relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software
- softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit)
- test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver
* fix(gui/security): bind cell + filter fallbacks as text, not HTML
Replace the {@html} fallbacks in 4 components with split-conditional text
bindings. Registered formatters keep their {@html ...} branch (output is
sanitized as of the previous commit). Unregistered columns now render as
plain text — closes the silent regression vector where any column without
a formatter would re-introduce XSS.
- data-view/table/DataTable.svelte: fallback row[column.key] now text-bound
- lists/table/DataTable.svelte: same
- lists/table/Filters.svelte: filter button fallback now text-bound
- data-view/filters/DataViewFilters.svelte: same
* fix build failures
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (#933)
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v3.0.5...v3.0.7)
---
updated-dependencies:
- dependency-name: http-proxy-middleware
dependency-version: 3.0.7
dependency-type: direct:development
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore(deps): bump dompurify from 3.4.2 to 3.4.11 (#934)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.11)
---
updated-dependencies:
- dependency-name: dompurify
dependency-version: 3.4.11
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (#935)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0)
---
updated-dependencies:
- dependency-name: js-yaml
dependency-version: 4.2.0
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: sandwich <299465+dskvr@users.noreply.github.com>
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(deps-dev): bump vitest from 3.1.2 to 3.2.6 in /libraries/schemata-js-ajv (#921)
* chore(deps-dev): bump vitest in /libraries/schemata-js-ajv
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.2 to 3.2.6.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest)
---
updated-dependencies:
- dependency-name: vitest
dependency-version: 3.2.6
dependency-type: direct:development
...
Signed-off-by: dependabot[bot] <support@github.com>
* Add RelayMon trusted relay assertion publishing (#923)
* Expose RelayMon trust observations as Nostr assertions
RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses.
The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture.
Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags
Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks
Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating
Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring
Confidence: high
Scope-risk: moderate
Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults
Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test .
Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check
* Keep docs lint focused on checked-in docs
Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs.
Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request
Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface
Confidence: high
Scope-risk: narrow
Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md'
Not-tested: Full GitHub Actions matrix before push
* Keep RelayMon TRA assertions monitor-local
RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data.
Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events
Constraint: monitors that do not publish TRA must not write detailed TRA observation history
Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity
Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon
Confidence: high
Scope-risk: moderate
Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator
Tested: deno task test:config-types
Tested: deno task test:database
Tested: deno task test:kind30385
Tested: deno task test:unit
Tested: deno task compile
Tested: git diff --check
Tested: deno lint with existing noisy rules excluded on changed TypeScript files
Not-tested: full deno check remains blocked by existing project-wide strict type errors
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* Allow RelayMon TRA publication during warmup (#925)
Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended.
Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active.
Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence.
Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events.
Confidence: high
Scope-risk: narrow
Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder.
Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts
Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts
Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts
Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* fix(relaymon): move TRA per-check DB work off the check hot path (#926)
* Let remediation deletions publish after warmup
Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued.
Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression.
Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing.
Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller.
Confidence: high
Scope-risk: narrow
Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics.
Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts
Tested: git diff --check -- apps/relaymon/src/core/daemon.ts
Not-tested: live Fisherman deployment and relay ACK drain
* fix(relaymon): move TRA per-check DB work off the check hot path
Trusted Relay Assertion (kind 30385) processing ran inline on every relay
check: recordTrustedRelayObservation + buildTrustedRelayAssertion do
synchronous SQLite work, including a read-back of up to
max_observations_per_relay history rows plus per-sample scoring. On Deno's
single-threaded event loop (and a CPU-constrained host) those bursts
starved in-flight WebSocket I/O and busted the tight check budgets
(read/open timeouts), so healthy relays were falsely reported offline.
This is what took Fisherman down once TRA was enabled there.
Decouple TRA from the check hot path:
- processRelay now calls enqueueTrustedRelayObservation(), a cheap guard +
array push (no DB work). The queue is capped (drops oldest, warns) so it
can't grow unbounded during warmup.
- A lazily-started background processor drains the queue one relay at a
time and yields the event loop (delay) between relays, so the synchronous
per-relay work never sustains a block long enough to starve checks. Start
is deferred via setTimeout so the first item never runs inside the check's
call stack.
- Throttle is configurable via trustedRelayAssertions.processing_throttle_ms
(default 25ms).
publishTrustedRelayAssertion (the per-item worker) is unchanged, so its
record/build/gate/publish behavior and existing coverage are preserved.
Tests: add coverage that enqueue defers work (nothing published
synchronously; queue drains via processTrustedRelayQueueOnce), that it is a
no-op when TRA is disabled, and that the queue caps memory by dropping
oldest. Full unit suite: 369 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (#928)
Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml
workflows (whose `<pkg>-v*` tag triggers never matched the release's actual
`<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a
single matrix-driven workflow covering every package that produces a Docker
image: rstate, trawler, relaymon (nocapd excluded — deprecated).
Behaviour:
- push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for
ALL matrix packages.
- release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest`
when it is the highest non-prerelease version (computed from git tags).
- both paths runnable via workflow_dispatch (mode dev|tagged, package, version,
update_latest auto|true|false).
- every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR
(ghcr.io/sandwichfarm/<app>).
The build plan (which images, which tags, latest-or-not) lives in
scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so
the workflow logic is verifiable outside CI. Build contexts match each app's
Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root).
Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the
built-in GITHUB_TOKEN (packages: write).
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* rstate: publish trusted relay assertions from computed state (#922)
* Prevent stale rstate relay assertion cache hits
The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks.
This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only.
Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind.
Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next.
Confidence: high
Scope-risk: narrow
Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases.
Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit
Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing).
Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce.
Not-tested: full workspace recursive lint/build/test.
* Publish trusted relay assertions from computed rstate output
rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached.
Constraint: must not ingest kind 1066 for this slice
Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity
Confidence: high
Scope-risk: moderate
Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here
Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check
Not-tested: live relay publish round-trip against a production signer/relay pair
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* fix(relaymon): publish each relay deletion once, not on every boot (#932)
On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored
relay (~17k), because the "already deleted" guard was an in-memory Set that
resets each process. The deletion only needs to be sent once per ignore episode.
Persist the guard:
- relay_status gains a `deletion_published_at` column (idempotent migration).
- deleteRelayCheckEvent skips when the durable marker is set (or the in-memory
fast-path is) and stamps it on successful publish — so the once-only guarantee
survives restarts.
- IgnoreListSync.publishDeletions now only iterates ignored relays that are
actually pending (deletion_published_at = 0) instead of all of them.
- The marker is cleared whenever a relay becomes unignored (markRelayUnignored,
persistResult, dedup re-evaluation), so a later re-ignore republishes exactly
once (a prior deletion only covered the earlier 30166).
Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip,
cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite:
374 passed.
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(trawler): remove committed merge-conflict markers from CHANGELOG (#931)
A prior merge committed unresolved conflict markers into
apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side
held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content
only — no version/release change.
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix build failures (#937)
* test(quick/260503-kc3): add failing regression tests for remaining XSS vectors
- operators.formatters.test.ts: name/about/reference/filterFormatters
- monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters
- relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy
- DataTableFallback.test.ts: structural regression for {@html row[column.key]}
* fix(gui/security): sanitize remaining XSS sinks in dataTable formatters
- operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters
- monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo
- relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software
- softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit)
- test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver
* fix(gui/security): bind cell + filter fallbacks as text, not HTML
Replace the {@html} fallbacks in 4 components with split-conditional text
bindings. Registered formatters keep their {@html ...} branch (output is
sanitized as of the previous commit). Unregistered columns now render as
plain text — closes the silent regression vector where any column without
a formatter would re-introduce XSS.
- data-view/table/DataTable.svelte: fallback row[column.key] now text-bound
- lists/table/DataTable.svelte: same
- lists/table/Filters.svelte: filter button fallback now text-bound
- data-view/filters/DataViewFilters.svelte: same
* fix build failures
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (#933)
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v3.0.5...v3.0.7)
---
updated-dependencies:
- dependency-name: http-proxy-middleware
dependency-version: 3.0.7
dependency-type: direct:development
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore(deps): bump dompurify from 3.4.2 to 3.4.11 (#934)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.11)
---
updated-dependencies:
- dependency-name: dompurify
dependency-version: 3.4.11
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (#935)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0)
---
updated-dependencies:
- dependency-name: js-yaml
dependency-version: 4.2.0
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* fix(gui/security): close remaining XSS vectors (#938)
* test(quick/260503-kc3): add failing regression tests for remaining XSS vectors
- operators.formatters.test.ts: name/about/reference/filterFormatters
- monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters
- relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy
- DataTableFallback.test.ts: structural regression for {@html row[column.key]}
* fix(gui/security): sanitize remaining XSS sinks in dataTable formatters
- operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters
- monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo
- relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software
- softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit)
- test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver
* fix(gui/security): bind cell + filter fallbacks as text, not HTML
Replace the {@html} fallbacks in 4 components with split-conditional text
bindings. Registered formatters keep their {@html ...} branch (output is
sanitized as of the previous commit). Unregistered columns now render as
plain text — closes the silent regression vector where any column without
a formatter would re-introduce XSS.
- data-view/table/DataTable.svelte: fallback row[column.key] now text-bound
- lists/table/DataTable.svelte: same
- lists/table/Filters.svelte: filter button fallback now text-bound
- data-view/filters/DataViewFilters.svelte: same
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor (#920)
* chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.1 to 3.2.6.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest)
---
updated-dependencies:
- dependency-name: vitest
dependency-version: 3.2.6
dependency-type: direct:development
...
Signed-off-by: dependabot[bot] <support@github.com>
* Add RelayMon trusted relay assertion publishing (#923)
* Expose RelayMon trust observations as Nostr assertions
RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses.
The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture.
Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags
Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks
Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating
Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring
Confidence: high
Scope-risk: moderate
Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults
Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test .
Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check
* Keep docs lint focused on checked-in docs
Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs.
Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request
Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface
Confidence: high
Scope-risk: narrow
Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md'
Not-tested: Full GitHub Actions matrix before push
* Keep RelayMon TRA assertions monitor-local
RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data.
Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events
Constraint: monitors that do not publish TRA must not write detailed TRA observation history
Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity
Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon
Confidence: high
Scope-risk: moderate
Directive: Keep RelayMon TRA scoring limited to this monitor's obser…
…sandwichfarm#919) * chore(deps-dev): bump vitest from 0.34.6 to 3.2.6 in /libraries/nocap Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 0.34.6 to 3.2.6. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 3.2.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * Add RelayMon trusted relay assertion publishing (#923) * Expose RelayMon trust observations as Nostr assertions RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses. The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture. Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring Confidence: high Scope-risk: moderate Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test . Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check * Keep docs lint focused on checked-in docs Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs. Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface Confidence: high Scope-risk: narrow Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md' Not-tested: Full GitHub Actions matrix before push * Keep RelayMon TRA assertions monitor-local RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data. Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events Constraint: monitors that do not publish TRA must not write detailed TRA observation history Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon Confidence: high Scope-risk: moderate Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator Tested: deno task test:config-types Tested: deno task test:database Tested: deno task test:kind30385 Tested: deno task test:unit Tested: deno task compile Tested: git diff --check Tested: deno lint with existing noisy rules excluded on changed TypeScript files Not-tested: full deno check remains blocked by existing project-wide strict type errors --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * Allow RelayMon TRA publication during warmup (#925) Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended. Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active. Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence. Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events. Confidence: high Scope-risk: narrow Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): move TRA per-check DB work off the check hot path (#926) * Let remediation deletions publish after warmup Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued. Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression. Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing. Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller. Confidence: high Scope-risk: narrow Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts Not-tested: live Fisherman deployment and relay ACK drain * fix(relaymon): move TRA per-check DB work off the check hot path Trusted Relay Assertion (kind 30385) processing ran inline on every relay check: recordTrustedRelayObservation + buildTrustedRelayAssertion do synchronous SQLite work, including a read-back of up to max_observations_per_relay history rows plus per-sample scoring. On Deno's single-threaded event loop (and a CPU-constrained host) those bursts starved in-flight WebSocket I/O and busted the tight check budgets (read/open timeouts), so healthy relays were falsely reported offline. This is what took Fisherman down once TRA was enabled there. Decouple TRA from the check hot path: - processRelay now calls enqueueTrustedRelayObservation(), a cheap guard + array push (no DB work). The queue is capped (drops oldest, warns) so it can't grow unbounded during warmup. - A lazily-started background processor drains the queue one relay at a time and yields the event loop (delay) between relays, so the synchronous per-relay work never sustains a block long enough to starve checks. Start is deferred via setTimeout so the first item never runs inside the check's call stack. - Throttle is configurable via trustedRelayAssertions.processing_throttle_ms (default 25ms). publishTrustedRelayAssertion (the per-item worker) is unchanged, so its record/build/gate/publish behavior and existing coverage are preserved. Tests: add coverage that enqueue defers work (nothing published synchronously; queue drains via processTrustedRelayQueueOnce), that it is a no-op when TRA is disabled, and that the queue caps memory by dropping oldest. Full unit suite: 369 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (#928) Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml workflows (whose `<pkg>-v*` tag triggers never matched the release's actual `<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a single matrix-driven workflow covering every package that produces a Docker image: rstate, trawler, relaymon (nocapd excluded — deprecated). Behaviour: - push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for ALL matrix packages. - release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest` when it is the highest non-prerelease version (computed from git tags). - both paths runnable via workflow_dispatch (mode dev|tagged, package, version, update_latest auto|true|false). - every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR (ghcr.io/sandwichfarm/<app>). The build plan (which images, which tags, latest-or-not) lives in scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so the workflow logic is verifiable outside CI. Build contexts match each app's Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root). Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the built-in GITHUB_TOKEN (packages: write). Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * rstate: publish trusted relay assertions from computed state (#922) * Prevent stale rstate relay assertion cache hits The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks. This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only. Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind. Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next. Confidence: high Scope-risk: narrow Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases. Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing). Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce. Not-tested: full workspace recursive lint/build/test. * Publish trusted relay assertions from computed rstate output rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached. Constraint: must not ingest kind 1066 for this slice Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity Confidence: high Scope-risk: moderate Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check Not-tested: live relay publish round-trip against a production signer/relay pair --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): publish each relay deletion once, not on every boot (#932) On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored relay (~17k), because the "already deleted" guard was an in-memory Set that resets each process. The deletion only needs to be sent once per ignore episode. Persist the guard: - relay_status gains a `deletion_published_at` column (idempotent migration). - deleteRelayCheckEvent skips when the durable marker is set (or the in-memory fast-path is) and stamps it on successful publish — so the once-only guarantee survives restarts. - IgnoreListSync.publishDeletions now only iterates ignored relays that are actually pending (deletion_published_at = 0) instead of all of them. - The marker is cleared whenever a relay becomes unignored (markRelayUnignored, persistResult, dedup re-evaluation), so a later re-ignore republishes exactly once (a prior deletion only covered the earlier 30166). Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip, cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite: 374 passed. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(trawler): remove committed merge-conflict markers from CHANGELOG (#931) A prior merge committed unresolved conflict markers into apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content only — no version/release change. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix build failures (#937) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same * fix build failures --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (#933) Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7. - [Release notes](https://github.com/chimurai/http-proxy-middleware/releases) - [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md) - [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v3.0.5...v3.0.7) --- updated-dependencies: - dependency-name: http-proxy-middleware dependency-version: 3.0.7 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump dompurify from 3.4.2 to 3.4.11 (#934) Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.11) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.11 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (#935) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(gui/security): close remaining XSS vectors (#938) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor (#920) * chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.1 to 3.2.6. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 3.2.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * Add RelayMon trusted relay assertion publishing (#923) * Expose RelayMon trust observations as Nostr assertions RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses. The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture. Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring Confidence: high Scope-risk: moderate Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test . Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check * Keep docs lint focused on checked-in docs Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs. Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface Confidence: high Scope-risk: narrow Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md' Not-tested: Full GitHub Actions matrix before push * Keep RelayMon TRA assertions monitor-local RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data. Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events Constraint: monitors that do not publish TRA must not write detailed TRA observation history Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon Confidence: high Scope-risk: moderate Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator Tested: deno task test:config-types Tested: deno task test:database Tested: deno task test:kind30385 Tested: deno task test:unit Tested: deno task compile Tested: git diff --check Tested: deno lint with existing noisy rules excluded on changed TypeScript files Not-tested: full deno check remains blocked by existing project-wide strict type errors --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * Allow RelayMon TRA publication during warmup (#925) Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended. Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active. Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence. Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events. Confidence: high Scope-risk: narrow Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): move TRA per-check DB work off the check hot path (#926) * Let remediation deletions publish after warmup Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued. Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression. Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing. Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller. Confidence: high Scope-risk: narrow Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts Not-tested: live Fisherman deployment and relay ACK drain * fix(relaymon): move TRA per-check DB work off the check hot path Trusted Relay Assertion (kind 30385) processing ran inline on every relay check: recordTrustedRelayObservation + buildTrustedRelayAssertion do synchronous SQLite work, including a read-back of up to max_observations_per_relay history rows plus per-sample scoring. On Deno's single-threaded event loop (and a CPU-constrained host) those bursts starved in-flight WebSocket I/O and busted the tight check budgets (read/open timeouts), so healthy relays were falsely reported offline. This is what took Fisherman down once TRA was enabled there. Decouple TRA from the check hot path: - processRelay now calls enqueueTrustedRelayObservation(), a cheap guard + array push (no DB work). The queue is capped (drops oldest, warns) so it can't grow unbounded during warmup. - A lazily-started background processor drains the queue one relay at a time and yields the event loop (delay) between relays, so the synchronous per-relay work never sustains a block long enough to starve checks. Start is deferred via setTimeout so the first item never runs inside the check's call stack. - Throttle is configurable via trustedRelayAssertions.processing_throttle_ms (default 25ms). publishTrustedRelayAssertion (the per-item worker) is unchanged, so its record/build/gate/publish behavior and existing coverage are preserved. Tests: add coverage that enqueue defers work (nothing published synchronously; queue drains via processTrustedRelayQueueOnce), that it is a no-op when TRA is disabled, and that the queue caps memory by dropping oldest. Full unit suite: 369 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (#928) Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml workflows (whose `<pkg>-v*` tag triggers never matched the release's actual `<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a single matrix-driven workflow covering every package that produces a Docker image: rstate, trawler, relaymon (nocapd excluded — deprecated). Behaviour: - push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for ALL matrix packages. - release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest` when it is the highest non-prerelease version (computed from git tags). - both paths runnable via workflow_dispatch (mode dev|tagged, package, version, update_latest auto|true|false). - every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR (ghcr.io/sandwichfarm/<app>). The build plan (which images, which tags, latest-or-not) lives in scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so the workflow logic is verifiable outside CI. Build contexts match each app's Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root). Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the built-in GITHUB_TOKEN (packages: write). Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * rstate: publish trusted relay assertions from computed state (#922) * Prevent stale rstate relay assertion cache hits The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks. This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only. Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind. Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next. Confidence: high Scope-risk: narrow Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases. Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing). Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce. Not-tested: full workspace recursive lint/build/test. * Publish trusted relay assertions from computed rstate output rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached. Constraint: must not ingest kind 1066 for this slice Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity Confidence: high Scope-risk: moderate Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check Not-tested: live relay publish round-trip against a production signer/relay pair --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): publish each relay deletion once, not on every boot (#932) On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored relay (~17k), because the "already deleted" guard was an in-memory Set that resets each process. The deletion only needs to be sent once per ignore episode. Persist the guard: - relay_status gains a `deletion_published_at` column (idempotent migration). - deleteRelayCheckEvent skips when the durable marker is set (or the in-memory fast-path is) and stamps it on successful publish — so the once-only guarantee survives restarts. - IgnoreListSync.publishDeletions now only iterates ignored relays that are actually pending (deletion_published_at = 0) instead of all of them. - The marker is cleared whenever a relay becomes unignored (markRelayUnignored, persistResult, dedup re-evaluation), so a later re-ignore republishes exactly once (a prior deletion only covered the earlier 30166). Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip, cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite: 374 passed. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(trawler): remove committed merge-conflict markers from CHANGELOG (#931) A prior merge committed unresolved conflict markers into apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content only — no version/release change. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix build failures (#937) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same * fix build failures --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (#933) Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7. - [Release notes](https://github.com/chimurai/http-proxy-middleware/releases) - [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md) - [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v3.0.5...v3.0.7) --- updated-dependencies: - dependency-name: http-proxy-middleware dependency-version: 3.0.7 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump dompurify from 3.4.2 to 3.4.11 (#934) Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.11) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.11 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (#935) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <299465+dskvr@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(deps-dev): bump vitest from 3.1.2 to 3.2.6 in /libraries/schemata-js-ajv (#921) * chore(deps-dev): bump vitest in /libraries/schemata-js-ajv Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.2 to 3.2.6. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 3.2.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * Add RelayMon trusted relay assertion publishing (#923) * Expose RelayMon trust observations as Nostr assertions RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses. The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture. Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring Confidence: high Scope-risk: moderate Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test . Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check * Keep docs lint focused on checked-in docs Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs. Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface Confidence: high Scope-risk: narrow Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md' Not-tested: Full GitHub Actions matrix before push * Keep RelayMon TRA assertions monitor-local RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data. Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events Constraint: monitors that do not publish TRA must not write detailed TRA observation history Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon Confidence: high Scope-risk: moderate Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator Tested: deno task test:config-types Tested: deno task test:database Tested: deno task test:kind30385 Tested: deno task test:unit Tested: deno task compile Tested: git diff --check Tested: deno lint with existing noisy rules excluded on changed TypeScript files Not-tested: full deno check remains blocked by existing project-wide strict type errors --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * Allow RelayMon TRA publication during warmup (#925) Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended. Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active. Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence. Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events. Confidence: high Scope-risk: narrow Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): move TRA per-check DB work off the check hot path (#926) * Let remediation deletions publish after warmup Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued. Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression. Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing. Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller. Confidence: high Scope-risk: narrow Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts Not-tested: live Fisherman deployment and relay ACK drain * fix(relaymon): move TRA per-check DB work off the check hot path Trusted Relay Assertion (kind 30385) processing ran inline on every relay check: recordTrustedRelayObservation + buildTrustedRelayAssertion do synchronous SQLite work, including a read-back of up to max_observations_per_relay history rows plus per-sample scoring. On Deno's single-threaded event loop (and a CPU-constrained host) those bursts starved in-flight WebSocket I/O and busted the tight check budgets (read/open timeouts), so healthy relays were falsely reported offline. This is what took Fisherman down once TRA was enabled there. Decouple TRA from the check hot path: - processRelay now calls enqueueTrustedRelayObservation(), a cheap guard + array push (no DB work). The queue is capped (drops oldest, warns) so it can't grow unbounded during warmup. - A lazily-started background processor drains the queue one relay at a time and yields the event loop (delay) between relays, so the synchronous per-relay work never sustains a block long enough to starve checks. Start is deferred via setTimeout so the first item never runs inside the check's call stack. - Throttle is configurable via trustedRelayAssertions.processing_throttle_ms (default 25ms). publishTrustedRelayAssertion (the per-item worker) is unchanged, so its record/build/gate/publish behavior and existing coverage are preserved. Tests: add coverage that enqueue defers work (nothing published synchronously; queue drains via processTrustedRelayQueueOnce), that it is a no-op when TRA is disabled, and that the queue caps memory by dropping oldest. Full unit suite: 369 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (#928) Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml workflows (whose `<pkg>-v*` tag triggers never matched the release's actual `<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a single matrix-driven workflow covering every package that produces a Docker image: rstate, trawler, relaymon (nocapd excluded — deprecated). Behaviour: - push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for ALL matrix packages. - release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest` when it is the highest non-prerelease version (computed from git tags). - both paths runnable via workflow_dispatch (mode dev|tagged, package, version, update_latest auto|true|false). - every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR (ghcr.io/sandwichfarm/<app>). The build plan (which images, which tags, latest-or-not) lives in scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so the workflow logic is verifiable outside CI. Build contexts match each app's Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root). Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the built-in GITHUB_TOKEN (packages: write). Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * rstate: publish trusted relay assertions from computed state (#922) * Prevent stale rstate relay assertion cache hits The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks. This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only. Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind. Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next. Confidence: high Scope-risk: narrow Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases. Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing). Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce. Not-tested: full workspace recursive lint/build/test. * Publish trusted relay assertions from computed rstate output rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached. Constraint: must not ingest kind 1066 for this slice Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity Confidence: high Scope-risk: moderate Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check Not-tested: live relay publish round-trip against a production signer/relay pair --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): publish each relay deletion once, not on every boot (#932) On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored relay (~17k), because the "already deleted" guard was an in-memory Set that resets each process. The deletion only needs to be sent once per ignore episode. Persist the guard: - relay_status gains a `deletion_published_at` column (idempotent migration). - deleteRelayCheckEvent skips when the durable marker is set (or the in-memory fast-path is) and stamps it on successful publish — so the once-only guarantee survives restarts. - IgnoreListSync.publishDeletions now only iterates ignored relays that are actually pending (deletion_published_at = 0) instead of all of them. - The marker is cleared whenever a relay becomes unignored (markRelayUnignored, persistResult, dedup re-evaluation), so a later re-ignore republishes exactly once (a prior deletion only covered the earlier 30166). Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip, cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite: 374 passed. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(trawler): remove committed merge-conflict markers from CHANGELOG (#931) A prior merge committed unresolved conflict markers into apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content only — no version/release change. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix build failures (#937) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same * fix build failures --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (#933) Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7. - [Release notes](https://github.com/chimurai/http-proxy-middleware/releases) - [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md) - [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v3.0.5...v3.0.7) --- updated-dependencies: - dependency-name: http-proxy-middleware dependency-version: 3.0.7 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump dompurify from 3.4.2 to 3.4.11 (#934) Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.11) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.11 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (#935) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(gui/security): close remaining XSS vectors (#938) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor (#920) * chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.1 to 3.2.6. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 3.2.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * Add RelayMon trusted relay assertion publishing (#923) * Expose RelayMon trust observations as Nostr assertions RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses. The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture. Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring Confidence: high Scope-risk: moderate Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test . Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check * Keep docs lint focused on checked-in docs Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs. Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface Confidence: high Scope-risk: narrow Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md' Not-tested: Full GitHub Actions matrix before push * Keep RelayMon TRA assertions monitor-local RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data. Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events Constraint: monitors that do not publish TRA must not write detailed TRA observation history Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon Confidence: high Scope-risk: moderate Directive: Keep RelayMon TRA scoring limite…
…/adapters/default/WebsocketAdapterDefault (sandwichfarm#914) * chore(deps-dev): bump vitest Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 0.34.6 to 4.1.0. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.0/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 4.1.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * Add RelayMon trusted relay assertion publishing (#923) * Expose RelayMon trust observations as Nostr assertions RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses. The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture. Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring Confidence: high Scope-risk: moderate Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test . Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check * Keep docs lint focused on checked-in docs Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs. Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface Confidence: high Scope-risk: narrow Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md' Not-tested: Full GitHub Actions matrix before push * Keep RelayMon TRA assertions monitor-local RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data. Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events Constraint: monitors that do not publish TRA must not write detailed TRA observation history Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon Confidence: high Scope-risk: moderate Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator Tested: deno task test:config-types Tested: deno task test:database Tested: deno task test:kind30385 Tested: deno task test:unit Tested: deno task compile Tested: git diff --check Tested: deno lint with existing noisy rules excluded on changed TypeScript files Not-tested: full deno check remains blocked by existing project-wide strict type errors --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * Allow RelayMon TRA publication during warmup (#925) Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended. Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active. Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence. Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events. Confidence: high Scope-risk: narrow Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): move TRA per-check DB work off the check hot path (#926) * Let remediation deletions publish after warmup Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued. Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression. Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing. Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller. Confidence: high Scope-risk: narrow Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts Not-tested: live Fisherman deployment and relay ACK drain * fix(relaymon): move TRA per-check DB work off the check hot path Trusted Relay Assertion (kind 30385) processing ran inline on every relay check: recordTrustedRelayObservation + buildTrustedRelayAssertion do synchronous SQLite work, including a read-back of up to max_observations_per_relay history rows plus per-sample scoring. On Deno's single-threaded event loop (and a CPU-constrained host) those bursts starved in-flight WebSocket I/O and busted the tight check budgets (read/open timeouts), so healthy relays were falsely reported offline. This is what took Fisherman down once TRA was enabled there. Decouple TRA from the check hot path: - processRelay now calls enqueueTrustedRelayObservation(), a cheap guard + array push (no DB work). The queue is capped (drops oldest, warns) so it can't grow unbounded during warmup. - A lazily-started background processor drains the queue one relay at a time and yields the event loop (delay) between relays, so the synchronous per-relay work never sustains a block long enough to starve checks. Start is deferred via setTimeout so the first item never runs inside the check's call stack. - Throttle is configurable via trustedRelayAssertions.processing_throttle_ms (default 25ms). publishTrustedRelayAssertion (the per-item worker) is unchanged, so its record/build/gate/publish behavior and existing coverage are preserved. Tests: add coverage that enqueue defers work (nothing published synchronously; queue drains via processTrustedRelayQueueOnce), that it is a no-op when TRA is disabled, and that the queue caps memory by dropping oldest. Full unit suite: 369 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (#928) Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml workflows (whose `<pkg>-v*` tag triggers never matched the release's actual `<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a single matrix-driven workflow covering every package that produces a Docker image: rstate, trawler, relaymon (nocapd excluded — deprecated). Behaviour: - push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for ALL matrix packages. - release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest` when it is the highest non-prerelease version (computed from git tags). - both paths runnable via workflow_dispatch (mode dev|tagged, package, version, update_latest auto|true|false). - every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR (ghcr.io/sandwichfarm/<app>). The build plan (which images, which tags, latest-or-not) lives in scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so the workflow logic is verifiable outside CI. Build contexts match each app's Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root). Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the built-in GITHUB_TOKEN (packages: write). Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * rstate: publish trusted relay assertions from computed state (#922) * Prevent stale rstate relay assertion cache hits The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks. This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only. Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind. Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next. Confidence: high Scope-risk: narrow Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases. Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing). Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce. Not-tested: full workspace recursive lint/build/test. * Publish trusted relay assertions from computed rstate output rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached. Constraint: must not ingest kind 1066 for this slice Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity Confidence: high Scope-risk: moderate Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check Not-tested: live relay publish round-trip against a production signer/relay pair --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): publish each relay deletion once, not on every boot (#932) On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored relay (~17k), because the "already deleted" guard was an in-memory Set that resets each process. The deletion only needs to be sent once per ignore episode. Persist the guard: - relay_status gains a `deletion_published_at` column (idempotent migration). - deleteRelayCheckEvent skips when the durable marker is set (or the in-memory fast-path is) and stamps it on successful publish — so the once-only guarantee survives restarts. - IgnoreListSync.publishDeletions now only iterates ignored relays that are actually pending (deletion_published_at = 0) instead of all of them. - The marker is cleared whenever a relay becomes unignored (markRelayUnignored, persistResult, dedup re-evaluation), so a later re-ignore republishes exactly once (a prior deletion only covered the earlier 30166). Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip, cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite: 374 passed. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(trawler): remove committed merge-conflict markers from CHANGELOG (#931) A prior merge committed unresolved conflict markers into apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content only — no version/release change. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix build failures (#937) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same * fix build failures --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (#933) Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7. - [Release notes](https://github.com/chimurai/http-proxy-middleware/releases) - [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md) - [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v3.0.5...v3.0.7) --- updated-dependencies: - dependency-name: http-proxy-middleware dependency-version: 3.0.7 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump dompurify from 3.4.2 to 3.4.11 (#934) Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.11) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.11 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (#935) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(gui/security): close remaining XSS vectors (#938) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor (#920) * chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.1 to 3.2.6. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 3.2.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * Add RelayMon trusted relay assertion publishing (#923) * Expose RelayMon trust observations as Nostr assertions RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses. The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture. Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring Confidence: high Scope-risk: moderate Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test . Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check * Keep docs lint focused on checked-in docs Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs. Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface Confidence: high Scope-risk: narrow Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md' Not-tested: Full GitHub Actions matrix before push * Keep RelayMon TRA assertions monitor-local RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data. Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events Constraint: monitors that do not publish TRA must not write detailed TRA observation history Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon Confidence: high Scope-risk: moderate Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator Tested: deno task test:config-types Tested: deno task test:database Tested: deno task test:kind30385 Tested: deno task test:unit Tested: deno task compile Tested: git diff --check Tested: deno lint with existing noisy rules excluded on changed TypeScript files Not-tested: full deno check remains blocked by existing project-wide strict type errors --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * Allow RelayMon TRA publication during warmup (#925) Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended. Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active. Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence. Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events. Confidence: high Scope-risk: narrow Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): move TRA per-check DB work off the check hot path (#926) * Let remediation deletions publish after warmup Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued. Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression. Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing. Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller. Confidence: high Scope-risk: narrow Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts Not-tested: live Fisherman deployment and relay ACK drain * fix(relaymon): move TRA per-check DB work off the check hot path Trusted Relay Assertion (kind 30385) processing ran inline on every relay check: recordTrustedRelayObservation + buildTrustedRelayAssertion do synchronous SQLite work, including a read-back of up to max_observations_per_relay history rows plus per-sample scoring. On Deno's single-threaded event loop (and a CPU-constrained host) those bursts starved in-flight WebSocket I/O and busted the tight check budgets (read/open timeouts), so healthy relays were falsely reported offline. This is what took Fisherman down once TRA was enabled there. Decouple TRA from the check hot path: - processRelay now calls enqueueTrustedRelayObservation(), a cheap guard + array push (no DB work). The queue is capped (drops oldest, warns) so it can't grow unbounded during warmup. - A lazily-started background processor drains the queue one relay at a time and yields the event loop (delay) between relays, so the synchronous per-relay work never sustains a block long enough to starve checks. Start is deferred via setTimeout so the first item never runs inside the check's call stack. - Throttle is configurable via trustedRelayAssertions.processing_throttle_ms (default 25ms). publishTrustedRelayAssertion (the per-item worker) is unchanged, so its record/build/gate/publish behavior and existing coverage are preserved. Tests: add coverage that enqueue defers work (nothing published synchronously; queue drains via processTrustedRelayQueueOnce), that it is a no-op when TRA is disabled, and that the queue caps memory by dropping oldest. Full unit suite: 369 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (#928) Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml workflows (whose `<pkg>-v*` tag triggers never matched the release's actual `<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a single matrix-driven workflow covering every package that produces a Docker image: rstate, trawler, relaymon (nocapd excluded — deprecated). Behaviour: - push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for ALL matrix packages. - release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest` when it is the highest non-prerelease version (computed from git tags). - both paths runnable via workflow_dispatch (mode dev|tagged, package, version, update_latest auto|true|false). - every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR (ghcr.io/sandwichfarm/<app>). The build plan (which images, which tags, latest-or-not) lives in scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so the workflow logic is verifiable outside CI. Build contexts match each app's Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root). Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the built-in GITHUB_TOKEN (packages: write). Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * rstate: publish trusted relay assertions from computed state (#922) * Prevent stale rstate relay assertion cache hits The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks. This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only. Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind. Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next. Confidence: high Scope-risk: narrow Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases. Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing). Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce. Not-tested: full workspace recursive lint/build/test. * Publish trusted relay assertions from computed rstate output rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached. Constraint: must not ingest kind 1066 for this slice Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity Confidence: high Scope-risk: moderate Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check Not-tested: live relay publish round-trip against a production signer/relay pair --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): publish each relay deletion once, not on every boot (#932) On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored relay (~17k), because the "already deleted" guard was an in-memory Set that resets each process. The deletion only needs to be sent once per ignore episode. Persist the guard: - relay_status gains a `deletion_published_at` column (idempotent migration). - deleteRelayCheckEvent skips when the durable marker is set (or the in-memory fast-path is) and stamps it on successful publish — so the once-only guarantee survives restarts. - IgnoreListSync.publishDeletions now only iterates ignored relays that are actually pending (deletion_published_at = 0) instead of all of them. - The marker is cleared whenever a relay becomes unignored (markRelayUnignored, persistResult, dedup re-evaluation), so a later re-ignore republishes exactly once (a prior deletion only covered the earlier 30166). Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip, cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite: 374 passed. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(trawler): remove committed merge-conflict markers from CHANGELOG (#931) A prior merge committed unresolved conflict markers into apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content only — no version/release change. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix build failures (#937) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same * fix build failures --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (#933) Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7. - [Release notes](https://github.com/chimurai/http-proxy-middleware/releases) - [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md) - [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v3.0.5...v3.0.7) --- updated-dependencies: - dependency-name: http-proxy-middleware dependency-version: 3.0.7 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump dompurify from 3.4.2 to 3.4.11 (#934) Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.11) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.11 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (#935) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <299465+dskvr@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(deps-dev): bump vitest from 3.1.2 to 3.2.6 in /libraries/schemata-js-ajv (#921) * chore(deps-dev): bump vitest in /libraries/schemata-js-ajv Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.2 to 3.2.6. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 3.2.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * Add RelayMon trusted relay assertion publishing (#923) * Expose RelayMon trust observations as Nostr assertions RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses. The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture. Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring Confidence: high Scope-risk: moderate Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test . Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check * Keep docs lint focused on checked-in docs Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs. Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface Confidence: high Scope-risk: narrow Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md' Not-tested: Full GitHub Actions matrix before push * Keep RelayMon TRA assertions monitor-local RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data. Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events Constraint: monitors that do not publish TRA must not write detailed TRA observation history Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon Confidence: high Scope-risk: moderate Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator Tested: deno task test:config-types Tested: deno task test:database Tested: deno task test:kind30385 Tested: deno task test:unit Tested: deno task compile Tested: git diff --check Tested: deno lint with existing noisy rules excluded on changed TypeScript files Not-tested: full deno check remains blocked by existing project-wide strict type errors --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * Allow RelayMon TRA publication during warmup (#925) Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended. Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active. Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence. Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events. Confidence: high Scope-risk: narrow Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): move TRA per-check DB work off the check hot path (#926) * Let remediation deletions publish after warmup Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued. Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression. Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing. Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller. Confidence: high Scope-risk: narrow Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics. Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts Tested: git diff --check -- apps/relaymon/src/core/daemon.ts Not-tested: live Fisherman deployment and relay ACK drain * fix(relaymon): move TRA per-check DB work off the check hot path Trusted Relay Assertion (kind 30385) processing ran inline on every relay check: recordTrustedRelayObservation + buildTrustedRelayAssertion do synchronous SQLite work, including a read-back of up to max_observations_per_relay history rows plus per-sample scoring. On Deno's single-threaded event loop (and a CPU-constrained host) those bursts starved in-flight WebSocket I/O and busted the tight check budgets (read/open timeouts), so healthy relays were falsely reported offline. This is what took Fisherman down once TRA was enabled there. Decouple TRA from the check hot path: - processRelay now calls enqueueTrustedRelayObservation(), a cheap guard + array push (no DB work). The queue is capped (drops oldest, warns) so it can't grow unbounded during warmup. - A lazily-started background processor drains the queue one relay at a time and yields the event loop (delay) between relays, so the synchronous per-relay work never sustains a block long enough to starve checks. Start is deferred via setTimeout so the first item never runs inside the check's call stack. - Throttle is configurable via trustedRelayAssertions.processing_throttle_ms (default 25ms). publishTrustedRelayAssertion (the per-item worker) is unchanged, so its record/build/gate/publish behavior and existing coverage are preserved. Tests: add coverage that enqueue defers work (nothing published synchronously; queue drains via processTrustedRelayQueueOnce), that it is a no-op when TRA is disabled, and that the queue caps memory by dropping oldest. Full unit suite: 369 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (#928) Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml workflows (whose `<pkg>-v*` tag triggers never matched the release's actual `<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a single matrix-driven workflow covering every package that produces a Docker image: rstate, trawler, relaymon (nocapd excluded — deprecated). Behaviour: - push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for ALL matrix packages. - release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest` when it is the highest non-prerelease version (computed from git tags). - both paths runnable via workflow_dispatch (mode dev|tagged, package, version, update_latest auto|true|false). - every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR (ghcr.io/sandwichfarm/<app>). The build plan (which images, which tags, latest-or-not) lives in scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so the workflow logic is verifiable outside CI. Build contexts match each app's Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root). Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the built-in GITHUB_TOKEN (packages: write). Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * rstate: publish trusted relay assertions from computed state (#922) * Prevent stale rstate relay assertion cache hits The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks. This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only. Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind. Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next. Confidence: high Scope-risk: narrow Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases. Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing). Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce. Not-tested: full workspace recursive lint/build/test. * Publish trusted relay assertions from computed rstate output rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached. Constraint: must not ingest kind 1066 for this slice Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity Confidence: high Scope-risk: moderate Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check Not-tested: live relay publish round-trip against a production signer/relay pair --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * fix(relaymon): publish each relay deletion once, not on every boot (#932) On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored relay (~17k), because the "already deleted" guard was an in-memory Set that resets each process. The deletion only needs to be sent once per ignore episode. Persist the guard: - relay_status gains a `deletion_published_at` column (idempotent migration). - deleteRelayCheckEvent skips when the durable marker is set (or the in-memory fast-path is) and stamps it on successful publish — so the once-only guarantee survives restarts. - IgnoreListSync.publishDeletions now only iterates ignored relays that are actually pending (deletion_published_at = 0) instead of all of them. - The marker is cleared whenever a relay becomes unignored (markRelayUnignored, persistResult, dedup re-evaluation), so a later re-ignore republishes exactly once (a prior deletion only covered the earlier 30166). Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip, cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite: 374 passed. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(trawler): remove committed merge-conflict markers from CHANGELOG (#931) A prior merge committed unresolved conflict markers into apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content only — no version/release change. Co-authored-by: sandwich <dskvr@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix build failures (#937) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same * fix build failures --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (#933) Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7. - [Release notes](https://github.com/chimurai/http-proxy-middleware/releases) - [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md) - [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v3.0.5...v3.0.7) --- updated-dependencies: - dependency-name: http-proxy-middleware dependency-version: 3.0.7 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * chore(deps): bump dompurify from 3.4.2 to 3.4.11 (#934) Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.11) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.11 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (#935) Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0. - [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md) - [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0) --- updated-dependencies: - dependency-name: js-yaml dependency-version: 4.2.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(gui/security): close remaining XSS vectors (#938) * test(quick/260503-kc3): add failing regression tests for remaining XSS vectors - operators.formatters.test.ts: name/about/reference/filterFormatters - monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters - relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy - DataTableFallback.test.ts: structural regression for {@html row[column.key]} * fix(gui/security): sanitize remaining XSS sinks in dataTable formatters - operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters - monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo - relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software - softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit) - test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver * fix(gui/security): bind cell + filter fallbacks as text, not HTML Replace the {@html} fallbacks in 4 components with split-conditional text bindings. Registered formatters keep their {@html ...} branch (output is sanitized as of the previous commit). Unregistered columns now render as plain text — closes the silent regression vector where any column without a formatter would re-introduce XSS. - data-view/table/DataTable.svelte: fallback row[column.key] now text-bound - lists/table/DataTable.svelte: same - lists/table/Filters.svelte: filter button fallback now text-bound - data-view/filters/DataViewFilters.svelte: same --------- Co-authored-by: sandwich <dskvr@users.noreply.github.com> * chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor (#920) * chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.1 to 3.2.6. - [Release notes](https://github.com/vitest-dev/vitest/releases) - [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md) - [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest) --- updated-dependencies: - dependency-name: vitest dependency-version: 3.2.6 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * Add RelayMon trusted relay assertion publishing (#923) * Expose RelayMon trust observations as Nostr assertions RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses. The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture. Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring Confidence: high Scope-risk: moderate Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test . Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check * Keep docs lint focused on checked-in docs Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs. Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface Confidence: high Scope-risk: narrow Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md' Not-tested: Full GitHub Actions matrix before push * Keep RelayMon TRA assertions monitor-local RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data. Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events Constraint: monitors that do not publish TRA must not write detailed TRA observation history Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon Confidence: high Scope-risk: moderate Directive: Keep RelayMon TRA scoring limite…
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.25.12 to 0.28.1. - [Release notes](https://github.com/evanw/esbuild/releases) - [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG-2025.md) - [Commits](evanw/esbuild@v0.25.12...v0.28.1) --- updated-dependencies: - dependency-name: esbuild dependency-version: 0.28.1 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore(deps-dev): bump vite from 5.4.21 to 6.4.2
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.4.21 to 6.4.2.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.4.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.4.2/packages/vite)
---
updated-dependencies:
- dependency-name: vite
dependency-version: 6.4.2
dependency-type: direct:development
...
Signed-off-by: dependabot[bot] <support@github.com>
* Add RelayMon trusted relay assertion publishing (#923)
* Expose RelayMon trust observations as Nostr assertions
RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses.
The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture.
Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags
Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks
Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating
Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring
Confidence: high
Scope-risk: moderate
Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults
Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test .
Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check
* Keep docs lint focused on checked-in docs
Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs.
Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request
Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface
Confidence: high
Scope-risk: narrow
Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md'
Not-tested: Full GitHub Actions matrix before push
* Keep RelayMon TRA assertions monitor-local
RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data.
Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events
Constraint: monitors that do not publish TRA must not write detailed TRA observation history
Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity
Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon
Confidence: high
Scope-risk: moderate
Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator
Tested: deno task test:config-types
Tested: deno task test:database
Tested: deno task test:kind30385
Tested: deno task test:unit
Tested: deno task compile
Tested: git diff --check
Tested: deno lint with existing noisy rules excluded on changed TypeScript files
Not-tested: full deno check remains blocked by existing project-wide strict type errors
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* Allow RelayMon TRA publication during warmup (#925)
Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended.
Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active.
Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence.
Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events.
Confidence: high
Scope-risk: narrow
Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder.
Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts
Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts
Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts
Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* fix(relaymon): move TRA per-check DB work off the check hot path (#926)
* Let remediation deletions publish after warmup
Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued.
Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression.
Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing.
Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller.
Confidence: high
Scope-risk: narrow
Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics.
Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts
Tested: git diff --check -- apps/relaymon/src/core/daemon.ts
Not-tested: live Fisherman deployment and relay ACK drain
* fix(relaymon): move TRA per-check DB work off the check hot path
Trusted Relay Assertion (kind 30385) processing ran inline on every relay
check: recordTrustedRelayObservation + buildTrustedRelayAssertion do
synchronous SQLite work, including a read-back of up to
max_observations_per_relay history rows plus per-sample scoring. On Deno's
single-threaded event loop (and a CPU-constrained host) those bursts
starved in-flight WebSocket I/O and busted the tight check budgets
(read/open timeouts), so healthy relays were falsely reported offline.
This is what took Fisherman down once TRA was enabled there.
Decouple TRA from the check hot path:
- processRelay now calls enqueueTrustedRelayObservation(), a cheap guard +
array push (no DB work). The queue is capped (drops oldest, warns) so it
can't grow unbounded during warmup.
- A lazily-started background processor drains the queue one relay at a
time and yields the event loop (delay) between relays, so the synchronous
per-relay work never sustains a block long enough to starve checks. Start
is deferred via setTimeout so the first item never runs inside the check's
call stack.
- Throttle is configurable via trustedRelayAssertions.processing_throttle_ms
(default 25ms).
publishTrustedRelayAssertion (the per-item worker) is unchanged, so its
record/build/gate/publish behavior and existing coverage are preserved.
Tests: add coverage that enqueue defers work (nothing published
synchronously; queue drains via processTrustedRelayQueueOnce), that it is a
no-op when TRA is disabled, and that the queue caps memory by dropping
oldest. Full unit suite: 369 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (#928)
Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml
workflows (whose `<pkg>-v*` tag triggers never matched the release's actual
`<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a
single matrix-driven workflow covering every package that produces a Docker
image: rstate, trawler, relaymon (nocapd excluded — deprecated).
Behaviour:
- push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for
ALL matrix packages.
- release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest`
when it is the highest non-prerelease version (computed from git tags).
- both paths runnable via workflow_dispatch (mode dev|tagged, package, version,
update_latest auto|true|false).
- every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR
(ghcr.io/sandwichfarm/<app>).
The build plan (which images, which tags, latest-or-not) lives in
scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so
the workflow logic is verifiable outside CI. Build contexts match each app's
Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root).
Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the
built-in GITHUB_TOKEN (packages: write).
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* rstate: publish trusted relay assertions from computed state (#922)
* Prevent stale rstate relay assertion cache hits
The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks.
This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only.
Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind.
Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next.
Confidence: high
Scope-risk: narrow
Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases.
Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit
Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing).
Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce.
Not-tested: full workspace recursive lint/build/test.
* Publish trusted relay assertions from computed rstate output
rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached.
Constraint: must not ingest kind 1066 for this slice
Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity
Confidence: high
Scope-risk: moderate
Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here
Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check
Not-tested: live relay publish round-trip against a production signer/relay pair
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* fix(relaymon): publish each relay deletion once, not on every boot (#932)
On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored
relay (~17k), because the "already deleted" guard was an in-memory Set that
resets each process. The deletion only needs to be sent once per ignore episode.
Persist the guard:
- relay_status gains a `deletion_published_at` column (idempotent migration).
- deleteRelayCheckEvent skips when the durable marker is set (or the in-memory
fast-path is) and stamps it on successful publish — so the once-only guarantee
survives restarts.
- IgnoreListSync.publishDeletions now only iterates ignored relays that are
actually pending (deletion_published_at = 0) instead of all of them.
- The marker is cleared whenever a relay becomes unignored (markRelayUnignored,
persistResult, dedup re-evaluation), so a later re-ignore republishes exactly
once (a prior deletion only covered the earlier 30166).
Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip,
cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite:
374 passed.
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(trawler): remove committed merge-conflict markers from CHANGELOG (#931)
A prior merge committed unresolved conflict markers into
apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side
held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content
only — no version/release change.
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix build failures (#937)
* test(quick/260503-kc3): add failing regression tests for remaining XSS vectors
- operators.formatters.test.ts: name/about/reference/filterFormatters
- monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters
- relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy
- DataTableFallback.test.ts: structural regression for {@html row[column.key]}
* fix(gui/security): sanitize remaining XSS sinks in dataTable formatters
- operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters
- monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo
- relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software
- softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit)
- test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver
* fix(gui/security): bind cell + filter fallbacks as text, not HTML
Replace the {@html} fallbacks in 4 components with split-conditional text
bindings. Registered formatters keep their {@html ...} branch (output is
sanitized as of the previous commit). Unregistered columns now render as
plain text — closes the silent regression vector where any column without
a formatter would re-introduce XSS.
- data-view/table/DataTable.svelte: fallback row[column.key] now text-bound
- lists/table/DataTable.svelte: same
- lists/table/Filters.svelte: filter button fallback now text-bound
- data-view/filters/DataViewFilters.svelte: same
* fix build failures
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (#933)
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v3.0.5...v3.0.7)
---
updated-dependencies:
- dependency-name: http-proxy-middleware
dependency-version: 3.0.7
dependency-type: direct:development
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore(deps): bump dompurify from 3.4.2 to 3.4.11 (#934)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.11)
---
updated-dependencies:
- dependency-name: dompurify
dependency-version: 3.4.11
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (#935)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0)
---
updated-dependencies:
- dependency-name: js-yaml
dependency-version: 4.2.0
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* fix(gui/security): close remaining XSS vectors (#938)
* test(quick/260503-kc3): add failing regression tests for remaining XSS vectors
- operators.formatters.test.ts: name/about/reference/filterFormatters
- monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters
- relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy
- DataTableFallback.test.ts: structural regression for {@html row[column.key]}
* fix(gui/security): sanitize remaining XSS sinks in dataTable formatters
- operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters
- monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo
- relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software
- softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit)
- test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver
* fix(gui/security): bind cell + filter fallbacks as text, not HTML
Replace the {@html} fallbacks in 4 components with split-conditional text
bindings. Registered formatters keep their {@html ...} branch (output is
sanitized as of the previous commit). Unregistered columns now render as
plain text — closes the silent regression vector where any column without
a formatter would re-introduce XSS.
- data-view/table/DataTable.svelte: fallback row[column.key] now text-bound
- lists/table/DataTable.svelte: same
- lists/table/Filters.svelte: filter button fallback now text-bound
- data-view/filters/DataViewFilters.svelte: same
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor (#920)
* chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.1 to 3.2.6.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest)
---
updated-dependencies:
- dependency-name: vitest
dependency-version: 3.2.6
dependency-type: direct:development
...
Signed-off-by: dependabot[bot] <support@github.com>
* Add RelayMon trusted relay assertion publishing (#923)
* Expose RelayMon trust observations as Nostr assertions
RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses.
The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture.
Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags
Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks
Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating
Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring
Confidence: high
Scope-risk: moderate
Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults
Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test .
Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check
* Keep docs lint focused on checked-in docs
Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs.
Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request
Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface
Confidence: high
Scope-risk: narrow
Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md'
Not-tested: Full GitHub Actions matrix before push
* Keep RelayMon TRA assertions monitor-local
RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data.
Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events
Constraint: monitors that do not publish TRA must not write detailed TRA observation history
Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity
Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon
Confidence: high
Scope-risk: moderate
Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator
Tested: deno task test:config-types
Tested: deno task test:database
Tested: deno task test:kind30385
Tested: deno task test:unit
Tested: deno task compile
Tested: git diff --check
Tested: deno lint with existing noisy rules excluded on changed TypeScript files
Not-tested: full deno check remains blocked by existing project-wide strict type errors
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* Allow RelayMon TRA publication during warmup (#925)
Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended.
Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active.
Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence.
Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events.
Confidence: high
Scope-risk: narrow
Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder.
Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts
Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts
Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts
Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* fix(relaymon): move TRA per-check DB work off the check hot path (#926)
* Let remediation deletions publish after warmup
Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued.
Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression.
Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing.
Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller.
Confidence: high
Scope-risk: narrow
Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics.
Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts
Tested: git diff --check -- apps/relaymon/src/core/daemon.ts
Not-tested: live Fisherman deployment and relay ACK drain
* fix(relaymon): move TRA per-check DB work off the check hot path
Trusted Relay Assertion (kind 30385) processing ran inline on every relay
check: recordTrustedRelayObservation + buildTrustedRelayAssertion do
synchronous SQLite work, including a read-back of up to
max_observations_per_relay history rows plus per-sample scoring. On Deno's
single-threaded event loop (and a CPU-constrained host) those bursts
starved in-flight WebSocket I/O and busted the tight check budgets
(read/open timeouts), so healthy relays were falsely reported offline.
This is what took Fisherman down once TRA was enabled there.
Decouple TRA from the check hot path:
- processRelay now calls enqueueTrustedRelayObservation(), a cheap guard +
array push (no DB work). The queue is capped (drops oldest, warns) so it
can't grow unbounded during warmup.
- A lazily-started background processor drains the queue one relay at a
time and yields the event loop (delay) between relays, so the synchronous
per-relay work never sustains a block long enough to starve checks. Start
is deferred via setTimeout so the first item never runs inside the check's
call stack.
- Throttle is configurable via trustedRelayAssertions.processing_throttle_ms
(default 25ms).
publishTrustedRelayAssertion (the per-item worker) is unchanged, so its
record/build/gate/publish behavior and existing coverage are preserved.
Tests: add coverage that enqueue defers work (nothing published
synchronously; queue drains via processTrustedRelayQueueOnce), that it is a
no-op when TRA is disabled, and that the queue caps memory by dropping
oldest. Full unit suite: 369 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (#928)
Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml
workflows (whose `<pkg>-v*` tag triggers never matched the release's actual
`<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a
single matrix-driven workflow covering every package that produces a Docker
image: rstate, trawler, relaymon (nocapd excluded — deprecated).
Behaviour:
- push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for
ALL matrix packages.
- release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest`
when it is the highest non-prerelease version (computed from git tags).
- both paths runnable via workflow_dispatch (mode dev|tagged, package, version,
update_latest auto|true|false).
- every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR
(ghcr.io/sandwichfarm/<app>).
The build plan (which images, which tags, latest-or-not) lives in
scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so
the workflow logic is verifiable outside CI. Build contexts match each app's
Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root).
Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the
built-in GITHUB_TOKEN (packages: write).
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* rstate: publish trusted relay assertions from computed state (#922)
* Prevent stale rstate relay assertion cache hits
The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks.
This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only.
Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind.
Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next.
Confidence: high
Scope-risk: narrow
Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases.
Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit
Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing).
Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce.
Not-tested: full workspace recursive lint/build/test.
* Publish trusted relay assertions from computed rstate output
rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached.
Constraint: must not ingest kind 1066 for this slice
Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity
Confidence: high
Scope-risk: moderate
Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here
Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check
Not-tested: live relay publish round-trip against a production signer/relay pair
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* fix(relaymon): publish each relay deletion once, not on every boot (#932)
On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored
relay (~17k), because the "already deleted" guard was an in-memory Set that
resets each process. The deletion only needs to be sent once per ignore episode.
Persist the guard:
- relay_status gains a `deletion_published_at` column (idempotent migration).
- deleteRelayCheckEvent skips when the durable marker is set (or the in-memory
fast-path is) and stamps it on successful publish — so the once-only guarantee
survives restarts.
- IgnoreListSync.publishDeletions now only iterates ignored relays that are
actually pending (deletion_published_at = 0) instead of all of them.
- The marker is cleared whenever a relay becomes unignored (markRelayUnignored,
persistResult, dedup re-evaluation), so a later re-ignore republishes exactly
once (a prior deletion only covered the earlier 30166).
Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip,
cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite:
374 passed.
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(trawler): remove committed merge-conflict markers from CHANGELOG (#931)
A prior merge committed unresolved conflict markers into
apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side
held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content
only — no version/release change.
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix build failures (#937)
* test(quick/260503-kc3): add failing regression tests for remaining XSS vectors
- operators.formatters.test.ts: name/about/reference/filterFormatters
- monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters
- relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy
- DataTableFallback.test.ts: structural regression for {@html row[column.key]}
* fix(gui/security): sanitize remaining XSS sinks in dataTable formatters
- operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters
- monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo
- relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software
- softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit)
- test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver
* fix(gui/security): bind cell + filter fallbacks as text, not HTML
Replace the {@html} fallbacks in 4 components with split-conditional text
bindings. Registered formatters keep their {@html ...} branch (output is
sanitized as of the previous commit). Unregistered columns now render as
plain text — closes the silent regression vector where any column without
a formatter would re-introduce XSS.
- data-view/table/DataTable.svelte: fallback row[column.key] now text-bound
- lists/table/DataTable.svelte: same
- lists/table/Filters.svelte: filter button fallback now text-bound
- data-view/filters/DataViewFilters.svelte: same
* fix build failures
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (#933)
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v3.0.5...v3.0.7)
---
updated-dependencies:
- dependency-name: http-proxy-middleware
dependency-version: 3.0.7
dependency-type: direct:development
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore(deps): bump dompurify from 3.4.2 to 3.4.11 (#934)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.11)
---
updated-dependencies:
- dependency-name: dompurify
dependency-version: 3.4.11
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (#935)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0)
---
updated-dependencies:
- dependency-name: js-yaml
dependency-version: 4.2.0
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---------
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: sandwich <299465+dskvr@users.noreply.github.com>
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(deps-dev): bump vitest from 3.1.2 to 3.2.6 in /libraries/schemata-js-ajv (#921)
* chore(deps-dev): bump vitest in /libraries/schemata-js-ajv
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.2 to 3.2.6.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest)
---
updated-dependencies:
- dependency-name: vitest
dependency-version: 3.2.6
dependency-type: direct:development
...
Signed-off-by: dependabot[bot] <support@github.com>
* Add RelayMon trusted relay assertion publishing (#923)
* Expose RelayMon trust observations as Nostr assertions
RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses.
The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture.
Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags
Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks
Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating
Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring
Confidence: high
Scope-risk: moderate
Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults
Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test .
Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check
* Keep docs lint focused on checked-in docs
Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs.
Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request
Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface
Confidence: high
Scope-risk: narrow
Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md'
Not-tested: Full GitHub Actions matrix before push
* Keep RelayMon TRA assertions monitor-local
RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data.
Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events
Constraint: monitors that do not publish TRA must not write detailed TRA observation history
Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity
Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon
Confidence: high
Scope-risk: moderate
Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly becomes a social trust aggregator
Tested: deno task test:config-types
Tested: deno task test:database
Tested: deno task test:kind30385
Tested: deno task test:unit
Tested: deno task compile
Tested: git diff --check
Tested: deno lint with existing noisy rules excluded on changed TypeScript files
Not-tested: full deno check remains blocked by existing project-wide strict type errors
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* Allow RelayMon TRA publication during warmup (#925)
Fisherman can spend a long time in warmup while it drains unchecked relays. The worker previously suppressed trusted relay assertions in warmup, so enabled TRA publishing did not record observation history or enqueue kind 30385 events until warmup ended.
Keep normal NIP-66 check and delta event suppression intact, but let the opt-in TRA path run during warmup. Start health reporting before warmup so external monitors can see the process while the warmup queue is active.
Constraint: Fisherman warmup is long enough to keep status stale and prevent any live kind 30385 evidence.
Rejected: Wait for warmup to finish | leaves TRA publishing absent for the first long-running pass and does not explain current missing events.
Confidence: high
Scope-risk: narrow
Directive: Do not re-add warmup suppression around publishTrustedRelayAssertion without a separate warmup TRA publisher or observation recorder.
Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/worker-tra.test.ts tests/unit/kind30385.test.ts tests/unit/config-types.test.ts
Tested: deno fmt --check tests/unit/worker-tra.test.ts src/core/worker.ts
Tested: git diff --check -- apps/relaymon/src/core/daemon.ts apps/relaymon/src/core/worker.ts apps/relaymon/tests/unit/worker-tra.test.ts
Not-tested: live Fisherman deployment and live relay kind 30385 publication after image rollout
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* fix(relaymon): move TRA per-check DB work off the check hot path (#926)
* Let remediation deletions publish after warmup
Remediation queue draining used the same deletion helper as ordinary NIP-09 publishes, but daemon startup drained that queue while deletion publishing was still suppressed for warmup. That made valid queued deletions return false before any kind:5 event was sent, then remediation logged the skipped local publish as NOT ACK'd and left the row queued.
Move the remediation drain until immediately after warmup finishes, where runWarmup's finally block has re-enabled deletion publishing. The queue still retries rows that relays reject or fail to ACK, but valid entries are no longer blocked by local startup suppression.
Constraint: Deletion publishing is intentionally suppressed during warmup for normal check processing.
Rejected: Bypass suppression inside deleteRelayCheckEvent | that would weaken the warmup guard for every deletion caller.
Confidence: high
Scope-risk: narrow
Directive: Keep remediation queue drain after runWarmup unless deletion.ts gains caller-specific suppression semantics.
Tested: deno test --no-check --unstable-sloppy-imports --allow-net --allow-env --allow-read --allow-write --allow-run tests/unit/deletion.test.ts tests/unit/nato-purge.test.ts tests/unit/nostrings-sweep.test.ts
Tested: git diff --check -- apps/relaymon/src/core/daemon.ts
Not-tested: live Fisherman deployment and relay ACK drain
* fix(relaymon): move TRA per-check DB work off the check hot path
Trusted Relay Assertion (kind 30385) processing ran inline on every relay
check: recordTrustedRelayObservation + buildTrustedRelayAssertion do
synchronous SQLite work, including a read-back of up to
max_observations_per_relay history rows plus per-sample scoring. On Deno's
single-threaded event loop (and a CPU-constrained host) those bursts
starved in-flight WebSocket I/O and busted the tight check budgets
(read/open timeouts), so healthy relays were falsely reported offline.
This is what took Fisherman down once TRA was enabled there.
Decouple TRA from the check hot path:
- processRelay now calls enqueueTrustedRelayObservation(), a cheap guard +
array push (no DB work). The queue is capped (drops oldest, warns) so it
can't grow unbounded during warmup.
- A lazily-started background processor drains the queue one relay at a
time and yields the event loop (delay) between relays, so the synchronous
per-relay work never sustains a block long enough to starve checks. Start
is deferred via setTimeout so the first item never runs inside the check's
call stack.
- Throttle is configurable via trustedRelayAssertions.processing_throttle_ms
(default 25ms).
publishTrustedRelayAssertion (the per-item worker) is unchanged, so its
record/build/gate/publish behavior and existing coverage are preserved.
Tests: add coverage that enqueue defers work (nothing published
synchronously; queue drains via processTrustedRelayQueueOnce), that it is a
no-op when TRA is disabled, and that the queue caps memory by dropping
oldest. Full unit suite: 369 passed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* ci: unified Docker image CD (dev + tagged, Docker Hub + GHCR) (#928)
Replaces the two ad-hoc, manual-only docker-relaymon.yml / docker-trawler.yml
workflows (whose `<pkg>-v*` tag triggers never matched the release's actual
`<pkg>@v<version>` tags, so they only ever ran via manual dispatch) with a
single matrix-driven workflow covering every package that produces a Docker
image: rstate, trawler, relaymon (nocapd excluded — deprecated).
Behaviour:
- push to `next` (integration branch): rolling `:dev` + `:dev-<sha>` images for
ALL matrix packages.
- release tag `<pkg>@v<version>`: `:<version>` for that package, plus `:latest`
when it is the highest non-prerelease version (computed from git tags).
- both paths runnable via workflow_dispatch (mode dev|tagged, package, version,
update_latest auto|true|false).
- every image pushed to BOTH Docker Hub (nostrwatch/<app>) and GHCR
(ghcr.io/sandwichfarm/<app>).
The build plan (which images, which tags, latest-or-not) lives in
scripts/ci/plan-docker-images.mjs — dependency-free and locally runnable, so
the workflow logic is verifiable outside CI. Build contexts match each app's
Dockerfile (rstate builds from apps/rstate; trawler/relaymon from repo root).
Requires existing secrets DOCKERHUB_USERNAME / DOCKERHUB_TOKEN; GHCR uses the
built-in GITHUB_TOKEN (packages: write).
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* rstate: publish trusted relay assertions from computed state (#922)
* Prevent stale rstate relay assertion cache hits
The relay MCP tools had inline cache keys that missed output-affecting inputs as the surface grew into simple, detailed, and full variants. Centralizing cache config keeps registration and tests aligned with the current slash-based tool names, and the software grouping tool now honors the family filter used by REST parity checks.
This also fixes the rstate type-check failures that surfaced while validating the PR: scorer code now calls the current observation-store API, stale unused private state was removed, and relay pool connection state is exposed through stats instead of being written-only.
Constraint: PR branch had to be rebuilt onto current origin/next because the old trusted-relay-assertions branch was 133 commits behind.
Rejected: Push the stale local feature/trusted-relay-assertions branch directly | it would open a noisy outdated PR against next.
Confidence: high
Scope-risk: narrow
Directive: Keep cache config keys aligned with actual MCP tool names, not internal aliases.
Tested: cd apps/rstate && ./node_modules/.bin/tsc --noEmit
Tested: cd apps/rstate && ./node_modules/.bin/vitest --run test/cache-key-coverage.test.ts test/rest_tool_parity.test.ts test/parity.test.ts (140 passing).
Tested: cd apps/rstate && ./node_modules/.bin/tsup after building workspace prerequisites logger, utils, publisher, announce.
Not-tested: full workspace recursive lint/build/test.
* Publish trusted relay assertions from computed rstate output
rstate now owns kind 30385 generation from its aggregate relay state, with a separate trust query surface and no new 1066 ingestion path. The implementation keeps TRA history gated behind publishing enablement and suppresses unchanged publishes unless material state moves or a refresh boundary is reached.
Constraint: must not ingest kind 1066 for this slice
Rejected: fold trust data into default relay responses | would bloat the existing API surface and complicate cache/tool parity
Confidence: high
Scope-risk: moderate
Directive: keep TRA history writes gated on publishing enablement and do not add social trust ingestion here
Tested: npm run test -- --run, npm run type-check, npm run build, git diff --check
Not-tested: live relay publish round-trip against a production signer/relay pair
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* fix(relaymon): publish each relay deletion once, not on every boot (#932)
On startup relaymon re-published a NIP-09 (kind:5) deletion for every ignored
relay (~17k), because the "already deleted" guard was an in-memory Set that
resets each process. The deletion only needs to be sent once per ignore episode.
Persist the guard:
- relay_status gains a `deletion_published_at` column (idempotent migration).
- deleteRelayCheckEvent skips when the durable marker is set (or the in-memory
fast-path is) and stamps it on successful publish — so the once-only guarantee
survives restarts.
- IgnoreListSync.publishDeletions now only iterates ignored relays that are
actually pending (deletion_published_at = 0) instead of all of them.
- The marker is cleared whenever a relay becomes unignored (markRelayUnignored,
persistResult, dedup re-evaluation), so a later re-ignore republishes exactly
once (a prior deletion only covered the earlier 30166).
Tests: deletion-publish-once.test.ts covers pending selection, mark/round-trip,
cross-boot idempotency, and unignore/persist reset. Full relaymon unit suite:
374 passed.
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(trawler): remove committed merge-conflict markers from CHANGELOG (#931)
A prior merge committed unresolved conflict markers into
apps/trawler/CHANGELOG.md on next. The HEAD side was empty and the other side
held the 0.6.0/0.6.1 entries; dropped the markers and kept the entries. Content
only — no version/release change.
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix build failures (#937)
* test(quick/260503-kc3): add failing regression tests for remaining XSS vectors
- operators.formatters.test.ts: name/about/reference/filterFormatters
- monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters
- relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy
- DataTableFallback.test.ts: structural regression for {@html row[column.key]}
* fix(gui/security): sanitize remaining XSS sinks in dataTable formatters
- operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters
- monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo
- relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software
- softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit)
- test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver
* fix(gui/security): bind cell + filter fallbacks as text, not HTML
Replace the {@html} fallbacks in 4 components with split-conditional text
bindings. Registered formatters keep their {@html ...} branch (output is
sanitized as of the previous commit). Unregistered columns now render as
plain text — closes the silent regression vector where any column without
a formatter would re-introduce XSS.
- data-view/table/DataTable.svelte: fallback row[column.key] now text-bound
- lists/table/DataTable.svelte: same
- lists/table/Filters.svelte: filter button fallback now text-bound
- data-view/filters/DataViewFilters.svelte: same
* fix build failures
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* chore(deps-dev): bump http-proxy-middleware from 3.0.5 to 3.0.7 (#933)
Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 3.0.5 to 3.0.7.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v3.0.7/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v3.0.5...v3.0.7)
---
updated-dependencies:
- dependency-name: http-proxy-middleware
dependency-version: 3.0.7
dependency-type: direct:development
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* chore(deps): bump dompurify from 3.4.2 to 3.4.11 (#934)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.4.2 to 3.4.11.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.4.2...3.4.11)
---
updated-dependencies:
- dependency-name: dompurify
dependency-version: 3.4.11
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* chore(deps): bump js-yaml from 4.1.1 to 4.2.0 (#935)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.1 to 4.2.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.1...4.2.0)
---
updated-dependencies:
- dependency-name: js-yaml
dependency-version: 4.2.0
dependency-type: direct:production
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* fix(gui/security): close remaining XSS vectors (#938)
* test(quick/260503-kc3): add failing regression tests for remaining XSS vectors
- operators.formatters.test.ts: name/about/reference/filterFormatters
- monitors.formatters.test.ts: nip05/pubkey/checks/filterFormatters
- relays.formatters.test.ts: software/geocode/minPowDifficulty/supportedNips/networks/ipv4/retentionPolicy
- DataTableFallback.test.ts: structural regression for {@html row[column.key]}
* fix(gui/security): sanitize remaining XSS sinks in dataTable formatters
- operators.ts: hex-pubkey guard, escapeHtml on name/about, nostr-id guard on reference, alt="" on imgs, escaped filterFormatters
- monitors.ts: hex-pubkey guard, safeImageUrl on photos, escaped name/pubkey/checks/nip05, fixed pastelPair?.dark and class typo
- relays.ts: networks slug allowlist, IPv4 strict guard, retentionPolicy try/catch + escape, escaped geocode/supportedNips/minPowDifficulty/software
- softwares.ts/isps.ts/geocodes.ts: escaped filterFormatters; geocodes.ts geocode formatter also escaped (additional sink found during audit)
- test fixes: tighten 'x"' assertion to 'alert(1)' (avoided false positive on class="flex"); replace fileURLToPath with cwd-walking path resolver
* fix(gui/security): bind cell + filter fallbacks as text, not HTML
Replace the {@html} fallbacks in 4 components with split-conditional text
bindings. Registered formatters keep their {@html ...} branch (output is
sanitized as of the previous commit). Unregistered columns now render as
plain text — closes the silent regression vector where any column without
a formatter would re-introduce XSS.
- data-view/table/DataTable.svelte: fallback row[column.key] now text-bound
- lists/table/DataTable.svelte: same
- lists/table/Filters.svelte: filter button fallback now text-bound
- data-view/filters/DataViewFilters.svelte: same
---------
Co-authored-by: sandwich <dskvr@users.noreply.github.com>
* chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor (#920)
* chore(deps-dev): bump vitest from 3.1.1 to 3.2.6 in /libraries/auditor
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 3.1.1 to 3.2.6.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v3.2.6/packages/vitest)
---
updated-dependencies:
- dependency-name: vitest
dependency-version: 3.2.6
dependency-type: direct:development
...
Signed-off-by: dependabot[bot] <support@github.com>
* Add RelayMon trusted relay assertion publishing (#923)
* Expose RelayMon trust observations as Nostr assertions
RelayMon can now opt into kind 30385 Trusted Relay Assertion publication using the trustedrelays draft NIP shape. The worker records local observation history, derives reliability, quality, accessibility, score, confidence, and metadata tags from existing check results, and only republishes when material state changes or the refresh interval elapses.
The change also adds config defaults, docs and docker-stack examples, database persistence for assertion state, unit coverage for the kind 30385 builder/material-change logic, and small validation repairs for current nostr-tools key handling, the kind1066 operational-status tag, and a date-sensitive SSL fixture.
Constraint: TRA draft kind 30385 comes from Letdown2491/trustedrelays and uses replaceable d/status/score/component tags
Constraint: RelayMon's current test and compile contract uses Deno --no-check tasks
Rejected: Publish kind 30385 on every relay check | would spam replaceable assertions without material-change gating
Rejected: Add a runtime dependency on the trustedrelays implementation | no new dependencies requested and RelayMon already has the observation data needed for local scoring
Confidence: high
Scope-risk: moderate
Directive: Keep TRA publishing opt-in and preserve material-change gating before changing defaults
Tested: deno task test:kind30385; deno task test:unit; deno task test; deno task compile; docker build -f apps/relaymon/.docker/Dockerfile -t nostrwatch/relaymon:tra-test .
Not-tested: Strict deno type checking; repo has existing strict-check debt and RelayMon tasks use --no-check
* Keep docs lint focused on checked-in docs
Docs Lint requires package README anchor headings, so the Docker stacks README now exposes the required Installation section before Quick Start. Link Check was also failing on package README links into .planning, which is not part of the CI checkout or shipped docs surface, so lychee now ignores those local planning artifact URIs.
Constraint: Docs Lint enforces package README heading anchors and lychee checks package/docs links on pull_request
Rejected: Add .planning/codebase/CONCERNS.md for CI | planning artifacts are not part of the checked-in docs surface
Confidence: high
Scope-risk: narrow
Tested: ./node_modules/.bin/markdownlint-cli2 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md'; docker run --rm -v "/tmp/nostr-watch-tra":/input -w /input lycheeverse/lychee:latest --cache --max-cache-age 1d 'libraries/*/README.md' 'apps/*/README.md' 'internal/*/README.md' 'docs/**/*.md'
Not-tested: Full GitHub Actions matrix before push
* Keep RelayMon TRA assertions monitor-local
RelayMon should publish measured Trusted Relay Assertions without becoming a social trust aggregator. This keeps kind 30385 on the existing monitor identity, records detailed TRA history only when TRA publishing is enabled, and scores assertions from local observations plus current RelayMon probe data.
Constraint: kind 30385 must use the same RelayMon monitor key as NIP-66 events
Constraint: monitors that do not publish TRA must not write detailed TRA observation history
Rejected: Add a providerKey for TRA signing | user clarified the monitor key is the provider identity
Rejected: Ingest kind 1985 or manage kind 10385 in RelayMon | social trust aggregation and provider discovery belong outside RelayMon
Confidence: high
Scope-risk: moderate
Directive: Keep RelayMon TRA scoring limited to this monitor's observations unless RelayMon explicitly beco…
The PR's default import fixes Vite/Rollup's namespace import warning, but calling the default export directly changes runtime behavior because the default is a DOMPurify factory in this environment. Keep the default import and call .sanitize so the note and subtitle sanitizer paths continue returning strings. Constraint: PR 908 is meant to fix DOMPurify's import shape after the dependency updates on next. Rejected: Restore namespace import | it reintroduces the Rollup export warning the PR is fixing. Rejected: Keep direct default calls | sanitizer tests receive functions instead of sanitized strings. Confidence: high Scope-risk: narrow Directive: Default-import DOMPurify in GUI code, but use DOMPurify.sanitize for sanitization. Tested: pnpm 9 frozen install; targeted GUI sanitizer vitest suite; full GUI dependency graph build; full GUI vitest suite; git diff --check Not-tested: live browser rendering
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
dompurify has no named exports - only a default export (the sanitize function itself). But notes.ts and PageHeader.svelte both used
import * as DOMPurifyand calledDOMPurify.sanitize(), which Rollup can not resolve statically. It warns "sanitize is not exported by purify.es.mjs" and silently replaces the reference with undefined, crashing any page with a non-empty subtitle prop.Fix both files to use
import DOMPurify from "dompurify"and callDOMPurify(text)directly. Drop theas anycasts and the defensive.sanitize ?? .default?.sanitizefallback in notes.ts.Originated in two separate commits, same author ("sandwich"):