Skip to content

Commit 37ccec0

Browse files
committed
fix(nostr): cap profile import relay timers
1 parent cb4d2e7 commit 37ccec0

2 files changed

Lines changed: 41 additions & 4 deletions

File tree

extensions/nostr/src/nostr-profile-import.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Tests for Nostr Profile Import
33
*/
44

5+
import { MAX_TIMER_TIMEOUT_MS } from "openclaw/plugin-sdk/number-runtime";
56
import { describe, it, expect, beforeEach, vi } from "vitest";
67
import type { NostrProfile } from "./config-schema.js";
78
import { importProfileFromRelays, mergeProfiles } from "./nostr-profile-import.js";
@@ -63,6 +64,27 @@ describe("nostr-profile-import", () => {
6364
limit: 1,
6465
});
6566
});
67+
68+
it("caps oversized relay timeouts and clears pending timeout handles", async () => {
69+
vi.useFakeTimers();
70+
try {
71+
const timeoutSpy = vi.spyOn(globalThis, "setTimeout");
72+
const clearSpy = vi.spyOn(globalThis, "clearTimeout");
73+
74+
await importProfileFromRelays({
75+
pubkey: "a".repeat(64),
76+
relays: ["wss://relay.example"],
77+
timeoutMs: Number.MAX_SAFE_INTEGER,
78+
});
79+
80+
expect(timeoutSpy).toHaveBeenCalledWith(expect.any(Function), MAX_TIMER_TIMEOUT_MS);
81+
expect(timeoutSpy).toHaveBeenCalledTimes(2);
82+
expect(clearSpy).toHaveBeenCalledTimes(2);
83+
} finally {
84+
vi.useRealTimers();
85+
vi.restoreAllMocks();
86+
}
87+
});
6688
});
6789

6890
describe("mergeProfiles", () => {

extensions/nostr/src/nostr-profile-import.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import { SimplePool, verifyEvent, type Event } from "nostr-tools";
9+
import { resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
910
import type { NostrProfile } from "./config-schema.js";
1011
import { validateUrlSafety } from "./nostr-profile-url-safety.js";
1112
import { contentToProfile, type ProfileContent } from "./nostr-profile.js";
@@ -85,7 +86,8 @@ function sanitizeProfileUrls(profile: NostrProfile): NostrProfile {
8586
export async function importProfileFromRelays(
8687
opts: ProfileImportOptions,
8788
): Promise<ProfileImportResult> {
88-
const { pubkey, relays, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
89+
const { pubkey, relays } = opts;
90+
const timeoutMs = resolveTimerTimeoutMs(opts.timeoutMs, DEFAULT_TIMEOUT_MS);
8991

9092
if (!pubkey || !/^[0-9a-fA-F]{64}$/.test(pubkey)) {
9193
return {
@@ -105,14 +107,21 @@ export async function importProfileFromRelays(
105107

106108
const pool = new SimplePool();
107109
const relaysQueried: string[] = [];
110+
const timers: Array<ReturnType<typeof setTimeout>> = [];
111+
const scheduleTimeout = (callback: () => void) => {
112+
const timer = setTimeout(callback, timeoutMs);
113+
timer.unref?.();
114+
timers.push(timer);
115+
return timer;
116+
};
108117

109118
try {
110119
// Query all relays for kind:0 events from this pubkey
111120
const events: Array<{ event: Event; relay: string }> = [];
112121

113122
// Create timeout promise
114123
const timeoutPromise = new Promise<void>((resolve) => {
115-
setTimeout(resolve, timeoutMs);
124+
scheduleTimeout(resolve);
116125
});
117126

118127
// Create subscription promise
@@ -147,14 +156,17 @@ export async function importProfileFromRelays(
147156
});
148157

149158
// Clean up subscription after timeout
150-
setTimeout(() => {
159+
scheduleTimeout(() => {
151160
sub.close();
152-
}, timeoutMs);
161+
});
153162
}
154163
});
155164

156165
// Wait for either all relays to respond or timeout
157166
await Promise.race([subscriptionPromise, timeoutPromise]);
167+
for (const timer of timers.splice(0)) {
168+
clearTimeout(timer);
169+
}
158170

159171
// No events found
160172
if (events.length === 0) {
@@ -223,6 +235,9 @@ export async function importProfileFromRelays(
223235
sourceRelay: bestEvent.relay,
224236
};
225237
} finally {
238+
for (const timer of timers) {
239+
clearTimeout(timer);
240+
}
226241
pool.close(relays);
227242
}
228243
}

0 commit comments

Comments
 (0)