Compare commits

...

3 commits

Author SHA1 Message Date
9f2f592f20 Merge pull request 'fix(admin): harden the boot DM — await a connected relay + guard teardown/rejection (#48, review CS-3)' (#49) from fix/boot-dm-await-connect into dev
Some checks failed
Docker image / build-and-push-image (push) Has been cancelled
Reviewed-on: #49
2026-06-27 11:19:50 +00:00
434817c899 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
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
2026-06-27 12:22:24 +02:00
d8790087b4 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
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
2026-06-27 12:03:27 +02:00
2 changed files with 42 additions and 21 deletions

View file

@ -110,9 +110,13 @@ class AdminInterface {
this.config().then((config) => {
if (config.admin?.notifyAdminsOnBoot) {
this.notifyAdminsOfNewConnection(connectionString);
// .catch so a boot-DM failure can't surface as an unhandled
// rejection (process-terminating under Node defaults). #48 / CS-3.
this.notifyAdminsOfNewConnection(connectionString).catch((e) =>
console.log('notifyAdminsOfNewConnection failed:', e?.message ?? e),
);
}
});
}).catch((e) => console.log('config() failed during admin init:', e?.message ?? e));
}
public async config(): Promise<IConfig> {
@ -127,14 +131,27 @@ class AdminInterface {
const sk = secretKeyBytes(this.adminNsec);
const pool = new RelayPool(['wss://blastr.f7z.xyz', 'wss://nostr.mutinywallet.com'], {});
pool.start();
// Give the connections a moment to come up before publishing.
await new Promise((r) => setTimeout(r, 2500));
// Wait until at least one relay is actually connected (capped), rather
// than a fixed sleep — these external public relays can be slow to come
// up, and a too-short fixed wait made the DM publish fail on boot. Still
// best-effort: if none connect in time we fall through and dmUser logs
// the publish failure without affecting the daemon. (#48)
const deadline = Date.now() + 8000;
while (pool.connectedCount() === 0 && Date.now() < deadline) {
await new Promise((r) => setTimeout(r, 100));
}
// try/finally so a throw mid-loop can't leak the pool's reconnect loops +
// sockets for the process lifetime (#48 / review CS-3). dmUser itself is
// now fully guarded, but keep the finally as belt-and-suspenders.
try {
for (const npub of this.npubs || []) {
await dmUser(sk, npub, `nsecBunker has started; use ${connectionString} to connect to it and unlock your key(s)`, pool);
}
} finally {
pool.stop();
}
}
/**
* Get the npub of the admin interface.

View file

@ -12,6 +12,11 @@ export async function dmUser(
content: string,
pool: RelayPool,
): Promise<void> {
// Guard the whole thing: nip19.decode throws on a malformed npub (passes the
// startsWith check but fails the bech32 checksum), and that previously threw
// *before* the publish try, escaping the caller. Best-effort — never throw.
// (review CS-3)
try {
const recipientHex = recipient.startsWith("npub1")
? (nip19.decode(recipient).data as string)
: recipient;
@ -25,9 +30,8 @@ export async function dmUser(
},
sk,
);
try {
await pool.publish(event);
} catch (e) {
console.log(e);
console.log('dmUser failed for', recipient, '-', (e as any)?.message ?? e);
}
}