Skip to content

use default import for dompurify instead of namespace import#908

Merged
dskvr merged 27 commits into
sandwichfarm:nextfrom
KTibow:fix/dompurify-sanitize-import
Jul 8, 2026
Merged

use default import for dompurify instead of namespace import#908
dskvr merged 27 commits into
sandwichfarm:nextfrom
KTibow:fix/dompurify-sanitize-import

Conversation

@KTibow

@KTibow KTibow commented May 10, 2026

Copy link
Copy Markdown
Contributor

dompurify has no named exports - only a default export (the sanitize function itself). But notes.ts and PageHeader.svelte both used import * as DOMPurify and called DOMPurify.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 call DOMPurify(text) directly. Drop the as any casts and the defensive .sanitize ?? .default?.sanitize fallback in notes.ts.

Originated in two separate commits, same author ("sandwich"):

  1. notes.ts - commit 42c2a23 (Dec 9, 2024, "single relay pages."): the original introduction of the file, used import * as DOMPurify from day one.
  2. PageHeader.svelte - commit fff65b5 (May 3, 2026, "fix: XSS via unsanitized NIP-11 fields rendered with {@html}" from PR fix: XSS via unsanitized NIP-11 fields rendered with {@html} #899): copied the same broken import * as DOMPurify + .sanitize pattern from notes.ts when adding DOMPurify sanitization to the subtitle sink.

KTibow and others added 27 commits May 9, 2026 17:40
* 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
@dskvr dskvr merged commit 9c4d1bf into sandwichfarm:next Jul 8, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants