@openclaw/buzz 2026.9.6 → 2026.9.8

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.
@@ -0,0 +1,537 @@
1
+ import { l as BUZZ_CHANNEL_ID_PATTERN, m as parseBuzzTarget, u as buildBuzzTarget } from "./types-DRUZMyTx.mjs";
2
+ import { t as applyBuzzDirectoryQueryAndLimit } from "./directory-query-D1BNu8nO.mjs";
3
+ import { Relay, finalizeEvent } from "nostr-tools";
4
+ import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
5
+ import { readProviderJsonObjectResponse } from "openclaw/plugin-sdk/provider-http";
6
+ import { fetchWithSsrFGuard, ssrfPolicyFromHttpBaseUrlAllowedOrigin } from "openclaw/plugin-sdk/ssrf-runtime";
7
+ //#region extensions/buzz/src/event-order.ts
8
+ function isNewerBuzzRevision(candidate, current) {
9
+ return !current || candidate.createdAt > current.createdAt || candidate.createdAt === current.createdAt && candidate.eventId < current.eventId;
10
+ }
11
+ const BUZZ_ROOM_METADATA_KIND = 39e3;
12
+ const DEFAULT_BUZZ_DIRECTORY_PROFILE_LIMIT = 2e3;
13
+ const HEX_PUBLIC_KEY_PATTERN$2 = /^[0-9a-f]{64}$/u;
14
+ const MAX_DIRECTORY_NAME_CHARS = 512;
15
+ const MAX_DIRECTORY_HANDLE_CHARS = 320;
16
+ const MAX_DIRECTORY_URL_CHARS = 4096;
17
+ function normalizeBoundedString(value, maxChars) {
18
+ if (typeof value !== "string") return;
19
+ const trimmed = value.trim();
20
+ if (!trimmed) return;
21
+ return truncateUtf16Safe(trimmed, maxChars);
22
+ }
23
+ function readPreferredString(params) {
24
+ if (Object.hasOwn(params.content, params.primary)) return normalizeBoundedString(params.content[params.primary], params.maxChars);
25
+ return normalizeBoundedString(params.content[params.fallback], params.maxChars);
26
+ }
27
+ function fallbackPublicKeyLabel(publicKey) {
28
+ return `${publicKey.slice(0, 8)}...${publicKey.slice(-6)}`;
29
+ }
30
+ function parseBuzzDirectoryProfileEvent(event) {
31
+ const publicKey = event.pubkey.trim().toLowerCase();
32
+ if (event.kind !== 0 || !HEX_PUBLIC_KEY_PATTERN$2.test(publicKey)) return;
33
+ let content;
34
+ try {
35
+ const parsed = JSON.parse(event.content);
36
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return;
37
+ content = parsed;
38
+ } catch {
39
+ return;
40
+ }
41
+ return {
42
+ publicKey,
43
+ displayName: readPreferredString({
44
+ content,
45
+ primary: "display_name",
46
+ fallback: "name",
47
+ maxChars: MAX_DIRECTORY_NAME_CHARS
48
+ }),
49
+ handle: normalizeBoundedString(content.nip05, MAX_DIRECTORY_HANDLE_CHARS),
50
+ avatarUrl: readPreferredString({
51
+ content,
52
+ primary: "picture",
53
+ fallback: "image",
54
+ maxChars: MAX_DIRECTORY_URL_CHARS
55
+ }),
56
+ createdAt: event.created_at,
57
+ eventId: event.id
58
+ };
59
+ }
60
+ function parseBuzzDirectoryRoomEvent(event) {
61
+ if (event.kind !== 39e3) return;
62
+ const roomId = event.tags.find((tag) => tag[0] === "d")?.[1]?.trim().toLowerCase();
63
+ if (!roomId) return;
64
+ try {
65
+ parseBuzzTarget(roomId);
66
+ } catch {
67
+ return;
68
+ }
69
+ return {
70
+ roomId,
71
+ name: normalizeBoundedString(event.tags.find((tag) => tag[0] === "name")?.[1], MAX_DIRECTORY_NAME_CHARS),
72
+ archived: event.tags.some((tag) => tag[0] === "archived" && tag[1] === "true"),
73
+ createdAt: event.created_at,
74
+ eventId: event.id
75
+ };
76
+ }
77
+ var BuzzDirectoryState = class {
78
+ #publicKey;
79
+ #fallbackProfileName;
80
+ #configuredRoomIds;
81
+ #profileLimit;
82
+ #memberships = /* @__PURE__ */ new Map();
83
+ #profilePublicKeys = /* @__PURE__ */ new Set();
84
+ #profiles = /* @__PURE__ */ new Map();
85
+ #rooms = /* @__PURE__ */ new Map();
86
+ constructor(params) {
87
+ this.#publicKey = params.publicKey.trim().toLowerCase();
88
+ this.#fallbackProfileName = params.fallbackProfileName.trim() || "OpenClaw";
89
+ this.#configuredRoomIds = new Set(params.channelIds.map(parseBuzzTarget));
90
+ const requestedProfileLimit = params.profileLimit ?? DEFAULT_BUZZ_DIRECTORY_PROFILE_LIMIT;
91
+ this.#profileLimit = Number.isFinite(requestedProfileLimit) && requestedProfileLimit >= 0 ? Math.floor(requestedProfileLimit) : DEFAULT_BUZZ_DIRECTORY_PROFILE_LIMIT;
92
+ if (this.#profileLimit > 0) this.#profilePublicKeys.add(this.#publicKey);
93
+ }
94
+ replaceMemberships(memberships) {
95
+ const nextMemberships = /* @__PURE__ */ new Map();
96
+ const memberPublicKeys = /* @__PURE__ */ new Set();
97
+ for (const roomId of this.#configuredRoomIds) {
98
+ const membership = memberships.get(roomId);
99
+ if (!membership) continue;
100
+ nextMemberships.set(roomId, membership);
101
+ for (const publicKey of membership.members) memberPublicKeys.add(publicKey);
102
+ }
103
+ memberPublicKeys.delete(this.#publicKey);
104
+ const nextProfilePublicKeys = this.#profileLimit === 0 ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set([this.#publicKey, ...[...memberPublicKeys].toSorted().slice(0, this.#profileLimit - 1)]);
105
+ const profileSelectionChanged = nextProfilePublicKeys.size !== this.#profilePublicKeys.size || [...nextProfilePublicKeys].some((publicKey) => !this.#profilePublicKeys.has(publicKey));
106
+ this.#memberships = nextMemberships;
107
+ this.#profilePublicKeys = nextProfilePublicKeys;
108
+ for (const publicKey of this.#profiles.keys()) if (!nextProfilePublicKeys.has(publicKey)) this.#profiles.delete(publicKey);
109
+ return profileSelectionChanged;
110
+ }
111
+ profilePublicKeys() {
112
+ return [...this.#profilePublicKeys];
113
+ }
114
+ activeRoomIds() {
115
+ return [...this.#configuredRoomIds].filter((roomId) => !this.#rooms.get(roomId)?.archived);
116
+ }
117
+ isRoomArchived(roomId) {
118
+ return this.#rooms.get(parseBuzzTarget(roomId))?.archived === true;
119
+ }
120
+ isBotMember(roomId, publicKey) {
121
+ return this.isMember(roomId, publicKey) && this.#memberships.get(parseBuzzTarget(roomId))?.roles.get(publicKey) === "bot";
122
+ }
123
+ isMember(roomId, publicKey) {
124
+ const normalizedRoomId = parseBuzzTarget(roomId);
125
+ return !this.#rooms.get(normalizedRoomId)?.archived && this.#memberships.get(normalizedRoomId)?.members.has(publicKey) === true;
126
+ }
127
+ applyProfileEvent(event) {
128
+ const profile = parseBuzzDirectoryProfileEvent(event);
129
+ if (!profile || !this.#profilePublicKeys.has(profile.publicKey) || !isNewerBuzzRevision(profile, this.#profiles.get(profile.publicKey))) return false;
130
+ this.#profiles.set(profile.publicKey, profile);
131
+ return true;
132
+ }
133
+ applyRoomEvent(event) {
134
+ const room = parseBuzzDirectoryRoomEvent(event);
135
+ if (!room || !this.#configuredRoomIds.has(room.roomId) || !isNewerBuzzRevision(room, this.#rooms.get(room.roomId))) return false;
136
+ this.#rooms.set(room.roomId, room);
137
+ return true;
138
+ }
139
+ resolveSenderName(publicKey) {
140
+ const normalized = publicKey.trim().toLowerCase();
141
+ return this.#profiles.get(normalized)?.displayName ?? fallbackPublicKeyLabel(normalized);
142
+ }
143
+ resolveRoomName(roomId) {
144
+ const normalized = parseBuzzTarget(roomId);
145
+ return this.#rooms.get(normalized)?.name ?? normalized;
146
+ }
147
+ self() {
148
+ return this.#buildUserEntry(this.#publicKey);
149
+ }
150
+ listPeers(params) {
151
+ const peers = /* @__PURE__ */ new Set();
152
+ for (const roomId of this.activeRoomIds()) {
153
+ const membership = this.#memberships.get(roomId);
154
+ if (!membership) continue;
155
+ for (const publicKey of membership.members) if (publicKey !== this.#publicKey) peers.add(publicKey);
156
+ }
157
+ const entries = [...peers].map((publicKey) => this.#buildUserEntry(publicKey)).toSorted(compareDirectoryEntries);
158
+ return applyBuzzDirectoryQueryAndLimit(entries, params);
159
+ }
160
+ listGroups(params) {
161
+ const entries = this.activeRoomIds().map((roomId) => this.#buildRoomEntry(roomId)).toSorted(compareDirectoryEntries);
162
+ return applyBuzzDirectoryQueryAndLimit(entries, params);
163
+ }
164
+ listGroupMembers(params) {
165
+ let roomId;
166
+ try {
167
+ roomId = parseBuzzTarget(params.groupId);
168
+ } catch {
169
+ return [];
170
+ }
171
+ if (this.#rooms.get(roomId)?.archived) return [];
172
+ const membership = this.#memberships.get(roomId);
173
+ if (!membership) return [];
174
+ const entries = [...membership.members].map((publicKey) => {
175
+ const entry = this.#buildUserEntry(publicKey);
176
+ entry.raw = {
177
+ publicKey,
178
+ role: membership.roles.get(publicKey),
179
+ roomId
180
+ };
181
+ return entry;
182
+ }).toSorted(compareDirectoryEntries);
183
+ return applyBuzzDirectoryQueryAndLimit(entries, { limit: params.limit });
184
+ }
185
+ mentionMembers(roomId) {
186
+ const normalized = parseBuzzTarget(roomId);
187
+ if (this.#rooms.get(normalized)?.archived) return;
188
+ const membership = this.#memberships.get(normalized);
189
+ if (!membership) return;
190
+ return [...membership.members].map((publicKey) => ({
191
+ publicKey,
192
+ displayName: this.#profiles.get(publicKey)?.displayName
193
+ })).toSorted((left, right) => left.publicKey.localeCompare(right.publicKey));
194
+ }
195
+ #buildUserEntry(publicKey) {
196
+ const profile = this.#profiles.get(publicKey);
197
+ return {
198
+ kind: "user",
199
+ id: publicKey,
200
+ name: profile?.displayName ?? (publicKey === this.#publicKey ? this.#fallbackProfileName : fallbackPublicKeyLabel(publicKey)),
201
+ handle: profile?.handle,
202
+ avatarUrl: profile?.avatarUrl,
203
+ raw: { publicKey }
204
+ };
205
+ }
206
+ #buildRoomEntry(roomId) {
207
+ const room = this.#rooms.get(roomId);
208
+ return {
209
+ kind: "group",
210
+ id: buildBuzzTarget(roomId),
211
+ name: room?.name ?? roomId,
212
+ handle: room?.name ? `#${room.name}` : void 0,
213
+ raw: { roomId }
214
+ };
215
+ }
216
+ };
217
+ function compareDirectoryEntries(a, b) {
218
+ const aLabel = a.name ?? a.handle ?? a.id;
219
+ const bLabel = b.name ?? b.handle ?? b.id;
220
+ return aLabel.localeCompare(bLabel) || a.id.localeCompare(b.id);
221
+ }
222
+ //#endregion
223
+ //#region extensions/buzz/src/subscription-budget.ts
224
+ const BUZZ_DIRECTORY_MAX_PROFILE_SUBSCRIPTIONS = 10;
225
+ const BUZZ_MAX_CONFIGURED_ROOMS = 1020;
226
+ function resolveBuzzSubscriptionBudget(roomCount) {
227
+ if (!Number.isSafeInteger(roomCount) || roomCount < 0) throw new Error("Buzz configured room count must be a non-negative integer");
228
+ const availableProfileSubscriptions = 1020 - roomCount;
229
+ if (availableProfileSubscriptions < 0) throw new Error(`Buzz supports at most ${BUZZ_MAX_CONFIGURED_ROOMS} configured rooms per account`);
230
+ return { profileLimit: Math.min(BUZZ_DIRECTORY_MAX_PROFILE_SUBSCRIPTIONS, availableProfileSubscriptions) * 200 };
231
+ }
232
+ //#endregion
233
+ //#region extensions/buzz/src/relay-subscription.ts
234
+ const snapshotQueries = /* @__PURE__ */ new WeakMap();
235
+ function openBuzzRelaySubscription(relay, filters, params, requestFilters = filters) {
236
+ relay.idleSince = void 0;
237
+ relay.ongoingOperations += 1;
238
+ let subscription;
239
+ try {
240
+ subscription = relay.prepareSubscription(filters, params);
241
+ } catch (error) {
242
+ relay.ongoingOperations -= 1;
243
+ if (relay.ongoingOperations === 0) {
244
+ relay.idleSince = Date.now();
245
+ relay.scheduleIdleClose();
246
+ }
247
+ throw error;
248
+ }
249
+ const frame = JSON.stringify([
250
+ "REQ",
251
+ subscription.id,
252
+ ...requestFilters
253
+ ]);
254
+ relay.send(frame).catch((error) => {
255
+ if (subscription.closed || relay.openSubs.get(subscription.id) !== subscription) return;
256
+ const message = error instanceof Error ? error.message : String(error);
257
+ subscription.close(`Buzz relay subscription request failed: ${message}`);
258
+ });
259
+ return subscription;
260
+ }
261
+ async function queryBuzzRelaySnapshot(params) {
262
+ params.signal?.throwIfAborted();
263
+ const queries = snapshotQueries.get(params.relay) ?? {
264
+ active: 0,
265
+ waiting: []
266
+ };
267
+ snapshotQueries.set(params.relay, queries);
268
+ if (queries.active >= 3) await new Promise((resolve, reject) => {
269
+ const grant = () => {
270
+ params.signal?.removeEventListener("abort", onAbort);
271
+ resolve();
272
+ };
273
+ const onAbort = () => {
274
+ const index = queries.waiting.indexOf(grant);
275
+ if (index < 0) return;
276
+ queries.waiting.splice(index, 1);
277
+ params.signal?.removeEventListener("abort", onAbort);
278
+ const reason = params.signal?.reason ?? new Error(params.abortMessage);
279
+ reject(reason instanceof Error ? reason : new Error(params.failureMessage, { cause: reason }));
280
+ };
281
+ queries.waiting.push(grant);
282
+ params.signal?.addEventListener("abort", onAbort, { once: true });
283
+ if (params.signal?.aborted) onAbort();
284
+ });
285
+ else queries.active += 1;
286
+ try {
287
+ params.signal?.throwIfAborted();
288
+ return await queryBuzzRelaySnapshotNow(params);
289
+ } finally {
290
+ const next = queries.waiting.shift();
291
+ if (next) next();
292
+ else {
293
+ queries.active -= 1;
294
+ if (queries.active === 0) snapshotQueries.delete(params.relay);
295
+ }
296
+ }
297
+ }
298
+ async function queryBuzzRelaySnapshotNow(params) {
299
+ return await new Promise((resolve, reject) => {
300
+ let settled = false;
301
+ let receivedEose = false;
302
+ let subscriptionClosed = false;
303
+ let subscription;
304
+ const timeout = setTimeout(() => {
305
+ const error = new Error(params.timeoutMessage);
306
+ finish(error);
307
+ params.onTimeout?.(error);
308
+ if (params.closeRelayOnTimeout !== false) params.relay.close();
309
+ }, params.timeoutMs ?? 1e4);
310
+ const closeAfterRealEose = () => {
311
+ if (receivedEose && subscription && !subscriptionClosed) {
312
+ subscriptionClosed = true;
313
+ subscription.close(params.closeReason);
314
+ }
315
+ };
316
+ const finish = (error) => {
317
+ if (settled) return;
318
+ settled = true;
319
+ clearTimeout(timeout);
320
+ params.signal?.removeEventListener("abort", onAbort);
321
+ closeAfterRealEose();
322
+ if (error === void 0) resolve(params.result());
323
+ else reject(error instanceof Error ? error : new Error(params.failureMessage, { cause: error }));
324
+ };
325
+ const onAbort = () => finish(params.signal?.reason ?? new Error(params.abortMessage));
326
+ params.signal?.addEventListener("abort", onAbort, { once: true });
327
+ try {
328
+ subscription = openBuzzRelaySubscription(params.relay, params.filters, {
329
+ onevent: params.onEvent,
330
+ oneose: () => {
331
+ receivedEose = true;
332
+ if (settled) closeAfterRealEose();
333
+ else finish();
334
+ },
335
+ onclose: (reason) => {
336
+ if (reason !== params.closeReason) finish(new Error(params.closeMessage(reason)));
337
+ }
338
+ });
339
+ } catch (error) {
340
+ finish(error);
341
+ return;
342
+ }
343
+ closeAfterRealEose();
344
+ if (params.checkAbortAfterSubscribe && params.signal?.aborted) onAbort();
345
+ });
346
+ }
347
+ //#endregion
348
+ //#region extensions/buzz/src/relay-auth.ts
349
+ const AUTH_CHALLENGE_TIMEOUT_MS = 2e4;
350
+ const AUTH_CHALLENGE_POLL_MS = 25;
351
+ const RELAY_SESSION_SETUP_TIMEOUT_MS = 2e4;
352
+ const HEX_PUBLIC_KEY_PATTERN$1 = /^[0-9a-f]{64}$/u;
353
+ const BUZZ_RELAY_SOFTWARE = "https://github.com/block/buzz";
354
+ const BUZZ_LOCAL_DEV_RELAY_PUBLIC_KEY = "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";
355
+ function parseBuzzAuthTag(raw) {
356
+ if (!raw.trim()) return;
357
+ const parsed = JSON.parse(raw);
358
+ if (!Array.isArray(parsed) || parsed.length !== 4 || parsed[0] !== "auth" || parsed.some((value) => typeof value !== "string")) throw new Error("Buzz authTag must be [\"auth\",\"<pubkey>\",\"<conditions>\",\"<signature>\"]");
359
+ return parsed;
360
+ }
361
+ async function waitWithSignal(promise, signal) {
362
+ signal.throwIfAborted();
363
+ return await new Promise((resolve, reject) => {
364
+ const onAbort = () => {
365
+ const reason = signal.reason;
366
+ reject(reason instanceof Error ? reason : new Error("Buzz relay authentication aborted", { cause: reason }));
367
+ };
368
+ signal.addEventListener("abort", onAbort, { once: true });
369
+ promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
370
+ });
371
+ }
372
+ function createBuzzAuthSigner(params) {
373
+ return async (template) => finalizeEvent({
374
+ ...template,
375
+ tags: params.authTag ? [...template.tags, params.authTag] : template.tags
376
+ }, params.secretKey);
377
+ }
378
+ function isLoopbackRelayUrl(relayUrl) {
379
+ const hostname = new URL(relayUrl).hostname.toLowerCase();
380
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
381
+ }
382
+ async function resolveBuzzRelayPublicKey(params) {
383
+ const infoUrl = new URL(params.relayUrl);
384
+ infoUrl.protocol = infoUrl.protocol === "wss:" ? "https:" : "http:";
385
+ const url = infoUrl.toString();
386
+ const { response, release } = await fetchWithSsrFGuard({
387
+ url,
388
+ init: { headers: { Accept: "application/nostr+json" } },
389
+ signal: params.signal,
390
+ policy: ssrfPolicyFromHttpBaseUrlAllowedOrigin(url),
391
+ auditContext: "buzz.relay_info"
392
+ });
393
+ try {
394
+ if (!response.ok) {
395
+ await response.body?.cancel().catch(() => void 0);
396
+ throw new Error(`Buzz relay information request failed with HTTP ${response.status}`);
397
+ }
398
+ const document = await readProviderJsonObjectResponse(response, "Buzz relay information");
399
+ const relayPublicKey = typeof document.self === "string" ? document.self.trim().toLowerCase() : "";
400
+ if (HEX_PUBLIC_KEY_PATTERN$1.test(relayPublicKey)) return relayPublicKey;
401
+ if (document.software === BUZZ_RELAY_SOFTWARE && isLoopbackRelayUrl(params.relayUrl)) return BUZZ_LOCAL_DEV_RELAY_PUBLIC_KEY;
402
+ throw new Error("Buzz relay information document is missing a valid NIP-11 self public key");
403
+ } finally {
404
+ await release();
405
+ }
406
+ }
407
+ async function connectAndAuthenticateBuzzRelay(params) {
408
+ const signAuth = createBuzzAuthSigner({
409
+ secretKey: params.secretKey,
410
+ authTag: params.authTag
411
+ });
412
+ await params.relay.connect({ abort: params.signal });
413
+ await authenticateBuzzRelay({
414
+ relay: params.relay,
415
+ signAuth,
416
+ signal: params.signal
417
+ });
418
+ params.relay.onauth = signAuth;
419
+ }
420
+ async function connectAuthenticatedBuzzRelay(params) {
421
+ const relay = new Relay(params.relayUrl, { enableReconnect: false });
422
+ try {
423
+ await connectAndAuthenticateBuzzRelay({
424
+ ...params,
425
+ relay
426
+ });
427
+ return relay;
428
+ } catch (error) {
429
+ relay.close();
430
+ throw error;
431
+ }
432
+ }
433
+ async function connectAuthenticatedBuzzRelaySession(params) {
434
+ const relay = new Relay(params.relayUrl, { enableReconnect: false });
435
+ const setupAbort = new AbortController();
436
+ const signal = params.signal ? AbortSignal.any([params.signal, setupAbort.signal]) : setupAbort.signal;
437
+ let setupTimedOut = false;
438
+ const setupTimeout = setTimeout(() => {
439
+ setupTimedOut = true;
440
+ setupAbort.abort(/* @__PURE__ */ new Error("Timed out setting up Buzz relay session"));
441
+ }, RELAY_SESSION_SETUP_TIMEOUT_MS);
442
+ const authPromise = connectAndAuthenticateBuzzRelay({
443
+ ...params,
444
+ relay,
445
+ signal
446
+ });
447
+ const relayIdentityPromise = resolveBuzzRelayPublicKey({
448
+ relayUrl: params.relayUrl,
449
+ signal
450
+ });
451
+ try {
452
+ const [, relayPublicKey] = await Promise.all([authPromise, relayIdentityPromise]);
453
+ return {
454
+ relay,
455
+ relayPublicKey
456
+ };
457
+ } catch (error) {
458
+ setupAbort.abort(error);
459
+ relay.close();
460
+ await Promise.allSettled([authPromise, relayIdentityPromise]);
461
+ if (setupTimedOut && !params.signal?.aborted) throw new Error("Timed out setting up Buzz relay session", { cause: error });
462
+ throw error;
463
+ } finally {
464
+ clearTimeout(setupTimeout);
465
+ }
466
+ }
467
+ async function authenticateBuzzRelay(params) {
468
+ const challengeTimeout = AbortSignal.timeout(AUTH_CHALLENGE_TIMEOUT_MS);
469
+ const signal = params.signal ? AbortSignal.any([params.signal, challengeTimeout]) : challengeTimeout;
470
+ try {
471
+ while (true) {
472
+ signal.throwIfAborted();
473
+ try {
474
+ await waitWithSignal(params.relay.auth(params.signAuth), signal);
475
+ return;
476
+ } catch (error) {
477
+ if (!(error instanceof Error && error.message === "can't perform auth, no challenge was received")) throw error;
478
+ await waitWithSignal(new Promise((resolve) => {
479
+ setTimeout(resolve, AUTH_CHALLENGE_POLL_MS);
480
+ }), signal);
481
+ }
482
+ }
483
+ } catch (error) {
484
+ if (challengeTimeout.aborted && !params.signal?.aborted) throw new Error("Timed out waiting for Buzz NIP-42 authentication challenge", { cause: error });
485
+ throw error;
486
+ }
487
+ }
488
+ //#endregion
489
+ //#region extensions/buzz/src/room-membership.ts
490
+ const BUZZ_ROOM_MEMBERSHIP_KIND = 39002;
491
+ const BUZZ_ROOM_SYSTEM_KIND = 40099;
492
+ const HEX_PUBLIC_KEY_PATTERN = /^[0-9a-f]{64}$/u;
493
+ const MEMBERSHIP_CHANGE_TYPES = /* @__PURE__ */ new Set([
494
+ "member_joined",
495
+ "member_left",
496
+ "member_removed"
497
+ ]);
498
+ function parseBuzzRoomMembershipEvent(event, relayPublicKey) {
499
+ if (event.kind !== 39002 || event.pubkey.toLowerCase() !== relayPublicKey) return;
500
+ const roomId = event.tags.find((tag) => tag[0] === "d")?.[1]?.trim().toLowerCase();
501
+ if (!roomId || !BUZZ_CHANNEL_ID_PATTERN.test(roomId)) return;
502
+ const members = /* @__PURE__ */ new Set();
503
+ const roles = /* @__PURE__ */ new Map();
504
+ for (const tag of event.tags) {
505
+ if (tag[0] !== "p") continue;
506
+ const publicKey = tag[1]?.trim().toLowerCase();
507
+ if (!publicKey || !HEX_PUBLIC_KEY_PATTERN.test(publicKey)) continue;
508
+ members.add(publicKey);
509
+ const role = tag[3]?.trim().toLowerCase();
510
+ if (role) roles.set(publicKey, role);
511
+ }
512
+ return {
513
+ roomId,
514
+ createdAt: event.created_at,
515
+ eventId: event.id,
516
+ publisherPublicKey: event.pubkey.toLowerCase(),
517
+ members,
518
+ roles
519
+ };
520
+ }
521
+ function parseBuzzRoomMembershipChangeEvent(event, membership) {
522
+ if (event.kind !== 40099 || event.pubkey.toLowerCase() !== membership.publisherPublicKey || !event.tags.some((tag) => tag[0] === "h" && tag[1]?.toLowerCase() === membership.roomId)) return;
523
+ try {
524
+ const content = JSON.parse(event.content);
525
+ if (typeof content.type !== "string" || !MEMBERSHIP_CHANGE_TYPES.has(content.type)) return;
526
+ const target = typeof content.target === "string" ? content.target.trim().toLowerCase() : content.type === "member_left" && typeof content.actor === "string" ? content.actor.trim().toLowerCase() : "";
527
+ if (!HEX_PUBLIC_KEY_PATTERN.test(target)) return;
528
+ return {
529
+ type: content.type,
530
+ targetPublicKey: target
531
+ };
532
+ } catch {
533
+ return;
534
+ }
535
+ }
536
+ //#endregion
537
+ export { connectAuthenticatedBuzzRelay as a, openBuzzRelaySubscription as c, resolveBuzzSubscriptionBudget as d, BUZZ_ROOM_METADATA_KIND as f, parseBuzzRoomMembershipEvent as i, queryBuzzRelaySnapshot as l, isNewerBuzzRevision as m, BUZZ_ROOM_SYSTEM_KIND as n, connectAuthenticatedBuzzRelaySession as o, BuzzDirectoryState as p, parseBuzzRoomMembershipChangeEvent as r, parseBuzzAuthTag as s, BUZZ_ROOM_MEMBERSHIP_KIND as t, BUZZ_MAX_CONFIGURED_ROOMS as u };
@@ -1,4 +1,4 @@
1
- import { a as resolveBuzzAccountConfig } from "./types-CeupBG0P.mjs";
1
+ import { a as resolveBuzzAccountConfig } from "./types-DRUZMyTx.mjs";
2
2
  import { collectSecretInputAssignment, createChannelSecretTargetRegistryEntries, getChannelRecord, isRecord } from "openclaw/plugin-sdk/channel-secret-basic-runtime";
3
3
  //#region extensions/buzz/src/secret-contract.ts
4
4
  const fields = ["privateKey", "authTag"];
@@ -1,3 +1,3 @@
1
- import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries, t as channelSecrets } from "./secret-contract-DjibkTDL.mjs";
1
+ import { n as collectRuntimeConfigAssignments, r as secretTargetRegistryEntries, t as channelSecrets } from "./secret-contract-B2-Mrm4v.mjs";
2
2
  import "../secret-contract-api.js";
3
3
  export { channelSecrets, collectRuntimeConfigAssignments, secretTargetRegistryEntries };
@@ -0,0 +1,2 @@
1
+ import { t as buzzSetupPlugin } from "./setup-plugin-api-m1pk2Cow.mjs";
2
+ export { buzzSetupPlugin };
@@ -1,5 +1,5 @@
1
- import { c as BuzzConfigSchema, i as resolveBuzzAccount, r as listBuzzAccountIds, s as resolveDefaultBuzzAccountId } from "./types-CeupBG0P.mjs";
2
- import { n as buzzSetupContract, t as buzzSetupWizard } from "./setup-surface-DOHxdPor.mjs";
1
+ import { c as BuzzConfigSchema, i as resolveBuzzAccount, r as listBuzzAccountIds, s as resolveDefaultBuzzAccountId } from "./types-DRUZMyTx.mjs";
2
+ import { n as buzzSetupContract, t as buzzSetupWizard } from "./setup-surface-CVXY6_50.mjs";
3
3
  import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
4
4
  //#region extensions/buzz/src/channel.setup.ts
5
5
  const buzzSetupPlugin = {
@@ -1,5 +1,5 @@
1
- import { a as resolveBuzzAccountConfig, i as resolveBuzzAccount, l as BUZZ_CHANNEL_ID_PATTERN, m as parseBuzzTarget, n as decodeBuzzPrivateKey, o as resolveBuzzPublicKey, r as listBuzzAccountIds } from "./types-CeupBG0P.mjs";
2
- import { c as openBuzzRelaySubscription, i as parseBuzzRoomMembershipEvent, l as queryBuzzRelaySnapshot, o as connectAuthenticatedBuzzRelaySession, s as parseBuzzAuthTag, t as BUZZ_ROOM_MEMBERSHIP_KIND, u as isNewerBuzzRevision } from "./room-membership-tqLQznFE.mjs";
1
+ import { a as resolveBuzzAccountConfig, i as resolveBuzzAccount, l as BUZZ_CHANNEL_ID_PATTERN, m as parseBuzzTarget, n as decodeBuzzPrivateKey, o as resolveBuzzPublicKey, r as listBuzzAccountIds } from "./types-DRUZMyTx.mjs";
2
+ import { c as openBuzzRelaySubscription, i as parseBuzzRoomMembershipEvent, l as queryBuzzRelaySnapshot, m as isNewerBuzzRevision, o as connectAuthenticatedBuzzRelaySession, s as parseBuzzAuthTag, t as BUZZ_ROOM_MEMBERSHIP_KIND } from "./room-membership-7of91SyC.mjs";
3
3
  import { generateSecretKey, nip19 } from "nostr-tools";
4
4
  import { sleep } from "openclaw/plugin-sdk/runtime-env";
5
5
  import { defineChannelSetupContract } from "openclaw/plugin-sdk/channel-setup";
@@ -393,9 +393,7 @@ function hasSuccessfulBuzzProbe(payload, accountId, target) {
393
393
  }
394
394
  function isGatewayNotRunningError(error) {
395
395
  const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
396
- const identifiesMissingListener = message.includes("econnrefused") || message.includes("connection refused") || message.includes("no listener");
397
- if (identifiesMissingListener && typeof error === "object" && error !== null && "name" in error && "kind" in error && "code" in error && error.name === "GatewayTransportError" && error.kind === "closed" && error.code === 1006) return true;
398
- return identifiesMissingListener;
396
+ return message.includes("econnrefused") || message.includes("connection refused") || message.includes("no listener");
399
397
  }
400
398
  async function verifyBuzzAfterSetup(params) {
401
399
  try {
@@ -466,35 +464,23 @@ function isRemoteInsecureRelayUrl(value) {
466
464
  const isLoopback = hostname === "localhost" || hostname.endsWith(".localhost") || hostname === "::1" || isIpv4Loopback;
467
465
  return url.protocol === "ws:" && !isLoopback;
468
466
  }
469
- async function promptRelayUrl(params) {
467
+ async function resolveRelayUrl(params) {
468
+ const configuredValue = params.configuredValue?.trim();
469
+ let relayUrl = configuredValue && validateRelayUrl(configuredValue) === void 0 ? configuredValue : void 0;
470
470
  while (true) {
471
- const relayUrl = (await params.prompter.text({
471
+ relayUrl ??= (await params.prompter.text({
472
472
  message: "Buzz relay WebSocket URL",
473
473
  placeholder: "wss://buzz.example.com",
474
- initialValue: params.initialValue,
474
+ initialValue: configuredValue || void 0,
475
475
  validate: validateRelayUrl
476
476
  })).trim();
477
- if (!isRemoteInsecureRelayUrl(relayUrl)) return relayUrl;
478
- if (await params.prompter.confirm({
477
+ if (!isRemoteInsecureRelayUrl(relayUrl) || await params.prompter.confirm({
479
478
  message: "This remote ws:// relay is unencrypted. Continue anyway?",
480
479
  initialValue: false
481
480
  })) return relayUrl;
481
+ relayUrl = void 0;
482
482
  }
483
483
  }
484
- async function resolveRelayUrl(params) {
485
- const configuredValue = params.configuredValue?.trim();
486
- if (configuredValue && validateRelayUrl(configuredValue) === void 0) {
487
- if (!isRemoteInsecureRelayUrl(configuredValue)) return configuredValue;
488
- if (await params.prompter.confirm({
489
- message: "This remote ws:// relay is unencrypted. Continue anyway?",
490
- initialValue: false
491
- })) return configuredValue;
492
- }
493
- return await promptRelayUrl({
494
- ...configuredValue ? { initialValue: configuredValue } : {},
495
- prompter: params.prompter
496
- });
497
- }
498
484
  async function resolveSetupCredential(params) {
499
485
  const { config, configPath, allowEnv } = resolveBuzzAccountConfig(params);
500
486
  const { resolveConfiguredSecretInputWithFallback } = await import("openclaw/plugin-sdk/secret-input-runtime");
@@ -39,6 +39,7 @@ function looksLikeBuzzTarget(target) {
39
39
  const BuzzGroupConfigSchema = z.object({
40
40
  enabled: z.boolean().optional(),
41
41
  requireMention: z.boolean().optional(),
42
+ requireMentionInBotThreads: z.boolean().optional(),
42
43
  groupPolicy: GroupPolicySchema.optional(),
43
44
  groupAllowFrom: z.array(z.union([z.string(), z.number()])).optional()
44
45
  }).strict();
@@ -50,7 +51,7 @@ const BuzzAccountConfigSchema = z.object({
50
51
  responsePrefix: z.string().optional(),
51
52
  replyToMode: z.enum(["off", "all"]).optional(),
52
53
  markdown: MarkdownConfigSchema,
53
- relayUrl: z.string().url().and(z.string().regex(/^[wW][sS][sS]?:\/\//, "Buzz relay URL must use ws:// or wss://")).optional(),
54
+ relayUrl: z.string().url().and(z.string().regex(/^[wW][sS][sS]?:\/\/.+/, "Buzz relay URL must use ws:// or wss://")).optional(),
54
55
  privateKey: buildSecretInputSchema().optional(),
55
56
  authTag: buildSecretInputSchema().optional(),
56
57
  groupPolicy: GroupPolicySchema.optional(),
@@ -64,7 +65,14 @@ const RawBuzzConfigSchema = BuzzAccountConfigSchema.extend({
64
65
  accounts: z.record(BuzzAccountIdSchema, BuzzAccountConfigSchema).optional(),
65
66
  defaultAccount: BuzzAccountIdSchema.optional()
66
67
  });
67
- const BuzzConfigSchema = buildChannelConfigSchema(RawBuzzConfigSchema);
68
+ const botThreadMentionHint = {
69
+ label: "Require Mention in Bot Threads",
70
+ help: "Override mention gating in threads whose root message was signed by this bot in the same room. False allows unmentioned replies; true requires a mention. Omit to preserve the room's mention policy. Sender restrictions still apply."
71
+ };
72
+ const BuzzConfigSchema = buildChannelConfigSchema(RawBuzzConfigSchema, { uiHints: {
73
+ "groups.*.requireMentionInBotThreads": botThreadMentionHint,
74
+ "accounts.*.groups.*.requireMentionInBotThreads": botThreadMentionHint
75
+ } });
68
76
  //#endregion
69
77
  //#region extensions/buzz/src/types.ts
70
78
  function resolveChannelConfig(cfg) {
package/dist/api.js CHANGED
@@ -1,3 +1,3 @@
1
- import { i as resolveBuzzAccount, r as listBuzzAccountIds, s as resolveDefaultBuzzAccountId } from "./.setup/types-CeupBG0P.mjs";
2
- import { n as getBuzzRuntime, r as setBuzzRuntime, t as buzzPlugin } from "./.setup/channel-qV8kQiPi.mjs";
1
+ import { i as resolveBuzzAccount, r as listBuzzAccountIds, s as resolveDefaultBuzzAccountId } from "./.setup/types-DRUZMyTx.mjs";
2
+ import { n as getBuzzRuntime, r as setBuzzRuntime, t as buzzPlugin } from "./.setup/channel-B7r1381j.mjs";
3
3
  export { buzzPlugin, getBuzzRuntime, listBuzzAccountIds, resolveBuzzAccount, resolveDefaultBuzzAccountId, setBuzzRuntime };
@@ -1,2 +1,2 @@
1
- import { t as buzzPlugin } from "./.setup/channel-qV8kQiPi.mjs";
1
+ import { t as buzzPlugin } from "./.setup/channel-B7r1381j.mjs";
2
2
  export { buzzPlugin };
@@ -1,4 +1,4 @@
1
- import { n as listBuzzDirectoryPeersFromConfig, t as listBuzzDirectoryGroupsFromConfig } from "./.setup/directory-config-CzHz6fzo.mjs";
1
+ import { n as listBuzzDirectoryPeersFromConfig, t as listBuzzDirectoryGroupsFromConfig } from "./.setup/directory-config-M9KSexdh.mjs";
2
2
  //#region extensions/buzz/directory-contract-api.ts
3
3
  const buzzDirectoryContractPlugin = {
4
4
  id: "buzz",