import { currentMailboxEpoch } from '@/mesh/meshMailbox'; import { mailboxPeerRefs } from '@/mesh/meshDmConsent'; import { deriveSharedSecret, getStoredNodeDescriptor, type Contact } from '@/mesh/meshIdentity'; import { deriveWormholeDeadDropTokenPair, deriveWormholeDeadDropTokens, isWormholeReady, } from '@/mesh/wormholeIdentityClient'; function bufToHex(buf: ArrayBuffer): string { return Array.from(new Uint8Array(buf)) .map((b) => b.toString(16).padStart(2, '0')) .join(''); } export async function hmacSha256(keyBytes: ArrayBuffer, message: string): Promise { const key = await crypto.subtle.importKey('raw', keyBytes, { name: 'HMAC', hash: 'SHA-256' }, false, [ 'sign', ]); const data = new TextEncoder().encode(message); return crypto.subtle.sign('HMAC', key, data); } function contactContext(peerRef: string): string | null { const identity = getStoredNodeDescriptor(); if (!identity) return null; const ids = [identity.nodeId, peerRef].sort().join('|'); return ids; } export async function deadDropToken( peerId: string, peerDhPub: string, epoch?: number, peerRef?: string, ): Promise { const resolvedPeerRef = String(peerRef || peerId || '').trim(); if (await isWormholeReady()) { const pair = await deriveWormholeDeadDropTokenPair(peerId, peerDhPub, resolvedPeerRef).catch(() => null); if (pair?.ok) { return epoch === pair.epoch - 1 ? pair.previous : pair.current; } } const ctx = contactContext(resolvedPeerRef); if (!ctx) return ''; const bucket = typeof epoch === 'number' ? epoch : currentMailboxEpoch(); const secret = await deriveSharedSecret(peerDhPub); const digest = await hmacSha256(secret, `sb_dd|v1|${bucket}|${ctx}`); return bufToHex(digest); } export async function deadDropTokenPair( peerId: string, peerDhPub: string, peerRef?: string, ): Promise<{ current: string; previous: string; epoch: number }> { const epoch = currentMailboxEpoch(); const current = await deadDropToken(peerId, peerDhPub, epoch, peerRef); const previous = await deadDropToken(peerId, peerDhPub, epoch - 1, peerRef); return { current, previous, epoch }; } export async function deadDropTokensForContacts( contacts: Record, limit: number = 24, ): Promise { if (await isWormholeReady()) { const items = Object.entries(contacts) .filter(([_, contact]) => Boolean(contact?.dhPubKey) && !contact.blocked) .map(([peerId, contact]) => ({ peer_id: peerId, peer_dh_pub: String(contact?.dhPubKey || ''), peer_refs: mailboxPeerRefs(peerId, contact), })) .filter((item) => item.peer_refs.length > 0) .slice(0, limit); if (items.length > 0) { const batch = await deriveWormholeDeadDropTokens(items, limit).catch(() => null); if (batch?.ok && Array.isArray(batch.tokens)) { const seen = new Set(); const unique: string[] = []; for (const item of batch.tokens) { for (const token of [String(item.current || ''), String(item.previous || '')]) { if (!token || seen.has(token)) continue; seen.add(token); unique.push(token); if (unique.length >= limit) return unique; } } return unique; } } } const tokens: string[] = []; for (const [peerId, contact] of Object.entries(contacts)) { if (!contact?.dhPubKey || contact.blocked) continue; for (const candidateId of mailboxPeerRefs(peerId, contact)) { try { const pair = await deadDropTokenPair(peerId, contact.dhPubKey, candidateId); if (pair.current) tokens.push(pair.current); if (pair.previous) tokens.push(pair.previous); if (tokens.length >= limit) break; } catch { /* ignore */ } } if (tokens.length >= limit) break; } // dedupe while preserving order const seen = new Set(); const unique: string[] = []; for (const token of tokens) { if (seen.has(token)) continue; seen.add(token); unique.push(token); if (unique.length >= limit) break; } return unique; }