mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 19:41:34 +00:00
fix: verify Buzz relay membership identity
This commit is contained in:
@@ -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(() => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user