@openclaw/nostr 2026.5.2 → 2026.5.3-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/api.js +532 -0
  2. package/dist/channel-DfEqBtUh.js +1466 -0
  3. package/dist/channel-plugin-api.js +2 -0
  4. package/dist/config-schema-DIk4jlBg.js +64 -0
  5. package/dist/default-relays-DLwdWOTu.js +4 -0
  6. package/dist/inbound-direct-dm-runtime-22bZWcIW.js +2 -0
  7. package/dist/index.js +84 -0
  8. package/dist/runtime-api.js +2 -0
  9. package/dist/setup-api.js +2 -0
  10. package/dist/setup-entry.js +11 -0
  11. package/dist/setup-plugin-api.js +165 -0
  12. package/dist/setup-surface-DxAaUTyC.js +336 -0
  13. package/dist/test-api.js +2 -0
  14. package/package.json +15 -6
  15. package/api.ts +0 -10
  16. package/channel-plugin-api.ts +0 -1
  17. package/index.ts +0 -97
  18. package/runtime-api.ts +0 -6
  19. package/setup-api.ts +0 -1
  20. package/setup-entry.ts +0 -9
  21. package/setup-plugin-api.ts +0 -3
  22. package/src/channel-api.ts +0 -15
  23. package/src/channel.inbound.test.ts +0 -176
  24. package/src/channel.outbound.test.ts +0 -128
  25. package/src/channel.setup.ts +0 -231
  26. package/src/channel.test.ts +0 -519
  27. package/src/channel.ts +0 -207
  28. package/src/config-schema.ts +0 -98
  29. package/src/default-relays.ts +0 -1
  30. package/src/gateway.ts +0 -302
  31. package/src/inbound-direct-dm-runtime.ts +0 -1
  32. package/src/metrics.ts +0 -458
  33. package/src/nostr-bus.fuzz.test.ts +0 -360
  34. package/src/nostr-bus.inbound.test.ts +0 -526
  35. package/src/nostr-bus.integration.test.ts +0 -472
  36. package/src/nostr-bus.test.ts +0 -190
  37. package/src/nostr-bus.ts +0 -789
  38. package/src/nostr-key-utils.ts +0 -94
  39. package/src/nostr-profile-core.ts +0 -134
  40. package/src/nostr-profile-http-runtime.ts +0 -6
  41. package/src/nostr-profile-http.test.ts +0 -632
  42. package/src/nostr-profile-http.ts +0 -594
  43. package/src/nostr-profile-import.test.ts +0 -119
  44. package/src/nostr-profile-import.ts +0 -262
  45. package/src/nostr-profile-url-safety.ts +0 -21
  46. package/src/nostr-profile.fuzz.test.ts +0 -430
  47. package/src/nostr-profile.test.ts +0 -412
  48. package/src/nostr-profile.ts +0 -144
  49. package/src/nostr-state-store.test.ts +0 -237
  50. package/src/nostr-state-store.ts +0 -223
  51. package/src/runtime.ts +0 -9
  52. package/src/seen-tracker.ts +0 -289
  53. package/src/session-route.ts +0 -25
  54. package/src/setup-surface.ts +0 -265
  55. package/src/test-fixtures.ts +0 -45
  56. package/src/types.ts +0 -117
  57. package/test/setup.ts +0 -5
  58. package/test-api.ts +0 -1
  59. package/tsconfig.json +0 -16
