Commit Graph

12774 Commits

Author SHA1 Message Date
Vitor Pamplona 0210d608b5 Merge pull request #2719 from nrobi144/fix/bunker-timeout-and-decrypt
fix(nip46): fix bunker decrypt/encrypt parsing and increase timeout
2026-05-04 08:09:33 -04:00
Vitor Pamplona b1fb13cddc Merge pull request #2720 from davotoula/feat/log-lambda-overloads
chore: convert interpolated Log calls to lambda overload + restore throwables in Marmot catch blocks
2026-05-04 08:08:19 -04:00
Vitor Pamplona 85402c0d27 Merge pull request #2721 from greenart7c3/main
Add bulk unblock/unmute functionality to Security Filters
2026-05-04 08:04:26 -04:00
Claude 76b69cd440 feat: bulk-remove for blocked users and hidden words
Long-press a row in Settings -> Security Filters to enter selection mode,
pick more rows, tap Unblock. The mute list (kind 10000) and block list
are rebuilt and re-broadcast once for all selected entries instead of
once per item.

- Quartz: add `MuteListEvent.removeAll` and `PeopleListEvent.removeAll`
  bulk overloads using the existing `TagArray.removeAny` primitive.
- Account / state classes: add `showUsers(List)` and `showWords(List)`
  that produce a single resigned event per list.
- AccountViewModel: thin wrappers `showUsers` / `showWords`.
- SecurityFiltersScreen: hoist per-tab selection sets, swap top bar
  in selection mode, add a local selectable user list (the shared
  `UserCompose` row was kept untouched).
2026-05-04 08:55:29 -03:00
David Kaspar 4a789efa63 Merge pull request #2723 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-04 13:51:17 +02:00
Crowdin Bot 39ea8a6aae New Crowdin translations by GitHub Action 2026-05-04 11:49:25 +00:00
Vitor Pamplona cee3e28e97 Merge pull request #2656 from nrobi144/feat/desktop-multi-account
feat(desktop): Multi-Account Support + Feed Metadata Optimization
2026-05-04 07:47:35 -04:00
davotoula 72fcdfb6d1 docs(skill): extend find-non-lambda-logs to flag dropped throwables
Add Step 3 to the audit: catch-block Log.w/e calls that interpolate
${e.message} but don't pass `e` lose the stack trace and log "...: null"
when the exception's message is null. Document the throwable-overload
fix alongside the existing lambda-overload conversion.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 12:47:22 +02:00
davotoula 31dee7fe38 fix(log): pass throwable to Log.w in Marmot catch blocks
Previously the catch-block warnings interpolated ${e.message} but
dropped the throwable, losing the stack trace and showing nothing for
exceptions whose message is null. Switch to the (tag, msg, throwable)
overload so the cause and stack trace are logged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 10:52:39 +02:00
davotoula a22f63e42c refactor(log): convert interpolated Log calls to lambda overload
Avoid eager string interpolation when the log level is filtered out at
runtime. Covers Log.d/i calls and Log.w/e calls that did not pass a
throwable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 10:15:22 +02:00
nrobi144 bc2267d79a fix(nip46): fix bunker decrypt/encrypt response parsing and increase timeout
Two issues fixed:

