diff --git a/src/daemon/admin/index.ts b/src/daemon/admin/index.ts index e19aaa7..569e1c3 100644 --- a/src/daemon/admin/index.ts +++ b/src/daemon/admin/index.ts @@ -414,33 +414,13 @@ class AdminInterface { return new Promise((resolve) => { console.log(`requesting permission for`, keyName, { remotePubkey, method }); - const ids: string[] = []; - let settled = false; - // Resolve once and ALWAYS clear every pending sendRequest callback — - // on timeout AND on the first admin response. Previously the timeout - // cleared nothing and a single admin's response cleared only its own - // id, so every timed-out request and (with multiple admins) the - // non-responding admins' entries leaked in transport.pending for the - // process lifetime (review AD-1/CS-2). The `settled` latch also stops - // a late approval from acting after the request resolved (review AD-2). - const finish = (value: boolean | undefined) => { - if (settled) return; - settled = true; - for (const id of ids) this.transport.clearPending(id); - resolve(value); - }; - // If an admin doesn't respond within 10 seconds, report timeout. - setTimeout(() => finish(undefined), 10000); + setTimeout(() => { + resolve(undefined); + }, 10000); for (const npub of this.npubs) { - let adminPubkey: string; - try { - adminPubkey = nip19.decode(npub).data as string; - } catch { - console.log(`skipping malformed admin npub: ${npub}`); - continue; - } + const adminPubkey = nip19.decode(npub).data as string; const params = JSON.stringify({ keyName, remotePubkey, @@ -456,18 +436,17 @@ class AdminInterface { 'nip44', NIP46_ADMIN_RESPONSE_KIND, (res) => { - if (settled) return; // ignore late / duplicate responses + this.transport.clearPending(id); this.requestPermissionResponse( remotePubkey, keyName, method, param, - finish, + resolve, res ); } ); - ids.push(id); } }); } diff --git a/src/daemon/nip46/transport.ts b/src/daemon/nip46/transport.ts index 2a08a5f..b787cee 100644 --- a/src/daemon/nip46/transport.ts +++ b/src/daemon/nip46/transport.ts @@ -132,14 +132,6 @@ export class Nip46Transport { ): string { const id = Math.random().toString(36).substring(2, 12); this.pending.set(id, cb); - // Defense-in-depth bound: callers (admin requestPermission) clear pending - // entries on resolve/timeout, but cap the map so any un-cleared path can't - // grow it without limit — evict the oldest (it would time out anyway). - // (review AD-1/CS-2) - if (this.pending.size > 1000) { - const oldest = this.pending.keys().next().value; - if (oldest !== undefined) this.pending.delete(oldest); - } const content = this.encrypt(remotePubkey, JSON.stringify({ id, method, params }), encryption); const event = finalizeEvent( { kind, created_at: Math.floor(Date.now() / 1000), tags: [["p", remotePubkey]], content },