fix: verify Buzz relay membership identity

This commit is contained in:
Shakker
2026-07-31 09:32:30 +01:00
parent 70753c2199
commit 21f4e45a3b
14 changed files with 220 additions and 36 deletions

View File

@@ -101,6 +101,7 @@ const CHANNEL_ID = "7c4a6d2a-2ed9-4b4e-a5e2-4d705ee9b34c";
const SECOND_CHANNEL_ID = "45cedd86-f853-45b7-8fea-812b7fe63d7a";
const BOT_PUBLIC_KEY = getPublicKey(Uint8Array.from(Buffer.from(PRIVATE_KEY, "hex")));
const SENDER_PUBLIC_KEY = getPublicKey(Uint8Array.from(Buffer.from(SENDER_PRIVATE_KEY, "hex")));
const RELAY_PUBLIC_KEY = "f".repeat(64);
const tempDirs = new Set<string>();
let previousStateDir: string | undefined;
let stateDir: string;
@@ -127,7 +128,7 @@ describe("Buzz bus lifecycle", () => {
{
id: "membership-1",
kind: 39002,
pubkey: "f".repeat(64),
pubkey: RELAY_PUBLIC_KEY,
created_at: 1_700_000_000,
content: "",
sig: "e".repeat(128),
@@ -143,6 +144,16 @@ describe("Buzz bus lifecycle", () => {
relayMocks.publish.mockResolvedValue("");
relayMocks.send.mockResolvedValue();
relayMocks.connected = true;
vi.stubGlobal(
"fetch",
vi.fn(async () => ({
ok: true,
json: async () => ({
self: RELAY_PUBLIC_KEY,
software: "https://github.com/block/buzz",
}),
})),
);
});
afterEach(() => {

View File

@@ -11,7 +11,11 @@ import {
type BuzzInboundMessage,
} from "./message-event.js";
import { syncBuzzProfile } from "./profile.js";
import { connectAuthenticatedBuzzRelay, parseBuzzAuthTag } from "./relay-auth.js";
import {
connectAuthenticatedBuzzRelay,
connectAuthenticatedBuzzRelaySession,
parseBuzzAuthTag,
} from "./relay-auth.js";
import { openBuzzRelaySubscription } from "./relay-subscription.js";
import { queryBuzzRoomMemberships } from "./room-membership-query.js";
import {
@@ -176,6 +180,7 @@ async function sleepWithSignal(delayMs: number, signal?: AbortSignal): Promise<v
async function createBuzzRoomMembershipTracker(params: {
relay: Relay;
relayPublicKey: string;
channelIds: string[];
botPublicKey: string;
since: number;
@@ -240,6 +245,7 @@ async function createBuzzRoomMembershipTracker(params: {
(
await queryBuzzRoomMemberships({
relay: params.relay,
relayPublicKey: params.relayPublicKey,
channelIds: [channelId],
signal: params.signal,
})
@@ -546,7 +552,7 @@ export async function startBuzzBus(options: {
buildReplayKey: (event) => event.id,
namespace: () => options.accountId,
});
const relay = await connectAuthenticatedBuzzRelay({
const { relay, relayPublicKey } = await connectAuthenticatedBuzzRelaySession({
relayUrl: options.relayUrl,
secretKey,
authTag,
@@ -601,6 +607,7 @@ export async function startBuzzBus(options: {
try {
const membershipTracker = await createBuzzRoomMembershipTracker({
relay,
relayPublicKey,
channelIds: options.channelIds,
botPublicKey: publicKey,
since: sessionStartedAt,
@@ -646,6 +653,7 @@ export async function startBuzzBus(options: {
directory.replaceMemberships(membershipTracker.memberships());
directoryRelay = startBuzzDirectoryRelay({
relay,
relayPublicKey,
state: directory,
signal,
onError: options.onDirectoryError,

View File

@@ -14,6 +14,7 @@ type SubscriptionRecord = {
};
const BOT_PUBLIC_KEY = "a".repeat(64);
const RELAY_PUBLIC_KEY = "f".repeat(64);
const FIRST_MEMBER_PUBLIC_KEY = "b".repeat(64);
const SECOND_MEMBER_PUBLIC_KEY = "c".repeat(64);
const LATEST_MEMBER_PUBLIC_KEY = "d".repeat(64);
@@ -41,6 +42,7 @@ describe("Buzz directory relay", () => {
} as unknown as Relay;
const directory = startBuzzDirectoryRelay({
relay,
relayPublicKey: RELAY_PUBLIC_KEY,
state: new BuzzDirectoryState({
publicKey: BOT_PUBLIC_KEY,
fallbackProfileName: "OpenClaw",
@@ -90,6 +92,7 @@ describe("Buzz directory relay", () => {
} as unknown as Relay;
const directory = startBuzzDirectoryRelay({
relay,
relayPublicKey: RELAY_PUBLIC_KEY,
state: new BuzzDirectoryState({
publicKey: BOT_PUBLIC_KEY,
fallbackProfileName: "OpenClaw",
@@ -124,6 +127,7 @@ describe("Buzz directory relay", () => {
} as unknown as Relay;
const query = queryBuzzDirectoryRooms({
relay,
relayPublicKey: RELAY_PUBLIC_KEY,
state: new BuzzDirectoryState({
publicKey: BOT_PUBLIC_KEY,
fallbackProfileName: "OpenClaw",

View File

@@ -109,6 +109,7 @@ export async function queryBuzzDirectoryProfiles(params: {
export async function queryBuzzDirectoryRooms(params: {
relay: Relay;
relayPublicKey: string;
state: BuzzDirectoryState;
channelIds: string[];
signal?: AbortSignal;
@@ -118,11 +119,14 @@ export async function queryBuzzDirectoryRooms(params: {
relay: params.relay,
filter: {
kinds: [BUZZ_ROOM_METADATA_KIND],
authors: [params.relayPublicKey],
"#d": roomIds,
limit: roomIds.length,
},
onEvent: (event) => {
params.state.applyRoomEvent(event);
if (event.pubkey.toLowerCase() === params.relayPublicKey) {
params.state.applyRoomEvent(event);
}
},
signal: params.signal,
});
@@ -131,6 +135,7 @@ export async function queryBuzzDirectoryRooms(params: {
export function startBuzzDirectoryRelay(params: {
relay: Relay;
relayPublicKey: string;
state: BuzzDirectoryState;
signal?: AbortSignal;
onError?: (error: Error) => void;
@@ -273,6 +278,7 @@ export function startBuzzDirectoryRelay(params: {
pendingRoomIds.clear();
await queryBuzzDirectoryRooms({
relay: params.relay,
relayPublicKey: params.relayPublicKey,
state: params.state,
channelIds: nextRoomIds,
signal: params.signal,

View File

@@ -64,6 +64,7 @@ vi.mock("./gateway.js", () => ({
const PRIVATE_KEY = "11".repeat(32);
const BOT_PUBLIC_KEY = getPublicKey(Uint8Array.from(Buffer.from(PRIVATE_KEY, "hex")));
const MEMBER_PUBLIC_KEY = "b".repeat(64);
const RELAY_PUBLIC_KEY = "c".repeat(64);
const ROOM_ID = "7c4a6d2a-2ed9-4b4e-a5e2-4d705ee9b34c";
function event(params: Partial<Event> & Pick<Event, "kind" | "pubkey">): Event {
@@ -83,6 +84,16 @@ describe("Buzz live directory", () => {
vi.clearAllMocks();
relayMocks.filters.length = 0;
gatewayMocks.activeBus = undefined;
vi.stubGlobal(
"fetch",
vi.fn(async () => ({
ok: true,
json: async () => ({
self: RELAY_PUBLIC_KEY,
software: "https://github.com/block/buzz",
}),
})),
);
relayMocks.subscribe.mockImplementation(
(
filter: Filter,
@@ -95,7 +106,7 @@ describe("Buzz live directory", () => {
handlers.onevent(
event({
kind: 39_002,
pubkey: "c".repeat(64),
pubkey: RELAY_PUBLIC_KEY,
tags: [
["d", ROOM_ID],
["p", BOT_PUBLIC_KEY, "", "bot"],
@@ -107,7 +118,7 @@ describe("Buzz live directory", () => {
handlers.onevent(
event({
kind: 39_000,
pubkey: "c".repeat(64),
pubkey: RELAY_PUBLIC_KEY,
tags: [
["d", ROOM_ID],
["name", "Engineering"],
@@ -161,8 +172,18 @@ describe("Buzz live directory", () => {
]);
expect(relayMocks.filters).toEqual([
{ kinds: [39_002], "#d": [ROOM_ID], limit: 1 },
{ kinds: [39_000], "#d": [ROOM_ID], limit: 1 },
{
kinds: [39_002],
authors: [RELAY_PUBLIC_KEY],
"#d": [ROOM_ID],
limit: 1,
},
{
kinds: [39_000],
authors: [RELAY_PUBLIC_KEY],
"#d": [ROOM_ID],
limit: 1,
},
{ kinds: [0], authors: [BOT_PUBLIC_KEY, MEMBER_PUBLIC_KEY], limit: 2 },
]);
expect(relayMocks.auth).toHaveBeenCalledOnce();

View File

@@ -5,7 +5,7 @@ import type {
import { queryBuzzDirectoryProfiles, queryBuzzDirectoryRooms } from "./directory-relay.js";
import { BuzzDirectoryState } from "./directory-state.js";
import { getActiveBuzzBus } from "./gateway.js";
import { connectAuthenticatedBuzzRelay, parseBuzzAuthTag } from "./relay-auth.js";
import { connectAuthenticatedBuzzRelaySession, parseBuzzAuthTag } from "./relay-auth.js";
import { queryBuzzRoomMemberships } from "./room-membership-query.js";
import { parseBuzzTarget } from "./target.js";
import { decodeBuzzPrivateKey, resolveBuzzAccount } from "./types.js";
@@ -56,7 +56,7 @@ async function loadBuzzDirectoryState(
}
const timeoutSignal = AbortSignal.timeout(DIRECTORY_LIVE_TIMEOUT_MS);
const relay = await connectAuthenticatedBuzzRelay({
const { relay, relayPublicKey } = await connectAuthenticatedBuzzRelaySession({
relayUrl: configured.account.relayUrl,
secretKey: decodeBuzzPrivateKey(configured.account.privateKey),
authTag: parseBuzzAuthTag(configured.account.authTag),
@@ -66,6 +66,7 @@ async function loadBuzzDirectoryState(
configured.state.replaceMemberships(
await queryBuzzRoomMemberships({
relay,
relayPublicKey,
channelIds: configured.channelIds,
signal: timeoutSignal,
}),
@@ -73,6 +74,7 @@ async function loadBuzzDirectoryState(
await Promise.all([
queryBuzzDirectoryRooms({
relay,
relayPublicKey,
state: configured.state,
channelIds: configured.channelIds,
signal: timeoutSignal,

View File

@@ -74,6 +74,7 @@ vi.mock("nostr-tools", async (importOriginal) => {
import { createBuzzQaRelayDriver } from "./relay-client.js";
const RELAY_PUBLIC_KEY = "f".repeat(64);
const credentials = parseBuzzQaCredentialPayload({
relayUrl: "wss://relay.qa.example",
roomId: "123e4567-e89b-42d3-a456-426614174000",
@@ -86,6 +87,16 @@ describe("Buzz QA relay driver", () => {
vi.clearAllMocks();
relayMocks.subscriptions.length = 0;
relayMocks.replayedMessage = undefined;
vi.stubGlobal(
"fetch",
vi.fn(async () => ({
ok: true,
json: async () => ({
self: RELAY_PUBLIC_KEY,
software: "https://github.com/block/buzz",
}),
})),
);
});
it("authenticates, verifies membership, and publishes a native mentioned thread event", async () => {

View File

@@ -4,7 +4,7 @@ import {
parseBuzzMessageEvent,
type BuzzInboundMessage,
} from "../message-event.js";
import { connectAuthenticatedBuzzRelay, parseBuzzAuthTag } from "../relay-auth.js";
import { connectAuthenticatedBuzzRelaySession, parseBuzzAuthTag } from "../relay-auth.js";
import { openBuzzRelaySubscription } from "../relay-subscription.js";
import {
BUZZ_ROOM_MEMBERSHIP_KIND,
@@ -32,6 +32,7 @@ type BuzzQaRelayDriver = {
async function loadBuzzQaRoomMembership(params: {
relay: Relay;
relayPublicKey: string;
roomId: string;
}): Promise<BuzzRoomMembership> {
return await new Promise<BuzzRoomMembership>((resolve, reject) => {
@@ -59,10 +60,17 @@ async function loadBuzzQaRoomMembership(params: {
);
subscriptionRef.current = openBuzzRelaySubscription(
params.relay,
[{ kinds: [BUZZ_ROOM_MEMBERSHIP_KIND], "#d": [params.roomId], limit: 1 }],
[
{
kinds: [BUZZ_ROOM_MEMBERSHIP_KIND],
authors: [params.relayPublicKey],
"#d": [params.roomId],
limit: 1,
},
],
{
onevent: (event) => {
const membership = parseBuzzRoomMembershipEvent(event);
const membership = parseBuzzRoomMembershipEvent(event, params.relayPublicKey);
if (
membership?.roomId === params.roomId &&
isNewerBuzzRoomMembership(membership, latest)
@@ -110,7 +118,7 @@ export async function createBuzzQaRelayDriver(params: {
let transportError: Error | undefined;
let messageQueue = Promise.resolve();
const observedEventIds = new Set<string>();
const relay = await connectAuthenticatedBuzzRelay({
const { relay, relayPublicKey } = await connectAuthenticatedBuzzRelaySession({
relayUrl: credentials.relayUrl,
secretKey,
authTag: parseBuzzAuthTag(credentials.driverAuthTag ?? ""),
@@ -118,7 +126,11 @@ export async function createBuzzQaRelayDriver(params: {
});
try {
assertBuzzQaMembership(
await loadBuzzQaRoomMembership({ relay, roomId: credentials.roomId }),
await loadBuzzQaRoomMembership({
relay,
relayPublicKey,
roomId: credentials.roomId,
}),
credentials,
);
} catch (error) {

View File

@@ -2,6 +2,17 @@ import { type EventTemplate, finalizeEvent, Relay, type VerifiedEvent } from "no
const AUTH_CHALLENGE_TIMEOUT_MS = 20_000;
const AUTH_CHALLENGE_POLL_MS = 25;
const HEX_PUBLIC_KEY_PATTERN = /^[0-9a-f]{64}$/u;
const BUZZ_RELAY_SOFTWARE = "https://github.com/block/buzz";
// Buzz `just dev` uses private key 1 when auth tokens are disabled, but omits
// NIP-11 `self` because no production relay key was configured.
const BUZZ_LOCAL_DEV_RELAY_PUBLIC_KEY =
"79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798";
export type AuthenticatedBuzzRelaySession = {
relay: Relay;
relayPublicKey: string;
};
export function parseBuzzAuthTag(raw: string): string[] | undefined {
if (!raw.trim()) {
@@ -49,6 +60,54 @@ function createBuzzAuthSigner(params: {
);
}
function isLoopbackRelayUrl(relayUrl: string): boolean {
const hostname = new URL(relayUrl).hostname.toLowerCase();
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
}
async function resolveBuzzRelayPublicKey(params: {
relayUrl: string;
signal?: AbortSignal;
}): Promise<string> {
const infoUrl = new URL(params.relayUrl);
infoUrl.protocol = infoUrl.protocol === "wss:" ? "https:" : "http:";
const response = await fetch(infoUrl, {
headers: { Accept: "application/nostr+json" },
signal: params.signal,
});
if (!response.ok) {
throw new Error(`Buzz relay information request failed with HTTP ${response.status}`);
}
const document = (await response.json()) as {
self?: unknown;
software?: unknown;
};
const relayPublicKey =
typeof document.self === "string" ? document.self.trim().toLowerCase() : "";
if (HEX_PUBLIC_KEY_PATTERN.test(relayPublicKey)) {
return relayPublicKey;
}
if (document.software === BUZZ_RELAY_SOFTWARE && isLoopbackRelayUrl(params.relayUrl)) {
return BUZZ_LOCAL_DEV_RELAY_PUBLIC_KEY;
}
throw new Error("Buzz relay information document is missing a valid NIP-11 self public key");
}
async function connectAndAuthenticateBuzzRelay(params: {
relay: Relay;
secretKey: Uint8Array;
authTag?: string[];
signal?: AbortSignal;
}): Promise<void> {
const signAuth = createBuzzAuthSigner({
secretKey: params.secretKey,
authTag: params.authTag,
});
await params.relay.connect({ abort: params.signal });
await authenticateBuzzRelay({ relay: params.relay, signAuth, signal: params.signal });
params.relay.onauth = signAuth;
}
export async function connectAuthenticatedBuzzRelay(params: {
relayUrl: string;
secretKey: Uint8Array;
@@ -56,14 +115,8 @@ export async function connectAuthenticatedBuzzRelay(params: {
signal?: AbortSignal;
}): Promise<Relay> {
const relay = new Relay(params.relayUrl, { enableReconnect: false });
const signAuth = createBuzzAuthSigner({
secretKey: params.secretKey,
authTag: params.authTag,
});
try {
await relay.connect({ abort: params.signal });
await authenticateBuzzRelay({ relay, signAuth, signal: params.signal });
relay.onauth = signAuth;
await connectAndAuthenticateBuzzRelay({ ...params, relay });
return relay;
} catch (error) {
relay.close();
@@ -71,6 +124,25 @@ export async function connectAuthenticatedBuzzRelay(params: {
}
}
export async function connectAuthenticatedBuzzRelaySession(params: {
relayUrl: string;
secretKey: Uint8Array;
authTag?: string[];
signal?: AbortSignal;
}): Promise<AuthenticatedBuzzRelaySession> {
const relay = new Relay(params.relayUrl, { enableReconnect: false });
try {
const [, relayPublicKey] = await Promise.all([
connectAndAuthenticateBuzzRelay({ ...params, relay }),
resolveBuzzRelayPublicKey(params),
]);
return { relay, relayPublicKey };
} catch (error) {
relay.close();
throw error;
}
}
async function authenticateBuzzRelay(params: {
relay: Relay;
signAuth: (template: EventTemplate) => Promise<VerifiedEvent>;

View File

@@ -1,5 +1,5 @@
import type { Event, Relay } from "nostr-tools";
import { connectAuthenticatedBuzzRelay, parseBuzzAuthTag } from "./relay-auth.js";
import { connectAuthenticatedBuzzRelaySession, parseBuzzAuthTag } from "./relay-auth.js";
import { openBuzzRelaySubscription } from "./relay-subscription.js";
import { discoverBuzzRoomsOnRelay, type BuzzDiscoveredRoom } from "./room-discovery.js";
import { BUZZ_CHANNEL_ID_PATTERN } from "./target.js";
@@ -61,7 +61,7 @@ export async function waitForBuzzRoomAccess(params: {
const publicKey = resolveBuzzPublicKey(params.privateKey);
const timeoutSignal = AbortSignal.timeout(params.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS);
const signal = params.signal ? AbortSignal.any([params.signal, timeoutSignal]) : timeoutSignal;
const relay = await connectAuthenticatedBuzzRelay({
const { relay, relayPublicKey } = await connectAuthenticatedBuzzRelaySession({
relayUrl: params.relayUrl,
secretKey,
authTag: parseBuzzAuthTag(params.authTag ?? ""),
@@ -117,6 +117,7 @@ export async function waitForBuzzRoomAccess(params: {
try {
const rooms = await discoverBuzzRoomsOnRelay({
relay,
relayPublicKey,
publicKey,
timeoutMs: 10_000,
signal,

View File

@@ -47,6 +47,7 @@ vi.mock("nostr-tools", async (importOriginal) => {
});
const PRIVATE_KEY = "11".repeat(32);
const RELAY_PUBLIC_KEY = "f".repeat(64);
const ROOM_A = "7c4a6d2a-2ed9-4b4e-a5e2-4d705ee9b34c";
const ROOM_B = "940d0c32-4eb7-46d7-9d5b-d975aaef87f7";
const AUTH_TAG = ["auth", "bot", "kind=9", "signature"];
@@ -58,6 +59,16 @@ describe("discoverBuzzRooms", () => {
relayMocks.connect.mockClear();
relayMocks.filters.length = 0;
relayMocks.subscribe.mockReset();
vi.stubGlobal(
"fetch",
vi.fn(async () => ({
ok: true,
json: async () => ({
self: RELAY_PUBLIC_KEY,
software: "https://github.com/block/buzz",
}),
})),
);
});
it("discovers only rooms whose member event names the bot public key", async () => {
@@ -76,7 +87,7 @@ describe("discoverBuzzRooms", () => {
handlers.onevent({
id: "member-a",
kind: 39002,
pubkey: "relay",
pubkey: RELAY_PUBLIC_KEY,
created_at: 1,
content: "",
sig: "sig",
@@ -88,7 +99,7 @@ describe("discoverBuzzRooms", () => {
handlers.onevent({
id: "member-b-wrong-role",
kind: 39002,
pubkey: "relay",
pubkey: RELAY_PUBLIC_KEY,
created_at: 1,
content: "",
sig: "sig",
@@ -109,7 +120,7 @@ describe("discoverBuzzRooms", () => {
handlers.onevent({
id: "metadata-a",
kind: 39000,
pubkey: "relay",
pubkey: RELAY_PUBLIC_KEY,
created_at: 2,
content: "",
sig: "sig",
@@ -140,8 +151,18 @@ describe("discoverBuzzRooms", () => {
]);
expect(relayMocks.filters).toEqual([
{ kinds: [39002], "#p": [publicKey], limit: 1000 },
{ kinds: [39000], "#d": [ROOM_A], limit: 1 },
{
kinds: [39002],
authors: [RELAY_PUBLIC_KEY],
"#p": [publicKey],
limit: 1000,
},
{
kinds: [39000],
authors: [RELAY_PUBLIC_KEY],
"#d": [ROOM_A],
limit: 1,
},
]);
expect(relayMocks.auth).toHaveBeenCalledOnce();
expect(signedAuthTags).toContainEqual(AUTH_TAG);

View File

@@ -1,5 +1,5 @@
import type { Event, Filter, Relay } from "nostr-tools";
import { connectAuthenticatedBuzzRelay, parseBuzzAuthTag } from "./relay-auth.js";
import { connectAuthenticatedBuzzRelaySession, parseBuzzAuthTag } from "./relay-auth.js";
import { openBuzzRelaySubscription } from "./relay-subscription.js";
import { BUZZ_ROOM_MEMBERSHIP_KIND, parseBuzzRoomMembershipEvent } from "./room-membership.js";
import { BUZZ_CHANNEL_ID_PATTERN } from "./target.js";
@@ -73,6 +73,7 @@ async function queryRelay(params: {
export async function discoverBuzzRoomsOnRelay(params: {
relay: Relay;
relayPublicKey: string;
publicKey: string;
timeoutMs?: number;
signal?: AbortSignal;
@@ -82,6 +83,7 @@ export async function discoverBuzzRoomsOnRelay(params: {
relay: params.relay,
filter: {
kinds: [BUZZ_ROOM_MEMBERSHIP_KIND],
authors: [params.relayPublicKey],
"#p": [params.publicKey],
limit: 1000,
},
@@ -91,7 +93,7 @@ export async function discoverBuzzRoomsOnRelay(params: {
const roomIds = [
...new Set(
membershipEvents
.map(parseBuzzRoomMembershipEvent)
.map((event) => parseBuzzRoomMembershipEvent(event, params.relayPublicKey))
.filter((membership) => membership?.roles.get(params.publicKey) === "bot")
.map((membership) => membership?.roomId)
.filter((roomId): roomId is string => Boolean(roomId?.match(BUZZ_CHANNEL_ID_PATTERN))),
@@ -103,7 +105,12 @@ export async function discoverBuzzRoomsOnRelay(params: {
const metadataEvents = await queryRelay({
relay: params.relay,
filter: { kinds: [METADATA_KIND], "#d": roomIds, limit: roomIds.length },
filter: {
kinds: [METADATA_KIND],
authors: [params.relayPublicKey],
"#d": roomIds,
limit: roomIds.length,
},
timeoutMs,
signal: params.signal,
});
@@ -112,6 +119,7 @@ export async function discoverBuzzRoomsOnRelay(params: {
const roomId = tagValue(event, "d")?.toLowerCase();
if (
event.kind !== METADATA_KIND ||
event.pubkey.toLowerCase() !== params.relayPublicKey ||
!roomId ||
!roomIds.includes(roomId) ||
(latestMetadata.get(roomId)?.created_at ?? -1) >= event.created_at
@@ -150,7 +158,7 @@ export async function discoverBuzzRooms(params: {
// Status callers must not wait for a fresh timeout at every relay phase.
const timeoutSignal = AbortSignal.timeout(timeoutMs);
const signal = params.signal ? AbortSignal.any([params.signal, timeoutSignal]) : timeoutSignal;
const relay = await connectAuthenticatedBuzzRelay({
const { relay, relayPublicKey } = await connectAuthenticatedBuzzRelaySession({
relayUrl: params.relayUrl,
secretKey,
authTag: parseBuzzAuthTag(params.authTag ?? ""),
@@ -162,6 +170,7 @@ export async function discoverBuzzRooms(params: {
// discovery. Require the explicit Bot role before setup or probes accept a room.
return await discoverBuzzRoomsOnRelay({
relay,
relayPublicKey,
publicKey,
timeoutMs,
signal,

View File

@@ -12,6 +12,7 @@ const MEMBERSHIP_QUERY_COMPLETE_REASON = "membership snapshot loaded";
async function queryBuzzRoomMembershipBatch(params: {
relay: Relay;
relayPublicKey: string;
channelIds: string[];
signal?: AbortSignal;
}): Promise<Map<string, BuzzRoomMembership>> {
@@ -48,13 +49,14 @@ async function queryBuzzRoomMembershipBatch(params: {
[
{
kinds: [BUZZ_ROOM_MEMBERSHIP_KIND],
authors: [params.relayPublicKey],
"#d": params.channelIds,
limit: params.channelIds.length,
},
],
{
onevent: (event) => {
const membership = parseBuzzRoomMembershipEvent(event);
const membership = parseBuzzRoomMembershipEvent(event, params.relayPublicKey);
if (
!membership ||
!configuredRooms.has(membership.roomId) ||
@@ -90,6 +92,7 @@ async function queryBuzzRoomMembershipBatch(params: {
export async function queryBuzzRoomMemberships(params: {
relay: Relay;
relayPublicKey: string;
channelIds: string[];
signal?: AbortSignal;
}): Promise<Map<string, BuzzRoomMembership>> {

View File

@@ -21,8 +21,11 @@ export type BuzzRoomMembership = {
roles: ReadonlyMap<string, string>;
};
export function parseBuzzRoomMembershipEvent(event: Event): BuzzRoomMembership | undefined {
if (event.kind !== BUZZ_ROOM_MEMBERSHIP_KIND) {
export function parseBuzzRoomMembershipEvent(
event: Event,
relayPublicKey: string,
): BuzzRoomMembership | undefined {
if (event.kind !== BUZZ_ROOM_MEMBERSHIP_KIND || event.pubkey.toLowerCase() !== relayPublicKey) {
return undefined;
}
const roomId = event.tags