1. BunkerResponseDeserializer produces generic BunkerResponse for decrypt/encrypt
   results (plain strings that aren't pubkeys or JSON). The response parsers only
   matched typed subtypes (BunkerResponseDecrypt/Encrypt), causing all decrypt and
   encrypt operations through NIP-46 bunker to fail with ReceivedButCouldNotPerform.
   Fix: parsers now fall back to extracting response.result directly.

2. RemoteSignerManager timeout was 30s, shorter than Amber's 60s approval window.
   Fix: increased to 65s with safe retry (republishes same request, preserving UUID
   so bunker responses always match).

3. Desktop ChatPane showed raw ciphertext on decrypt failure instead of an error
   message. Fix: shows "Could not decrypt the message" in italic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 10:24:05 +03:00
nrobi144 fd34105439 merge: resolve conflicts with upstream/main
Merge upstream main into feat/desktop-multi-account, resolving:
- Main.kt: Surface wrapper + CompositionLocalProvider + nip11Fetcher from upstream, multi-account DeckSidebar/LoginScreen from HEAD
- FeedScreen.kt: viewport-aware scroll from HEAD + contentPadding from upstream
- DeckSidebar.kt: merged imports (multi-account + titleBarInsetTop + MaterialSymbols)
- Migrated AccountSwitcherDropdown + AddAccountDialog from material.icons to MaterialSymbols

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-04 07:29:20 +03:00
Vitor Pamplona 42d2a50436 Merge pull request #2714 from vitorpamplona/claude/dns-resolver-okhtp-XCJWi
Add SurgeDns: burst-tolerant DNS resolver for OkHttp
2026-05-03 17:28:52 -04:00
Claude b9a260f3ce refactor(okhttp): rename AmethystDns -> SurgeDns
"AmethystDns" identified the owner; "SurgeDns" identifies the
behavior. The class is built to absorb sudden bursts of concurrent
DNS work — 700 relays reconnecting, a feed scrolling through dozens
of media hosts, a profile screen requesting a NIP-05 — without
falling over: lock-free reads, single-flight coalescing, and
stale-while-revalidate so recurring hosts never block.

File renames:
- AmethystDns.kt        -> SurgeDns.kt
- AmethystDnsStore.kt   -> SurgeDnsStore.kt
- AmethystDnsTest.kt    -> SurgeDnsTest.kt

Symbol renames in every touchpoint (AppModules, factories,
managers, event listeners): AmethystDns -> SurgeDns,
amethystDns -> surgeDns.
2026-05-03 21:26:46 +00:00
Claude e173daf3c9 refactor(okhttp): clearer names in DNS resolver
- evictIfOverCap -> purgeExpiredIfOverCap. The method only purges
  expired entries; "evict" implied LRU behavior.
- triggerBackgroundRefresh -> scheduleBackgroundRefresh. Better
  reflects that we hand the work to an executor that may queue it.
- fresh -> freshEntry in resolveAsLeader. The bare adjective read as
  a boolean.
- KEY_CACHE -> KEY_SNAPSHOT in AmethystDnsStore. The constant
  identifier now matches what we actually store via dns.snapshot();
  the string value is unchanged.
2026-05-03 21:14:12 +00:00
Claude e86871a868 fix(okhttp): close save-race window and reject empty positive results
Two correctness fixes from the audit:

1. Save race. The previous order (snapshot -> write -> clearDirty)
   could lose a putPositive that landed between the write and the
   clearDirty: clearDirty unconditionally wiped the flag, so the
   just-written entry would never be persisted. Replace clearDirty
   with tryClearDirty (compareAndSet), called BEFORE the snapshot —
   any concurrent put then re-marks dirty and is captured by the next
   save. Add markDirty so the store can re-flag on write failure.

   Also drop the buggy clearDirty in load(): if puts happened between
   AmethystDns construction and load completion, we used to silently
   discard their dirty signal. restore() never marks dirty, so the
   manual clear was both unnecessary and incorrect.

2. Empty positive results. A misbehaving Dns delegate returning
   emptyList() would land in the cache as a positive entry — harmless
   in lookup semantics (unwrap throws on empty) but wrongly persisted
   to disk and dirty-flagged. Treat empty as UnknownHostException so
   it goes through putNegative.

Add three tests:
- failed refresh demotes a stale positive entry to negative
- failed lookup does not mark cache dirty
- refresh executor rejection cleans up the inflight slot
2026-05-03 21:01:03 +00:00
Claude 6240e771f8 refactor(okhttp): inject AmethystDns instead of using a singleton
Drop the AmethystDns.shared lazy companion. Construct one
amethystDns in AppModules and thread it through:

- DualHttpClientManager / OkHttpClientFactory
- DualHttpClientManagerForRelays / OkHttpClientFactoryForRelays
- MediaCallEventListenerFactory / MediaCallEventListener
- DnsInvalidatingEventListener.Factory
- AmethystDnsStore

This matches the dependency-injection style the rest of AppModules
uses and lets tests inject a mock Dns where needed. Behavior is
unchanged — every consumer still shares the same single instance,
just by construction rather than by a static singleton.
2026-05-03 20:48:12 +00:00
Claude eeb5f00732 refactor(okhttp): tidy AmethystDns internals
Mechanical cleanups from a final review pass. No behavior change.

- Drop the dead `private val xxxMillis = xxxMs` aliasing — just use
  the constructor params directly.
- Extract positiveExpiry()/negativeExpiry() and evictIfOverCap() so
  putPositive and putNegative read as one line each.
- Extract lookupAndCache() from resolveAsLeader so the leader's
  bookkeeping (re-check, complete future, remove inflight) is
  separate from the upstream/cache write logic.
- Tighten awaitFollower's cause-handling.
- Use runCatching in the refresh executor task.
- Drop DnsCacheRecord's no-arg constructor — jacksonObjectMapper()
  registers the Kotlin module, which uses the primary constructor
  reflectively.
- Rename `hostname` -> `host` in private methods that receive the
  already-normalized lowercase key.
2026-05-03 20:44:12 +00:00
David Kaspar 99ecab8ba6 Merge pull request #2718 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-03 22:44:03 +02:00
Crowdin Bot 0dce23198f New Crowdin translations by GitHub Action 2026-05-03 20:39:55 +00:00
Vitor Pamplona b20a7901d8 Merge pull request #2716 from davotoula/fix-hls-gallery-icons
Generate poster JPEGs and fix HLS video rendering in profile gallery
2026-05-03 16:38:38 -04:00
David Kaspar e4a7df4a06 Merge pull request #2717 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-03 21:55:43 +02:00
Crowdin Bot 3e62211bca New Crowdin translations by GitHub Action 2026-05-03 19:54:06 +00:00
davotoula e7022fb9dc Add missing translations for cs-rCZ, pt-rBR, sv-rSE, de-rDE
Translate Nests, PDF, and duration strings/plurals that were missing
from the four target locales.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:52:10 +02:00
Claude e4897126a1 fix(okhttp): canonicalize DNS hosts and invalidate on call failure
Two follow-ups from the resolver audit:

1. Lowercase the hostname at the public boundary (lookup, invalidate,
   restore). DNS is case-insensitive; OkHttp normally hands us
   lowercase but custom callers and persisted records can mix case.
   This prevents "Example.com" and "example.com" from creating
   separate cache entries.

2. Drop the cache entry when an OkHttp call to a host fails outright.
   Without this, a stale-cached IP that no longer works would be
   served for up to 24h before the soft TTL ran out. Hooking
   callFailed (final-stage signal after OkHttp tried every address)
   instead of per-attempt connectFailed avoids over-invalidating
   multi-A-record hosts where one IP is dead and OkHttp recovers via
   the next.

   - Media path: invalidation folded into MediaCallEventListener.finish
     alongside its existing timing logging.
   - Relay path: new DnsInvalidatingEventListener wired into
     OkHttpClientFactoryForRelays via eventListenerFactory.
2026-05-03 19:22:04 +00:00
davotoula c2dd052841 refactor(gallery): use minBy instead of minByOrNull + unreachable fallback
The isAllHls guard already forces imetas.isNotEmpty(), so the
?: imetas.first() fallback could never fire. Switch to the throwing
minBy variant — same runtime semantics, no elvis noise, no nullable
intermediate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 20:49:03 +02:00
davotoula 5aa001229a refactor(gallery): use HlsContentTypes constant and tighten comments
Use the existing HlsContentTypes.HLS_PLAYLIST constant for the
canonical HLS playlist mime in GalleryThumb's isHlsMimeType so the
read-side stays in sync with the publish path (HlsVideoEventBuilder,
HlsPublishOrchestrator). Trim two block comments down to the
non-obvious WHY only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 20:33:12 +02:00
davotoula 2ea25f73a3 fix(gallery): forward imeta poster when adding media to gallery
The "Add Media to Gallery" action in the zoomable view passed only
blurhash, dim, hash, and mime type — losing the poster URL that the
underlying NIP-71 imeta carries on its `image` field. The resulting
ProfileGalleryEntryEvent therefore had no `image` tag, and the
gallery card fell back to the play-icon placeholder for any HLS URL
because the .m3u8 playlist itself can't be decoded as an image.

Thread the poster through Account.addToGallery and
AccountViewModel.addMediaToGallery as an optional `image` parameter,
and pull it from the MediaUrlVideo's artworkUri at the call site.
Non-video content (MediaUrlImage, etc.) still passes null.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 20:02:58 +02:00
Claude 893cc249fb feat(okhttp): persist DNS cache across process restarts
Cold starts were the resolver's worst case: every host paid a sync
getaddrinfo. With a persisted snapshot, every previously-seen host
falls into the stale-while-revalidate path on first lookup — the
cached IP is served immediately and a background refresh updates it.
~700 blocking system calls at app start become zero.

Switch entry expiries from System.nanoTime to System.currentTimeMillis
so timestamps survive process death (nanoTime is monotonic per
process, undefined across restarts). Add snapshot()/restore() that
serialize only fresh positive entries — negative entries are skipped
and re-resolved synchronously, and restore() uses putIfAbsent so a
fresh in-memory entry is never clobbered by a stale on-disk one.

Add AmethystDnsStore as a thin SharedPreferences + Jackson wrapper.
The blob is plain (not encrypted): hostnames are already exposed in
the user's signed relay list, Coil's image cache, and the system
resolver's own state.

Wire load + save into AppModules:
- load() runs once at app start on the IO scope.
- save() runs every 5 minutes (skipped when nothing has changed via
  the dirty flag), on trim() when the app backgrounds, and once on
  terminate() as a best-effort flush.
2026-05-03 17:51:06 +00:00
davotoula e994080197 fix(gallery): collapse HLS multi-rendition videos to a single tile
A NIP-71 HLS publish writes one imeta tag per rendition (master + each
variant), all on the same event. The gallery card was expanding that
into one sub-tile per imeta inside AutoNonlazyGrid, producing a 3x6
sub-grid of black play-icon placeholders for a single video — the
.m3u8 playlist is a text manifest Coil can't decode.

When every imeta on a VideoEvent is an HLS playlist, pick a single
imeta to render: prefer one carrying a poster image, breaking ties
by smallest dimensions to favor the lowest-resolution variant. Fall
back to the first imeta if none carry a poster. Non-HLS multi-imeta
videos keep their existing per-imeta layout to avoid regressions for
any author publishing alternate-format renditions side by side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 18:47:35 +02:00
Claude feef50985d perf(okhttp): stale-while-revalidate + TTL jitter
After the first lookup of a host, recurring connections never block on
getaddrinfo again. Soft-expired positive entries are returned
immediately while a background refresh updates the cache through a
small fixed thread pool (8 daemon threads). The refresh is coalesced
through the same inflight map, so a fan-out of stale reads to the same
host still triggers exactly one upstream call.

Negative entries deliberately do NOT serve stale — a transient DNS
failure must recover quickly, not keep returning UnknownHostException.
A failed refresh demotes the entry to negative so the next caller sees
a fresh failure rather than forever-stale wrong IPs.

Add TTL jitter on positive writes so a burst of co-written entries
(e.g. ~700 relay reconnects at app start) doesn't all expire at the
same instant 24h later. Default jitter equals the base TTL, so entries
spread their expiries uniformly across [24h, 48h]. Combined with the
bounded refresh executor, even a daily-app-open user with 1000 stale
hosts generates a few seconds of background work and zero blocking
waits in the foreground.
2026-05-03 16:40:52 +00:00
Claude 09c91e8aa9 feat(hls): generate and upload a poster JPEG during HLS publish
HLS publishes built NIP-71 video events with imetas pointing only at
.m3u8 playlists, leaving HLS-unaware UI surfaces (gallery thumbnails,
previews) with nothing to render. This change extracts a still frame
from the picked source video, encodes it as JPEG, uploads it
alongside the segments, and threads the URL onto every imeta's
`image` property — exactly where the gallery already reads it.

- HlsVideoPublishInput gains a posterUrl field; HlsVideoEventBuilder
  applies it to every VideoMeta.image (master + renditions).
- HlsPublishOrchestrator gains an injected uploadPoster closure
  (default no-op for tests). Failures here log + continue rather
  than aborting — the user has already paid the cost of the long
  segment uploads, so a missing poster shouldn't block publish.
- HlsPublishOrchestratorFactory wires the production closure:
  MediaMetadataRetriever frame extraction (reusing
  service.uploads.getThumbnail), JPEG encode at quality 85,
  upload via the same HlsBlobUploader, temp-file cleanup.
- New tests: poster URL propagation (builder), poster URL flow
  through orchestrator, and graceful failure when the poster
  closure throws.

https://claude.ai/code/session_011e5CNmGU5Sbzqb9w9hhd66
2026-05-03 18:36:59 +02:00
Claude 26f75aedbb fix(gallery): render HLS videos with artwork and graceful fallback
Profile-gallery thumbnails routed every MediaUrlContent through
SubcomposeAsyncImage, which works for .mp4 frames via Coil's
VideoFrameDecoder but fails on .m3u8 playlists (text manifests, not
decodable as image frames). The result was a grid of identical
PlayCircleOutline error icons for HLS gallery entries.

- Plumb artworkUri through MediaUrlVideo construction in
  ProfileGalleryEntryEvent (image()/thumb() tags) and VideoEvent
  (imeta image property), so a published poster is used when present.
- Prefer artworkUri over content.url as the SubcomposeAsyncImage
  source for videos.
- For HLS videos with no artwork, skip the doomed image fetch and
  render blurhash + centered play overlay directly. Extracted into a
  VideoPlaceholder helper reused by the error fallback.
- Overlay the play icon on successfully decoded video frames so
  videos read as videos in the grid.

https://claude.ai/code/session_011e5CNmGU5Sbzqb9w9hhd66
2026-05-03 18:36:59 +02:00
Claude 71bf1fd290 perf(okhttp): lock-free DNS cache + 24h positive TTL
The previous cache was a synchronizedMap(LinkedHashMap(access-order=true)).
Access-order LRU rewrites the linked list on get(), so every cache hit
took the global monitor — at 700 concurrent relay reconnects, the lock
serialized every dispatcher worker through a single critical section.

Switch to ConcurrentHashMap so reads are lock-free. Amethyst's steady
state is well under maxEntries (~750 distinct hosts vs cap of 2000), so
strict LRU was never going to evict anything and the access-order
machinery was pure contention.

Bump positive TTL from 5min to 24h: relay and CDN IPs change on the
order of days, and we are not a recursive resolver — there's no
correctness reason to honor authoritative TTLs. A 5min cycle made us
re-resolve all 700 relays every 5 minutes.

Also add a leader-side cache re-check after acquiring inflight
ownership. If a peer leader refreshed the entry while we were claiming
the slot, we skip getaddrinfo entirely.

Eviction is now a cheap on-demand sweep of expired entries when the
map exceeds maxEntries; in normal use it never runs.
2026-05-03 16:32:40 +00:00
David Kaspar 984109eaf9 Merge pull request #2715 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-03 18:21:46 +02:00
Crowdin Bot 1b8f64095f New Crowdin translations by GitHub Action 2026-05-03 16:21:04 +00:00
Vitor Pamplona bf82ce454b Merge pull request #2707 from mstrofnone/feat/namecoin-import-nip05-only
feat(namecoin): resolve `import` items per ifa-0001 (NIP-05 only)
2026-05-03 12:20:41 -04:00
David Kaspar 6085cbaf6b Merge pull request #2713 from vitorpamplona/l10n_crowdin_translations
New Crowdin Translations
2026-05-03 18:20:11 +02:00
Vitor Pamplona 910f6e36ca Merge pull request #2709 from mstrofnone/feat/electrumx-relay-testls-bit-direct
feat(electrumx): add bare IP peer for relay.testls.bit (23.158.233.10)
2026-05-03 12:19:36 -04:00
Vitor Pamplona c122d99a5b Merge pull request #2705 from davotoula/feat/desktop-quartz-enhancements
desktop: verify event signatures on receive
2026-05-03 12:19:07 -04:00
Crowdin Bot a73324baac New Crowdin translations by GitHub Action 2026-05-03 16:18:32 +00:00
Vitor Pamplona 401ef86810 Merge pull request #2711 from davotoula/feat/save-pdf-and-fix-mime
Save downloads: validate Content-Type & support PDF downloads
2026-05-03 12:16:55 -04:00
Claude 95a3427115 chore(okhttp): bump AmethystDns cache to 2000 entries
A Nostr feed can touch hundreds of distinct hosts (Blossom servers,
relays, NIP-05 domains, image proxies). The 256-entry cap was small
enough that active users would churn the LRU and pay extra getaddrinfo
calls. 2000 still costs <100KB of heap.
2026-05-03 16:16:18 +00:00
davotoula 81d4898f3a Allow PDF downloads, route them to Downloads/Amethyst
expose Save-to-Disk button for PDFs
add Save-to-Phone row for PDFs
2026-05-03 17:54:43 +02:00
Claude 6383a001eb Reject bogus Content-Type when saving downloaded media
Code review:
- also validate URL-extension fallback as a media type
- hoist trimInlineMetaData + drop !! on mimeType
2026-05-03 17:52:49 +02:00
Claude a2102e2bec feat(okhttp): concurrent caching DNS resolver
Avoids paying the getaddrinfo tax on every HTTP call to the same
host. Adds a single-flight, LRU+TTL DNS resolver wired into both the
media and relay OkHttp clients via a process-wide shared instance, so
resolutions cross-cut images, relays, and NIP-05 lookups.

- Per-host coalescing: N concurrent lookups for the same host share one
  upstream call.
- Different hosts proceed in parallel — no global lock around the
  upstream resolver.
- Negative cache (10s) prevents hammering on typos / dead hosts.
- Positive cache (5m) survives a feed scroll.
2026-05-03 15:17:06 +00:00
mstrofnone 07f6c352e8 feat(electrumx): add bare IP peer for relay.testls.bit (23.158.233.10)
Adds the underlying IP literal as a separate ElectrumX entry in both
the clearnet default list and the Tor list, mirroring how
46.229.238.187:57002 sits beside nmc2.bitcoins.sk:57002.

Why a bare IP entry alongside a hostname entry?

  - Robustness against unhealthy ICANN DNS paths. The relay.testls.bit
    name resolves through Namecoin (.bit), which most resolvers ship
    via a NIP-05 lookup against ElectrumX itself \u2014 chicken-and-egg if
    the only listed servers ARE .bit-resolved. A bare IP gives the
    resolver pool a completely-DNS-free anchor.
  - Same physical box, same self-signed cert, same SHA-256 (DER) pin
    (bb0b35e6\u20266cba) added in the prior commit, so no new cert pin is
    needed.

The .onion entry added in the prior commit already serves as the
DNS-free Tor anchor; this commit handles the equivalent for clearnet.

Verified end-to-end:
  node tls direct connect 23.158.233.10:50002 (no SNI) -> server.version
  reply ElectrumX 1.16.0; cert SHA-256 matches the pinned hash exactly.
2026-05-03 17:37:10 +10:00
mstrofnone 737464e528 feat(electrumx): add relay.testls.bit endpoints (clearnet TLS + Tor)
Adds the relay.testls.bit ElectrumX-NMC deployment as a second public
Namecoin name resolver. The same box also runs the Namecoin-anchored
Nostr relay at wss://relay.testls.bit/, so a single TLSA record on
d/testls anchors the trust chain for both services.

Default server set (clearnet):
  + relay.testls.bit:50002      (TLS, self-signed cert pinned)

Tor server set (onion-first):
  + 6cbn4rsk...onion:50001      (plaintext over Tor; re-uses the
                                 relay's existing v3 hidden service,
                                 onion key already authenticates)
  + relay.testls.bit:50002      (clearnet TLS fallback)

Cert pin (SHA-256 of DER):
  bb0b35e64235a794e157b69acd72da4ccc16a4d81d0ff6b1d8f7fdfc6cca6cba

The new self-signed cert is appended to PINNED_ELECTRUMX_CERTS so
strict-TLS Android devices (Samsung One UI 7, GrapheneOS) accept it
without a trust-all fallback.

Reference deployment also exposes wss://relay.testls.bit/electrumx
(WebSocket transport for browser-based Nostr clients), which inherits
the relay's existing TLSA-pinned ECDSA cert from d/testls. The current
JVM ElectrumXClient is TCP+TLS only; adding a WS variant of the client
is a larger change and tracked separately.
2026-05-03 17:23:53 +10:00
mstrofnone 3835622fc5 feat(namecoin): resolve import items per ifa-0001 (NIP-05 only)
Adds a new `NamecoinImportResolver` that follows the `import` field of a
Namecoin Domain Name Object value, recursively merging the imported
values into the importing object before `NamecoinNameResolver` reads
the `nostr` field.

Why
---
The 520-byte per-name limit on Namecoin is tight when a single record
needs `ip`, NIP-05 names, TLS, and per-subdomain map entries. A common
workaround (used by `testls.bit` and others) is to keep the apex record
small by delegating shared blocks into a sibling name via
`"import":"dd/<name>"` per ifa-0001 §"import". Without import support,
NIP-05 search for `testls.bit` (and the like) fails: the resolver sees
no `nostr` field at `d/testls` and gives up before consulting the
imported `dd/testls` that actually carries the names block.

What
----
Per ifa-0001 §"import":
  * Importer items take precedence over imported items (including `null`,
    which suppresses the imported counterpart).
  * Recursion is supported up to 4 levels (the spec minimum) by default.
    Cycles are broken by a visited-set; over-budget chains are silently
    truncated, with the importer's own items still taking effect.
  * Multiple imports in one array merge later-wins (the array's own
    order), with the importing object on top of all of them.
  * Subdomain selectors (the optional 2nd element of each inner array)
    are resolved through a `map` walk inside the imported value, with
    the standard exact-label > `*` wildcard > `""` default rules.
  * Three short-hand value forms are accepted alongside the canonical
    array-of-arrays: `"import":"d/foo"`, `"import":["d/foo"]`, and
    `"import":["d/foo","selector"]`. They map onto the canonical
    representation losslessly.
  * A failed import lookup (name not found, malformed JSON, network
    error) is treated as `{}` rather than failing the whole record, so
    transient ElectrumX hiccups don't kill resolution. The importing
    object's own items still apply.

Wires the expander into `NamecoinNameResolver.performLookup` and
`performLookupDetailed`. Records without an `import` key skip the
resolver entirely (zero extra I/O), so non-import names pay no cost.

Tests
-----
20 new tests in `NamecoinImportTest`:
  * Unit (resolver in isolation): no-import passthrough, all four
    shorthand value forms, importer precedence, null suppression,
    depth-4 recursion happy path, over-budget truncation, lookup
    failure, malformed JSON, malformed `import` value, cycle
    protection, multi-label selector descending in DNS order, wildcard
    fallback.
  * Integration with `NamecoinNameResolver`: bare and named NIP-05 across
    an import (mirroring the real-world `testls.bit -> dd/testls`
    deployment); regression guard that no-import records issue exactly
    one ElectrumX query; importer-wins on `nostr.names`; lenient
    failure when the imported boilerplate is unreachable;
    `resolveDetailed` returns `Success` and `NoNostrField` correctly
    across imports.

All 15 existing `NamecoinNameResolverTest` cases continue to pass.

Scope note
----------
This PR is intentionally scoped to the NIP-05 / search path. The
relay-resolution path (#2595) also benefits from import support; that
wiring is left to land alongside #2595 so the two PRs can move
independently.
2026-05-03 09:53:10 +10:00
davotoula 3dc79ac6a5 Code review:
rethrow CancellationException; offload NWC verify off WS thread
align verification API with Amethyst Android
2026-05-02 18:42:09 +02:00