package/dist/api.js ADDED
@@ -0,0 +1,532 @@
1
+ import { o as resolveNostrAccount } from "./setup-surface-DxAaUTyC.js";
2
+ import { getPluginRuntimeGatewayRequestScope } from "./runtime-api.js";
3
+ import { n as NostrProfileSchema } from "./config-schema-DIk4jlBg.js";
4
+ import { a as setNostrRuntime, i as getNostrRuntime, n as nostrPlugin, o as contentToProfile, r as publishNostrProfile, t as getNostrProfileState } from "./channel-DfEqBtUh.js";
5
+ import { z } from "openclaw/plugin-sdk/zod";
6
+ import { SimplePool, verifyEvent } from "nostr-tools";
7
+ import { readJsonBodyWithLimit, requestBodyErrorToText } from "openclaw/plugin-sdk/webhook-request-guards";
8
+ import { createFixedWindowRateLimiter } from "openclaw/plugin-sdk/webhook-ingress";
9
+ import { isBlockedHostnameOrIp } from "openclaw/plugin-sdk/ssrf-runtime";
10
+ //#region extensions/nostr/src/nostr-profile-url-safety.ts
11
+ function validateUrlSafety(urlStr) {
12
+ try {
13
+ const url = new URL(urlStr);
14
+ if (url.protocol !== "https:") return {
15
+ ok: false,
16
+ error: "URL must use https:// protocol"
17
+ };
18
+ if (isBlockedHostnameOrIp(url.hostname.trim().toLowerCase())) return {
19
+ ok: false,
20
+ error: "URL must not point to private/internal addresses"
21
+ };
22
+ return { ok: true };
23
+ } catch {
24
+ return {
25
+ ok: false,
26
+ error: "Invalid URL format"
27
+ };
28
+ }
29
+ }
30
+ //#endregion
31
+ //#region extensions/nostr/src/nostr-profile-import.ts
32
+ /**
33
+ * Nostr Profile Import
34
+ *
35
+ * Fetches and verifies kind:0 profile events from relays.
36
+ * Used to import existing profiles before editing.
37
+ */
38
+ const DEFAULT_TIMEOUT_MS = 5e3;
39
+ /**
40
+ * Sanitize URLs in an imported profile to prevent SSRF attacks.
41
+ * Removes any URLs that don't pass SSRF validation.
42
+ */
43
+ function sanitizeProfileUrls(profile) {
44
+ const result = { ...profile };
45
+ for (const field of [
46
+ "picture",
47
+ "banner",
48
+ "website"
49
+ ]) {
50
+ const value = result[field];
51
+ if (value && typeof value === "string") {
52
+ if (!validateUrlSafety(value).ok) delete result[field];
53
+ }
54
+ }
55
+ return result;
56
+ }
57
+ /**
58
+ * Fetch the latest kind:0 profile event for a pubkey from relays.
59
+ *
60
+ * - Queries all relays in parallel
61
+ * - Takes the event with the highest created_at
62
+ * - Verifies the event signature
63
+ * - Parses and returns the profile
64
+ */
65
+ async function importProfileFromRelays(opts) {
66
+ const { pubkey, relays, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
67
+ if (!pubkey || !/^[0-9a-fA-F]{64}$/.test(pubkey)) return {
68
+ ok: false,
69
+ error: "Invalid pubkey format (must be 64 hex characters)",
70
+ relaysQueried: []
71
+ };
72
+ if (relays.length === 0) return {
73
+ ok: false,
74
+ error: "No relays configured",
75
+ relaysQueried: []
76
+ };
77
+ const pool = new SimplePool();
78
+ const relaysQueried = [];
79
+ try {
80
+ const events = [];
81
+ const timeoutPromise = new Promise((resolve) => {
82
+ setTimeout(resolve, timeoutMs);
83
+ });
84
+ const subscriptionPromise = new Promise((resolve) => {
85
+ let completed = 0;
86
+ for (const relay of relays) {
87
+ relaysQueried.push(relay);
88
+ const sub = pool.subscribeMany([relay], [{
89
+ kinds: [0],
90
+ authors: [pubkey],
91
+ limit: 1
92
+ }], {
93
+ onevent(event) {
94
+ events.push({
95
+ event,
96
+ relay
97
+ });
98
+ },
99
+ oneose() {
100
+ completed++;
101
+ if (completed >= relays.length) resolve();
102
+ },
103
+ onclose() {
104
+ completed++;
105
+ if (completed >= relays.length) resolve();
106
+ }
107
+ });
108
+ setTimeout(() => {
109
+ sub.close();
110
+ }, timeoutMs);
111
+ }
112
+ });
113
+ await Promise.race([subscriptionPromise, timeoutPromise]);
114
+ if (events.length === 0) return {
115
+ ok: false,
116
+ error: "No profile found on any relay",
117
+ relaysQueried
118
+ };
119
+ let bestEvent = null;
120
+ for (const item of events) if (!bestEvent || item.event.created_at > bestEvent.event.created_at) bestEvent = item;
121
+ if (!bestEvent) return {
122
+ ok: false,
123
+ error: "No valid profile event found",
124
+ relaysQueried
125
+ };
126
+ if (!verifyEvent(bestEvent.event)) return {
127
+ ok: false,
128
+ error: "Profile event has invalid signature",
129
+ relaysQueried,
130
+ sourceRelay: bestEvent.relay
131
+ };
132
+ let content;
133
+ try {
134
+ content = JSON.parse(bestEvent.event.content);
135
+ } catch {
136
+ return {
137
+ ok: false,
138
+ error: "Profile event has invalid JSON content",
139
+ relaysQueried,
140
+ sourceRelay: bestEvent.relay
141
+ };
142
+ }
143
+ return {
144
+ ok: true,
145
+ profile: sanitizeProfileUrls(contentToProfile(content)),
146
+ event: {
147
+ id: bestEvent.event.id,
148
+ pubkey: bestEvent.event.pubkey,
149
+ created_at: bestEvent.event.created_at
150
+ },
151
+ relaysQueried,
152
+ sourceRelay: bestEvent.relay
153
+ };
154
+ } finally {
155
+ pool.close(relays);
156
+ }
157
+ }
158
+ /**
159
+ * Merge imported profile with local profile.
160
+ *
161
+ * Strategy:
162
+ * - For each field, prefer local if set, otherwise use imported
163
+ * - This preserves user customizations while filling in missing data
164
+ */
165
+ function mergeProfiles(local, imported) {
166
+ if (!imported) return local ?? {};
167
+ if (!local) return imported;
168
+ return {
169
+ name: local.name ?? imported.name,
170
+ displayName: local.displayName ?? imported.displayName,
171
+ about: local.about ?? imported.about,
172
+ picture: local.picture ?? imported.picture,
173
+ banner: local.banner ?? imported.banner,
174
+ website: local.website ?? imported.website,
175
+ nip05: local.nip05 ?? imported.nip05,
176
+ lud16: local.lud16 ?? imported.lud16
177
+ };
178
+ }
179
+ //#endregion
180
+ //#region extensions/nostr/src/nostr-profile-http.ts
181
+ function readStringValue(value) {
182
+ return typeof value === "string" ? value : void 0;
183
+ }
184
+ function normalizeOptionalLowercaseString(value) {
185
+ if (typeof value !== "string") return;
186
+ const trimmed = value.trim();
187
+ return trimmed ? trimmed.toLowerCase() : void 0;
188
+ }
189
+ function normalizeLowercaseStringOrEmpty(value) {
190
+ return normalizeOptionalLowercaseString(value) ?? "";
191
+ }
192
+ const profileRateLimiter = createFixedWindowRateLimiter({
193
+ windowMs: 6e4,
194
+ maxRequests: 5,
195
+ maxTrackedKeys: 2048
196
+ });
197
+ function checkRateLimit(accountId) {
198
+ return !profileRateLimiter.isRateLimited(accountId);
199
+ }
200
+ const publishLocks = /* @__PURE__ */ new Map();
201
+ async function withPublishLock(accountId, fn) {
202
+ const prev = publishLocks.get(accountId) ?? Promise.resolve();
203
+ let resolve;
204
+ const next = new Promise((r) => {
205
+ resolve = r;
206
+ });
207
+ publishLocks.set(accountId, next);
208
+ await prev.catch(() => {});
209
+ try {
210
+ return await fn();
211
+ } finally {
212
+ resolve();
213
+ if (publishLocks.get(accountId) === next) publishLocks.delete(accountId);
214
+ }
215
+ }
216
+ const nip05FormatSchema = z.string().regex(/^[a-z0-9._-]+@[a-z0-9.-]+\.[a-z]{2,}$/i, "Invalid NIP-05 format (user@domain.com)").optional();
217
+ const lud16FormatSchema = z.string().regex(/^[a-z0-9._-]+@[a-z0-9.-]+\.[a-z]{2,}$/i, "Invalid Lightning address format").optional();
218
+ const ProfileUpdateSchema = NostrProfileSchema.extend({
219
+ nip05: nip05FormatSchema,
220
+ lud16: lud16FormatSchema
221
+ });
222
+ const PROFILE_MUTATION_SCOPE = "operator.admin";
223
+ function sendJson(res, status, body) {
224
+ res.statusCode = status;
225
+ res.setHeader("Content-Type", "application/json; charset=utf-8");
226
+ res.end(JSON.stringify(body));
227
+ }
228
+ async function readJsonBody(req, maxBytes = 64 * 1024, timeoutMs = 3e4) {
229
+ const result = await readJsonBodyWithLimit(req, {
230
+ maxBytes,
231
+ timeoutMs,
232
+ emptyObjectOnEmpty: true
233
+ });
234
+ if (result.ok) return result.value;
235
+ if (result.code === "PAYLOAD_TOO_LARGE") throw new Error("Request body too large");
236
+ if (result.code === "REQUEST_BODY_TIMEOUT") throw new Error(requestBodyErrorToText("REQUEST_BODY_TIMEOUT"));
237
+ if (result.code === "CONNECTION_CLOSED") throw new Error(requestBodyErrorToText("CONNECTION_CLOSED"));
238
+ throw new Error(result.code === "INVALID_JSON" ? "Invalid JSON" : result.error);
239
+ }
240
+ function parseAccountIdFromPath(pathname) {
241
+ return pathname.match(/^\/api\/channels\/nostr\/([^/]+)\/profile/)?.[1] ?? null;
242
+ }
243
+ function isLoopbackRemoteAddress(remoteAddress) {
244
+ if (!remoteAddress) return false;
245
+ const ipLower = normalizeLowercaseStringOrEmpty(remoteAddress).replace(/^\[|\]$/g, "");
246
+ if (ipLower === "::1") return true;
247
+ if (ipLower === "127.0.0.1" || ipLower.startsWith("127.")) return true;
248
+ const v4Mapped = ipLower.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
249
+ if (v4Mapped) return isLoopbackRemoteAddress(v4Mapped[1]);
250
+ return false;
251
+ }
252
+ function isLoopbackOriginLike(value) {
253
+ try {
254
+ const hostname = normalizeLowercaseStringOrEmpty(new URL(value).hostname);
255
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1";
256
+ } catch {
257
+ return false;
258
+ }
259
+ }
260
+ function firstHeaderValue(value) {
261
+ if (Array.isArray(value)) return value[0];
262
+ return readStringValue(value);
263
+ }
264
+ function normalizeIpCandidate(raw) {
265
+ const unquoted = raw.trim().replace(/^"|"$/g, "");
266
+ const bracketedWithOptionalPort = unquoted.match(/^\[([^[\]]+)\](?::\d+)?$/);
267
+ if (bracketedWithOptionalPort) return bracketedWithOptionalPort[1] ?? "";
268
+ const ipv4WithPort = unquoted.match(/^(\d+\.\d+\.\d+\.\d+):\d+$/);
269
+ if (ipv4WithPort) return ipv4WithPort[1] ?? "";
270
+ return unquoted;
271
+ }
272
+ function hasNonLoopbackForwardedClient(req) {
273
+ const forwardedFor = firstHeaderValue(req.headers["x-forwarded-for"]);
274
+ if (forwardedFor) for (const hop of forwardedFor.split(",")) {
275
+ const candidate = normalizeIpCandidate(hop);
276
+ if (!candidate) continue;
277
+ if (!isLoopbackRemoteAddress(candidate)) return true;
278
+ }
279
+ const realIp = firstHeaderValue(req.headers["x-real-ip"]);
280
+ if (realIp) {
281
+ const candidate = normalizeIpCandidate(realIp);
282
+ if (candidate && !isLoopbackRemoteAddress(candidate)) return true;
283
+ }
284
+ return false;
285
+ }
286
+ function enforceLoopbackMutationGuards(ctx, req, res) {
287
+ const remoteAddress = req.socket.remoteAddress;
288
+ if (!isLoopbackRemoteAddress(remoteAddress)) {
289
+ ctx.log?.warn?.(`Rejected mutation from non-loopback remoteAddress=${String(remoteAddress)}`);
290
+ sendJson(res, 403, {
291
+ ok: false,
292
+ error: "Forbidden"
293
+ });
294
+ return false;
295
+ }
296
+ if (hasNonLoopbackForwardedClient(req)) {
297
+ ctx.log?.warn?.("Rejected mutation with non-loopback forwarded client headers");
298
+ sendJson(res, 403, {
299
+ ok: false,
300
+ error: "Forbidden"
301
+ });
302
+ return false;
303
+ }
304
+ if (normalizeOptionalLowercaseString(firstHeaderValue(req.headers["sec-fetch-site"])) === "cross-site") {
305
+ ctx.log?.warn?.("Rejected mutation with cross-site sec-fetch-site header");
306
+ sendJson(res, 403, {
307
+ ok: false,
308
+ error: "Forbidden"
309
+ });
310
+ return false;
311
+ }
312
+ const origin = firstHeaderValue(req.headers.origin);
313
+ if (typeof origin === "string" && !isLoopbackOriginLike(origin)) {
314
+ ctx.log?.warn?.(`Rejected mutation with non-loopback origin=${origin}`);
315
+ sendJson(res, 403, {
316
+ ok: false,
317
+ error: "Forbidden"
318
+ });
319
+ return false;
320
+ }
321
+ const referer = firstHeaderValue(req.headers.referer ?? req.headers.referrer);
322
+ if (typeof referer === "string" && !isLoopbackOriginLike(referer)) {
323
+ ctx.log?.warn?.(`Rejected mutation with non-loopback referer=${referer}`);
324
+ sendJson(res, 403, {
325
+ ok: false,
326
+ error: "Forbidden"
327
+ });
328
+ return false;
329
+ }
330
+ return true;
331
+ }
332
+ function enforceGatewayMutationScope(ctx, accountId, res) {
333
+ const runtimeScopes = getPluginRuntimeGatewayRequestScope()?.client?.connect?.scopes;
334
+ if ((Array.isArray(runtimeScopes) ? runtimeScopes : []).includes(PROFILE_MUTATION_SCOPE)) return true;
335
+ ctx.log?.warn?.(`[${accountId}] Rejected profile mutation missing ${PROFILE_MUTATION_SCOPE}`);
336
+ sendJson(res, 403, {
337
+ ok: false,
338
+ error: `missing scope: ${PROFILE_MUTATION_SCOPE}`
339
+ });
340
+ return false;
341
+ }
342
+ function createNostrProfileHttpHandler(ctx) {
343
+ return async (req, res) => {
344
+ const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
345
+ if (!url.pathname.startsWith("/api/channels/nostr/")) return false;
346
+ const accountId = parseAccountIdFromPath(url.pathname);
347
+ if (!accountId) return false;
348
+ const isImport = url.pathname.endsWith("/profile/import");
349
+ if (!(url.pathname.endsWith("/profile") || isImport)) return false;
350
+ try {
351
+ if (req.method === "GET" && !isImport) return await handleGetProfile(accountId, ctx, res);
352
+ if (req.method === "PUT" && !isImport) return await handleUpdateProfile(accountId, ctx, req, res);
353
+ if (req.method === "POST" && isImport) return await handleImportProfile(accountId, ctx, req, res);
354
+ sendJson(res, 405, {
355
+ ok: false,
356
+ error: "Method not allowed"
357
+ });
358
+ return true;
359
+ } catch (err) {
360
+ ctx.log?.error(`Profile HTTP error: ${String(err)}`);
361
+ sendJson(res, 500, {
362
+ ok: false,
363
+ error: "Internal server error"
364
+ });
365
+ return true;
366
+ }
367
+ };
368
+ }
369
+ async function handleGetProfile(accountId, ctx, res) {
370
+ const configProfile = ctx.getConfigProfile(accountId);
371
+ const publishState = await getNostrProfileState(accountId);
372
+ sendJson(res, 200, {
373
+ ok: true,
374
+ profile: configProfile ?? null,
375
+ publishState: publishState ?? null
376
+ });
377
+ return true;
378
+ }
379
+ async function handleUpdateProfile(accountId, ctx, req, res) {
380
+ if (!enforceGatewayMutationScope(ctx, accountId, res)) return true;
381
+ if (!enforceLoopbackMutationGuards(ctx, req, res)) return true;
382
+ if (!checkRateLimit(accountId)) {
383
+ sendJson(res, 429, {
384
+ ok: false,
385
+ error: "Rate limit exceeded (5 requests/minute)"
386
+ });
387
+ return true;
388
+ }
389
+ let body;
390
+ try {
391
+ body = await readJsonBody(req);
392
+ } catch (err) {
393
+ sendJson(res, 400, {
394
+ ok: false,
395
+ error: String(err)
396
+ });
397
+ return true;
398
+ }
399
+ const parseResult = ProfileUpdateSchema.safeParse(body);
400
+ if (!parseResult.success) {
401
+ sendJson(res, 400, {
402
+ ok: false,
403
+ error: "Validation failed",
404
+ details: parseResult.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`)
405
+ });
406
+ return true;
407
+ }
408
+ const profile = parseResult.data;
409
+ if (profile.picture) {
410
+ const pictureCheck = validateUrlSafety(profile.picture);
411
+ if (!pictureCheck.ok) {
412
+ sendJson(res, 400, {
413
+ ok: false,
414
+ error: `picture: ${pictureCheck.error}`
415
+ });
416
+ return true;
417
+ }
418
+ }
419
+ if (profile.banner) {
420
+ const bannerCheck = validateUrlSafety(profile.banner);
421
+ if (!bannerCheck.ok) {
422
+ sendJson(res, 400, {
423
+ ok: false,
424
+ error: `banner: ${bannerCheck.error}`
425
+ });
426
+ return true;
427
+ }
428
+ }
429
+ if (profile.website) {
430
+ const websiteCheck = validateUrlSafety(profile.website);
431
+ if (!websiteCheck.ok) {
432
+ sendJson(res, 400, {
433
+ ok: false,
434
+ error: `website: ${websiteCheck.error}`
435
+ });
436
+ return true;
437
+ }
438
+ }
439
+ const mergedProfile = {
440
+ ...ctx.getConfigProfile(accountId) ?? {},
441
+ ...profile
442
+ };
443
+ try {
444
+ const result = await withPublishLock(accountId, async () => {
445
+ return await publishNostrProfile(accountId, mergedProfile);
446
+ });
447
+ if (result.successes.length > 0) {
448
+ await ctx.updateConfigProfile(accountId, mergedProfile);
449
+ ctx.log?.info(`[${accountId}] Profile published to ${result.successes.length} relay(s)`);
450
+ } else ctx.log?.warn(`[${accountId}] Profile publish failed on all relays`);
451
+ sendJson(res, 200, {
452
+ ok: true,
453
+ eventId: result.eventId,
454
+ createdAt: result.createdAt,
455
+ successes: result.successes,
456
+ failures: result.failures,
457
+ persisted: result.successes.length > 0
458
+ });
459
+ } catch (err) {
460
+ ctx.log?.error(`[${accountId}] Profile publish error: ${String(err)}`);
461
+ sendJson(res, 500, {
462
+ ok: false,
463
+ error: `Publish failed: ${String(err)}`
464
+ });
465
+ }
466
+ return true;
467
+ }
468
+ async function handleImportProfile(accountId, ctx, req, res) {
469
+ if (!enforceGatewayMutationScope(ctx, accountId, res)) return true;
470
+ if (!enforceLoopbackMutationGuards(ctx, req, res)) return true;
471
+ const accountInfo = ctx.getAccountInfo(accountId);
472
+ if (!accountInfo) {
473
+ sendJson(res, 404, {
474
+ ok: false,
475
+ error: `Account not found: ${accountId}`
476
+ });
477
+ return true;
478
+ }
479
+ const { pubkey, relays } = accountInfo;
480
+ if (!pubkey) {
481
+ sendJson(res, 400, {
482
+ ok: false,
483
+ error: "Account has no public key configured"
484
+ });
485
+ return true;
486
+ }
487
+ let autoMerge = false;
488
+ try {
489
+ const body = await readJsonBody(req);
490
+ if (typeof body === "object" && body !== null) autoMerge = body.autoMerge === true;
491
+ } catch {}
492
+ ctx.log?.info(`[${accountId}] Importing profile for ${pubkey.slice(0, 8)}...`);
493
+ const result = await importProfileFromRelays({
494
+ pubkey,
495
+ relays,
496
+ timeoutMs: 1e4
497
+ });
498
+ if (!result.ok) {
499
+ sendJson(res, 200, {
500
+ ok: false,
501
+ error: result.error,
502
+ relaysQueried: result.relaysQueried
503
+ });
504
+ return true;
505
+ }
506
+ if (autoMerge && result.profile) {
507
+ const merged = mergeProfiles(ctx.getConfigProfile(accountId), result.profile);
508
+ await ctx.updateConfigProfile(accountId, merged);
509
+ ctx.log?.info(`[${accountId}] Profile imported and merged`);
510
+ sendJson(res, 200, {
511
+ ok: true,
512
+ imported: result.profile,
513
+ merged,
514
+ saved: true,
515
+ event: result.event,
516
+ sourceRelay: result.sourceRelay,
517
+ relaysQueried: result.relaysQueried
518
+ });
519
+ return true;
520
+ }
521
+ sendJson(res, 200, {
522
+ ok: true,
523
+ imported: result.profile,
524
+ saved: false,
525
+ event: result.event,
526
+ sourceRelay: result.sourceRelay,
527
+ relaysQueried: result.relaysQueried
528
+ });
529
+ return true;
530
+ }
531
+ //#endregion
532
+ export { createNostrProfileHttpHandler, getNostrRuntime, getPluginRuntimeGatewayRequestScope, nostrPlugin, resolveNostrAccount, setNostrRuntime };