import { asAHostService } from "./service-credential.ts"; import type { BuzzAdapter } from "./adapters/buzz/adapter.ts"; import { pubkeyFromSecret } from "./adapters/buzz/user-session.ts"; export class MessageChangeRefused extends Error { constructor(message: string) { super(message); this.name = "MessageChangeRefused"; } } const signingSecretPath = "/admin/api/buzz/signing-secret"; export const secretHexPattern = /^[0-9a-f]{64}$/; export async function signerForMessageChange( adapter: BuzzAdapter, messageID: string, requesterPubkeyHex: string | undefined, admindBaseURL: string | undefined, ): Promise { const target = await adapter.readMessageEvent(messageID); if (!target) { throw new MessageChangeRefused(`message ${messageID} cannot be read from this relay, so it cannot be changed`); } if (target.pubkey === adapter.botPubkey) return undefined; const pubkeyHex = (requesterPubkeyHex ?? "").trim().toLowerCase(); if (!admindBaseURL || !secretHexPattern.test(pubkeyHex)) { throw new MessageChangeRefused( `message ${messageID} was written by somebody else and this device cannot sign as the person who asked`, ); } return signingSecretFor(pubkeyHex, admindBaseURL, messageID); } export function signingKeyring(admindBaseURL: string | undefined): (author: string, id: string) => Promise { const held = new Map>(); return (authorPubkeyHex, messageID) => { const known = held.get(authorPubkeyHex); if (known) return known; const asked = signingSecretFor(authorPubkeyHex, admindBaseURL, messageID); held.set(authorPubkeyHex, asked); return asked; }; } export async function signingSecretFor( authorPubkeyHex: string, admindBaseURL: string | undefined, messageID: string, ): Promise { const pubkeyHex = authorPubkeyHex.trim().toLowerCase(); if (!admindBaseURL || !secretHexPattern.test(pubkeyHex)) { throw new MessageChangeRefused( `message ${messageID} was written by somebody else and this device cannot sign as the person who wrote it`, ); } return await heldSigningSecret( pubkeyHex, admindBaseURL, (reason) => new MessageChangeRefused(`message ${messageID} cannot be changed: ${reason}`), ); } export async function heldSigningSecret( pubkeyHex: string, admindBaseURL: string, refused: (reason: string) => Error, ): Promise { const secretHex = await askAdmindForSigningSecret(admindBaseURL, pubkeyHex, refused); if (pubkeyFromSecret(secretHex) !== pubkeyHex) { throw refused(`the signing key this device was handed is not the key ${pubkeyHex} signs with`); } return secretHex; } async function askAdmindForSigningSecret( admindBaseURL: string, pubkeyHex: string, refused: (reason: string) => Error, ): Promise { let response: Response; try { response = await fetch(`${admindBaseURL}${signingSecretPath}`, asAHostService({ method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ pubkeyHex }), })); } catch (error) { throw refused(`this device cannot reach the service that holds ${pubkeyHex}'s signing key (${String(error)})`); } if (!response.ok) { throw refused(`this device holds no signing key for ${pubkeyHex} (answered ${response.status})`); } const document = (await response.json()) as { secretHex?: unknown }; const secretHex = typeof document.secretHex === "string" ? document.secretHex.trim().toLowerCase() : ""; if (!secretHexPattern.test(secretHex)) { throw refused(`the answer for ${pubkeyHex} carried no usable signing key`); } return secretHex; } export async function isElevatedIn( adapter: BuzzAdapter, channelID: string, actorPubkeyHex: string, freshly = false, ): Promise { if (actorPubkeyHex === "" || channelID === "") return false; return (await adapter.channelElevatedPubkeys(channelID, freshly)).has(actorPubkeyHex); }