fix(admin): harden the boot DM — await a connected relay + guard teardown/rejection (#48, review CS-3) #49

Merged
padreug merged 2 commits from fix/boot-dm-await-connect into dev 2026-06-27 11:19:51 +00:00
Owner

Closes #48; also folds in review finding CS-3.

The boot-time admin DM (notifyAdminsOfNewConnection) opens a throwaway RelayPool to two external public relays, then publishes. Two problems:

1. Publish failed on boot (#48). It slept a fixed 2500ms before publishing; those relays are often slower than that to connect, so on the aio-demo deploy the publish failed:

Error: publish failed on all relays after 5 attempts: relay not connected:
  wss://blastr.f7z.xyz; wss://nostr.mutinywallet.com

Non-fatal (caught + logged; the daemon reached "ready to serve" and the spire was signing) but noisy every boot. Fix: poll pool.connectedCount() > 0 up to an 8s cap — send as soon as a relay is up, still best-effort.

2. Leak + unhandled-rejection on a bad npub (review CS-3). The method had a bare pool.start()/loop/pool.stop() with no try/finally, and dmUser's nip19.decode/nip04.encrypt ran before its publish-only try. A malformed admin npub1… (passes startsWith, fails the bech32 checksum) threw through the loop, skipping pool.stop() (leaking the pool's reconnect loops + sockets), and since the method is called from an un-.catch'd .then in the constructor, the throw became an unhandled rejection — process-terminating under Node defaults. Fix:

  • try/finally { pool.stop() } around the publish loop;
  • .catch on the constructor's .then (both the notify and the config() it chains off);
  • dmUser now guards its whole body — best-effort, never throws.

One-file-ish change (admin/index.ts + utils/dm-user.ts). tsc at baseline; daemon bundles; admin suite green.

Refs: #48, #45; review CS-3

🤖 Generated with Claude Code

Closes #48; also folds in review finding **CS-3**. The boot-time admin DM (`notifyAdminsOfNewConnection`) opens a throwaway `RelayPool` to two **external** public relays, then publishes. Two problems: **1. Publish failed on boot (#48).** It slept a fixed `2500ms` before publishing; those relays are often slower than that to connect, so on the `aio-demo` deploy the publish failed: ``` Error: publish failed on all relays after 5 attempts: relay not connected: wss://blastr.f7z.xyz; wss://nostr.mutinywallet.com ``` Non-fatal (caught + logged; the daemon reached "ready to serve" and the spire was signing) but noisy every boot. **Fix:** poll `pool.connectedCount() > 0` up to an 8s cap — send as soon as a relay is up, still best-effort. **2. Leak + unhandled-rejection on a bad npub (review CS-3).** The method had a bare `pool.start()`/loop/`pool.stop()` with no `try/finally`, and `dmUser`'s `nip19.decode`/`nip04.encrypt` ran *before* its publish-only `try`. A malformed admin `npub1…` (passes `startsWith`, fails the bech32 checksum) threw through the loop, skipping `pool.stop()` (leaking the pool's reconnect loops + sockets), and since the method is called from an un-`.catch`'d `.then` in the constructor, the throw became an **unhandled rejection** — process-terminating under Node defaults. **Fix:** - `try/finally { pool.stop() }` around the publish loop; - `.catch` on the constructor's `.then` (both the notify and the `config()` it chains off); - `dmUser` now guards its whole body — best-effort, never throws. One-file-ish change (`admin/index.ts` + `utils/dm-user.ts`). `tsc` at baseline; daemon bundles; admin suite green. Refs: #48, #45; review CS-3 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(admin): wait for a connected relay before the boot DM, not a fixed sleep (#48)
Some checks failed
Docker image / build-and-push-image (push) Has been cancelled
d8790087b4
The boot-time admin notification (notifyAdminsOfNewConnection) spun up a
throwaway RelayPool to two external public relays, slept a fixed 2500ms, then
published. Those relays are often slow to connect, so on the aio-demo deploy the
publish failed on boot ("relay not connected: wss://blastr.f7z.xyz; …") — caught
and logged, non-fatal, but noisy every boot.

Poll pool.connectedCount() > 0 up to an 8s cap instead: send as soon as a relay
is up, and still best-effort — fall through and let dmUser log the failure if
none connect in time.

Refs: #48, #45
fix(admin): harden the boot DM — guard teardown + unhandled rejection (review CS-3)
Some checks failed
Docker image / build-and-push-image (push) Has been cancelled
434817c899
Folds the review's CS-3 finding into the boot-DM fix:
- notifyAdminsOfNewConnection wraps the publish loop in try/finally so a throw
  mid-loop can't leak the throwaway pool's reconnect loops + sockets for the
  process lifetime.
- The constructor's fire-and-forget call now .catch()es (both the notify and the
  config() it chains off), so a boot-DM failure can't surface as an unhandled
  rejection — process-terminating under Node defaults.
- dmUser guards its whole body: nip19.decode/nip04.encrypt ran *before* the old
  publish-only try, so a malformed admin npub (passes startsWith, fails the
  bech32 checksum) threw past the caller. Now best-effort, never throws.

Refs: #48, review CS-3
padreug changed title from fix(admin): wait for a connected relay before the boot DM, not a fixed sleep (#48) to fix(admin): harden the boot DM — await a connected relay + guard teardown/rejection (#48, review CS-3) 2026-06-27 10:22:49 +00:00
padreug deleted branch fix/boot-dm-await-connect 2026-06-27 11:19:51 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/nsecbunkerd!49
No description provided.