Ship DM connect delivery, fleet pubkey lookup, OpenClaw Infonet agent, and relay auto-wormhole.

Auto-relay connect DMs with End Contact severing, signed fleet prekey lookup,
OpenClaw private Infonet channel intents, headless relay Tor bootstrap on redeploy,
and swarm/DM live verification scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
BigBodyCobain
2026-06-12 02:15:56 -06:00
parent d48a0cdace
commit 89d6bb8fb9
52 changed files with 4211 additions and 339 deletions
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest';
import {
isLikelyDmShortAddress,
parseDmInviteImportBlob,
inviteFromParsedBlob,
} from '@/mesh/dmConnect';
describe('dmConnect', () => {
it('detects short lookup handles', () => {
expect(isLikelyDmShortAddress('5881eb8705c9abc1234567890abcd')).toBe(true);
expect(isLikelyDmShortAddress('{"type":"invite"}')).toBe(false);
});
it('parses short address without JSON', () => {
const parsed = parseDmInviteImportBlob('abcd1234ef567890abcd1234ef567890');
expect(parsed.short_address).toBe('abcd1234ef567890abcd1234ef567890');
});
it('unwraps nested invite objects', () => {
const invite = { event_type: 'dm_invite', payload: {} };
const parsed = inviteFromParsedBlob({ invite, version: 1 });
expect(parsed).toEqual(invite);
});
});
@@ -23,7 +23,12 @@ describe('fetchDmPublicKey lookup posture', () => {
it('uses invite lookup handles without enabling legacy agent-id lookup', async () => {
fetchMock.mockResolvedValueOnce({
json: async () => ({ ok: true, dh_pub_key: 'peer-dh', lookup_mode: 'invite_lookup_handle' }),
json: async () => ({
ok: true,
agent_id: '!sb_peer',
dh_pub_key: 'peer-dh',
lookup_mode: 'invite_lookup_handle',
}),
});
const mod = await import('@/mesh/meshDmClient');
@@ -39,6 +44,28 @@ describe('fetchDmPublicKey lookup posture', () => {
);
});
it('falls back to prekey-bundle when pubkey lookup lacks agent_id', async () => {
fetchMock
.mockResolvedValueOnce({
json: async () => ({ ok: true, dh_pub_key: 'peer-dh', lookup_mode: 'invite_lookup_handle' }),
})
.mockResolvedValueOnce({
json: async () => ({
ok: true,
agent_id: '!sb_peer',
lookup_mode: 'invite_lookup_handle',
bundle: { identity_dh_pub_key: 'peer-dh' },
}),
});
const mod = await import('@/mesh/meshDmClient');
const result = await mod.fetchDmPublicKey('http://localhost:8000', '', 'invite-handle-123');
expect(result?.agent_id).toBe('!sb_peer');
expect(result?.dh_pub_key).toBe('peer-dh');
expect(fetchMock).toHaveBeenCalledTimes(2);
});
it('still supports explicit legacy agent-id lookup for migration-only paths', async () => {
fetchMock.mockResolvedValueOnce({
json: async () => ({ ok: true, dh_pub_key: 'peer-dh', lookup_mode: 'legacy_agent_id' }),
@@ -682,6 +682,20 @@ export default function InfonetShell({
{/* Main Terminal Area */}
<div className="flex-1 overflow-y-auto pr-4 pb-4">
<button
type="button"
onClick={() => handleNavigate('messages')}
className="w-full mb-6 text-left border border-emerald-500/30 bg-emerald-950/10 hover:bg-emerald-950/20 px-4 py-3 transition-colors"
>
<div className="flex items-center gap-2 text-emerald-300 text-xs tracking-[0.2em] uppercase font-bold">
<Mail size={14} />
Secure Messages Quick Connect
</div>
<p className="mt-2 text-sm text-gray-400 leading-relaxed">
Message someone on the fleet in three steps: copy your short address (or ask for theirs),
paste it in Secure Messages, tap Send Request they tap Accept. No terminal commands.
</p>
</button>
<div className="flex flex-col lg:flex-row justify-between items-start gap-6 mb-8">
<TrendingPosts />
@@ -66,6 +66,7 @@ import {
purgeBrowserContactGraph,
purgeBrowserSigningMaterial,
removeContact,
severContact,
unblockContact,
unwrapSenderSealPayload,
updateContact,
@@ -74,6 +75,7 @@ import {
type Contact,
type NodeIdentity,
} from '@/mesh/meshIdentity';
import { connectDeliveryMeta, ensureDmOutboxReleased } from '@/mesh/dmConnectDelivery';
import {
getSenderRecoveryState,
recoverSenderSealWithFallback,
@@ -1516,6 +1518,7 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
API_BASE,
senderId,
existingContact?.invitePinnedPrekeyLookupHandle,
{ lookupPeerUrl: existingContact?.invitePinnedLookupPeerUrl },
);
if (senderKey?.dh_pub_key) {
const sharedKey = await deriveSharedKey(String(senderKey.dh_pub_key));
@@ -1532,6 +1535,7 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
API_BASE,
senderId,
existingContact?.invitePinnedPrekeyLookupHandle,
{ lookupPeerUrl: existingContact?.invitePinnedLookupPeerUrl },
).catch(() => null);
if (senderKey?.dh_pub_key) {
addContact(senderId, String(senderKey.dh_pub_key), undefined, senderKey.dh_algo);
@@ -2000,7 +2004,9 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
'This contact needs their full contact address once before messages can be sent. Paste it in Contacts and the app will handle the rest.',
);
}
const targetKey = await fetchDmPublicKey(API_BASE, recipient, lookupHandle);
const targetKey = await fetchDmPublicKey(API_BASE, recipient, lookupHandle, {
lookupPeerUrl: recipientContact?.invitePinnedLookupPeerUrl,
});
if (!targetKey?.dh_pub_key) {
queuePendingDeliveryMail({
senderId: activeIdentity.nodeId,
@@ -2037,15 +2043,23 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
}
const msgId = `dm_${Date.now()}_${activeIdentity.nodeId.slice(-4)}`;
const timestamp = Math.floor(Date.now() / 1000);
const sent = await sendOffLedgerConsentMessage({
apiBase: API_BASE,
identity: activeIdentity,
recipientId: recipient,
recipientDhPub: String(targetKey.dh_pub_key),
ciphertext,
msgId,
timestamp,
const connectMeta = connectDeliveryMeta({
intent: 'contact_request',
contact: recipientContact,
});
const sent = await ensureDmOutboxReleased(
await sendOffLedgerConsentMessage({
apiBase: API_BASE,
identity: activeIdentity,
recipientId: recipient,
recipientDhPub: String(targetKey.dh_pub_key),
ciphertext,
msgId,
timestamp,
connectIntent: connectMeta.connectIntent,
lookupPeerUrl: connectMeta.lookupPeerUrl,
}),
);
if (!sent.ok) {
throw new Error(sent.detail || 'contact request failed');
}
@@ -2110,7 +2124,9 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
throw new Error('Secure mail is still preparing your private identity.');
}
const { registration, myDhPub } = await ensureLocalDmKey(activeIdentity);
const targetKey = await fetchDmPublicKey(API_BASE, '', shortAddress);
const targetKey = await fetchDmPublicKey(API_BASE, '', shortAddress, {
allowLegacyAgentId: false,
});
if (!targetKey?.dh_pub_key || !targetKey.agent_id) {
throw new Error('That address is not reachable yet. Ask them to copy their address again while their device is online.');
}
@@ -2136,15 +2152,18 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
}
const msgId = `dm_${Date.now()}_${activeIdentity.nodeId.slice(-4)}`;
const timestamp = Math.floor(Date.now() / 1000);
const sent = await sendOffLedgerConsentMessage({
apiBase: API_BASE,
identity: activeIdentity,
recipientId: recipient,
recipientDhPub: String(targetKey.dh_pub_key),
ciphertext,
msgId,
timestamp,
});
const sent = await ensureDmOutboxReleased(
await sendOffLedgerConsentMessage({
apiBase: API_BASE,
identity: activeIdentity,
recipientId: recipient,
recipientDhPub: String(targetKey.dh_pub_key),
ciphertext,
msgId,
timestamp,
connectIntent: 'invite_short_address',
}),
);
if (!sent.ok) {
throw new Error(sent.detail || 'contact request failed');
}
@@ -2224,6 +2243,12 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
invitePayload.prekey_lookup_handle ||
'',
),
invitePinnedLookupPeerUrl: String(
resultContact.invitePinnedLookupPeerUrl ||
(invite as Record<string, unknown>).lookup_peer_url ||
invitePayload.lookup_peer_url ||
'',
),
dhPubKey: String(resultContact.dhPubKey || resultContact.invitePinnedDhPubKey || ''),
};
const mergedContacts = importedPeerId
@@ -2269,6 +2294,26 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
}
}, [applyHydratedContacts, handleSendShortAddressRequest, inviteImportAlias, inviteImportBlob, loadBackendContacts, syncSecureMailRuntime]);
const handleSeverContact = useCallback(
async (peerId: string) => {
const name = displayNameForPeer(peerId, contacts);
setComposeError('');
setComposeStatus('');
try {
await severContact(peerId);
setContacts(getContacts());
setComposeStatus(
`Secure contact ended with ${name}. You can message again only after a new request and approval.`,
);
} catch (error) {
setComposeError(
error instanceof Error ? error.message : 'Could not end secure contact right now.',
);
}
},
[contacts],
);
const refreshDmAddressHandles = useCallback(async () => {
try {
const result = await listWormholeDmInviteHandles();
@@ -2501,6 +2546,7 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
API_BASE,
mail.senderId,
existingContact?.invitePinnedPrekeyLookupHandle,
{ lookupPeerUrl: existingContact?.invitePinnedLookupPeerUrl },
).catch(() => null);
const dhPubKey = String(registry?.dh_pub_key || mail.requestDhPubKey || '').trim();
const dhAlgo = String(registry?.dh_algo || mail.requestDhAlgo || 'X25519').trim();
@@ -2551,15 +2597,19 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
const msgId = `dm_${Date.now()}_${activeIdentity.nodeId.slice(-4)}`;
const timestamp = Math.floor(Date.now() / 1000);
const sent = await sendOffLedgerConsentMessage({
apiBase: API_BASE,
identity: activeIdentity,
recipientId: mail.senderId,
recipientDhPub: dhPubKey,
ciphertext,
msgId,
timestamp,
});
const sent = await ensureDmOutboxReleased(
await sendOffLedgerConsentMessage({
apiBase: API_BASE,
identity: activeIdentity,
recipientId: mail.senderId,
recipientDhPub: dhPubKey,
ciphertext,
msgId,
timestamp,
connectIntent: 'contact_accept',
lookupPeerUrl: existingContact?.invitePinnedLookupPeerUrl,
}),
);
if (!sent.ok) {
throw new Error(sent.detail || 'contact accept failed');
}
@@ -2715,7 +2765,9 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
<Mail size={24} className="mr-3" />
SECURE MESSAGES
</h1>
<p className="text-gray-500 text-sm mt-1">End-to-end encrypted peer-to-peer comms.</p>
<p className="text-gray-500 text-sm mt-1">
Copy your short address and send it to someone. They paste it here and tap Send Request you tap Accept. No terminal required.
</p>
</div>
<div className="border border-cyan-900/30 bg-cyan-950/10 px-4 py-3 text-[11px] tracking-[0.16em] uppercase text-cyan-300 mb-4 shrink-0">
@@ -2755,7 +2807,7 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
</div>
</>
) : (
'Your contact address is being prepared automatically. Share it with someone so they can message you.'
'Your contact address is being prepared. Copy the short address above and send it to anyone you want to message you.'
)}
</div>
</div>
@@ -3428,7 +3480,7 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
)}
{contact.sharedAlias && (
<div className="text-[11px] text-emerald-300 mt-2">
Shared alias: {contact.sharedAlias}
Shared lane open you can exchange secure mail.
</div>
)}
</div>
@@ -3466,6 +3518,18 @@ export default function MessagesView({ onBack, onOpenDeadDrop }: MessagesViewPro
{nextStep.label}
</button>
)}
{contact.sharedAlias && (
<button
onClick={() => void handleSeverContact(peerId)}
className="px-3 py-2 border border-violet-500/30 text-violet-200 text-sm tracking-[0.18em] uppercase"
title="Close the shared lane. A fresh contact request and approval will be required to message again."
>
<span className="inline-flex items-center gap-1.5">
<ShieldOff size={14} />
End Contact
</span>
</button>
)}
<button
onClick={() => {
blockContact(peerId);
+3 -9
View File
@@ -361,15 +361,9 @@ const MeshChat = React.memo(function MeshChat(props: MeshChatProps) {
setShowSas((prev) => !prev);
};
const handleRequestComposerAction = () => {
const targetId = addContactId.trim();
if (!targetId) return;
const inviteLookupHandle = String(
contacts[targetId]?.invitePinnedPrekeyLookupHandle || '',
).trim();
if (!inviteLookupHandle) {
openTerminal();
}
void handleRequestAccess(targetId);
const pasted = addContactId.trim();
if (!pasted) return;
void handleRequestAccess(pasted);
};
const meshActivationText =
publicMeshBlockedByWormhole
@@ -27,6 +27,7 @@ import {
addContact,
updateContact,
blockContact,
severContact,
getDMNotify,
nextSequence,
verifyEventSignature,
@@ -103,6 +104,7 @@ import {
isEncryptedGateEnvelope,
} from '@/mesh/gateEnvelope';
import { fetchWormholeSettings, joinWormhole, leaveWormhole } from '@/mesh/wormholeClient';
import { connectDeliveryMeta, ensureDmOutboxReleased } from '@/mesh/dmConnectDelivery';
import {
buildMailboxClaims,
countDmMailboxes,
@@ -2295,6 +2297,7 @@ export function useMeshChatController({
API_BASE,
m.sender_id,
senderContact?.invitePinnedPrekeyLookupHandle,
{ lookupPeerUrl: senderContact?.invitePinnedLookupPeerUrl },
);
if (senderKey?.dh_pub_key) {
const sharedKey = await deriveSharedKey(String(senderKey.dh_pub_key));
@@ -2310,6 +2313,7 @@ export function useMeshChatController({
API_BASE,
m.sender_id,
senderContact?.invitePinnedPrekeyLookupHandle,
{ lookupPeerUrl: senderContact?.invitePinnedLookupPeerUrl },
).catch(() => null);
if (senderKey?.dh_pub_key) {
addContact(m.sender_id, String(senderKey.dh_pub_key), undefined, senderKey.dh_algo);
@@ -3336,7 +3340,9 @@ export function useMeshChatController({
'import or re-import a signed invite before refreshing this contact; legacy direct lookup is disabled',
);
}
const registry = await fetchDmPublicKey(API_BASE, targetId, lookupHandle).catch(() => null);
const registry = await fetchDmPublicKey(API_BASE, targetId, lookupHandle, {
lookupPeerUrl: existing?.invitePinnedLookupPeerUrl,
}).catch(() => null);
if (!registry?.dh_pub_key) {
throw new Error(
'invite-scoped lookup failed for this contact; re-import a signed invite and try again',
@@ -3585,29 +3591,26 @@ export function useMeshChatController({
setTimeout(() => setSendError(''), 3000);
return;
}
if (requiresVerifiedFirstContact(getContacts()[targetId])) {
setSendError('import a signed invite before first secure contact; TOFU requests are disabled');
setTimeout(() => setSendError(''), 4000);
return;
}
if (wormholeEnabled && !wormholeReadyState) {
setSendError('wormhole required for dead drop');
setTimeout(() => setSendError(''), 3000);
return;
}
try {
const registration = await ensureRegisteredDmKey(API_BASE, identity!, { force: false });
const myPub = registration.dhPubKey;
if (!myPub) return;
const dhAlgo = registration.dhAlgo || getDHAlgo() || 'X25519';
const targetContact = getContacts()[targetId];
const lookupHandle = String(targetContact?.invitePinnedPrekeyLookupHandle || '').trim();
let lookupHandle = String(targetContact?.invitePinnedPrekeyLookupHandle || '').trim();
let resolvedTargetId = targetId;
if (!lookupHandle && /^[a-fA-F0-9]{32,}$/.test(targetId)) {
lookupHandle = targetId;
resolvedTargetId = '';
}
if (!lookupHandle) {
throw new Error(
'import or re-import a signed invite before sending a contact request; legacy direct lookup is disabled',
'Paste their short contact address (from Secure Messages → Copy Short Address), not their node id.',
);
}
const targetKey = await fetchDmPublicKey(API_BASE, targetId, lookupHandle);
const targetKey = await fetchDmPublicKey(API_BASE, resolvedTargetId, lookupHandle, {
lookupPeerUrl: targetContact?.invitePinnedLookupPeerUrl,
});
if (!targetKey?.dh_pub_key) {
throw new Error(
'invite-scoped lookup failed for this contact; re-import a signed invite and try again',
@@ -3631,12 +3634,13 @@ export function useMeshChatController({
geoHint = '';
}
}
const recipientId = String(targetKey.agent_id || resolvedTargetId || targetId).trim();
const requestPlaintext = buildContactOfferMessage(myPub, dhAlgo, geoHint || undefined);
let ciphertext = '';
const secureRequired = await isWormholeSecureRequired();
if (await canUseWormholeBootstrap()) {
try {
ciphertext = await bootstrapEncryptAccessRequest(targetId, requestPlaintext);
ciphertext = await bootstrapEncryptAccessRequest(recipientId, requestPlaintext);
} catch {
ciphertext = '';
}
@@ -3651,16 +3655,24 @@ export function useMeshChatController({
const msgId = `dm_${Date.now()}_${identity!.nodeId.slice(-4)}`;
const msgTimestamp = Math.floor(Date.now() / 1000);
await sleep(jitterDelay(ACCESS_REQUEST_BATCH_DELAY_MS, ACCESS_REQUEST_BATCH_JITTER_MS));
const connectMeta = connectDeliveryMeta({
intent: lookupHandle === targetId ? 'invite_short_address' : 'contact_request',
contact: targetContact,
});
await enqueueDmSend(async () => {
const sent = await sendOffLedgerConsentMessage({
apiBase: API_BASE,
identity: identity!,
recipientId: targetId,
recipientDhPub: String(targetKey.dh_pub_key),
ciphertext,
msgId,
timestamp: msgTimestamp,
});
const sent = await ensureDmOutboxReleased(
await sendOffLedgerConsentMessage({
apiBase: API_BASE,
identity: identity!,
recipientId,
recipientDhPub: String(targetKey.dh_pub_key),
ciphertext,
msgId,
timestamp: msgTimestamp,
connectIntent: connectMeta.connectIntent,
lookupPeerUrl: connectMeta.lookupPeerUrl,
}),
);
if (!sent.ok) {
throw new Error(sent.detail || 'access_request_send_failed');
}
@@ -3668,7 +3680,8 @@ export function useMeshChatController({
setLastDmTransport(sent.transport);
}
});
const updated = [...pendingSent, targetId];
const recipientForPending = String(targetKey.agent_id || resolvedTargetId || targetId).trim();
const updated = [...pendingSent, recipientForPending];
setPendingSent(updated, dmConsentScopeId);
setPendingSentState(updated);
} catch (err) {
@@ -3680,11 +3693,6 @@ export function useMeshChatController({
const handleAcceptRequest = async (senderId: string) => {
if (!hasId) return;
if (requiresVerifiedFirstContact(getContacts()[senderId])) {
setSendError('import a signed invite before accepting an unverified request');
setTimeout(() => setSendError(''), 4000);
return;
}
if (anonymousDmBlocked) {
setSendError('hidden transport required for anonymous dm');
setTimeout(() => setSendError(''), 3000);
@@ -3697,6 +3705,7 @@ export function useMeshChatController({
API_BASE,
senderId,
existingContact?.invitePinnedPrekeyLookupHandle,
{ lookupPeerUrl: existingContact?.invitePinnedLookupPeerUrl },
).catch(() => null);
const resolvedDhPubKey = String(registry?.dh_pub_key || req?.dh_pub_key || '').trim();
const resolvedDhAlgo = String(registry?.dh_algo || req?.dh_algo || 'X25519').trim();
@@ -3843,16 +3852,24 @@ export function useMeshChatController({
}
const msgId = `dm_${Date.now()}_${identity!.nodeId.slice(-4)}`;
const msgTimestamp = Math.floor(Date.now() / 1000);
const acceptMeta = connectDeliveryMeta({
intent: 'contact_accept',
contact: existingContact,
});
await enqueueDmSend(async () => {
const sent = await sendOffLedgerConsentMessage({
apiBase: API_BASE,
identity: identity!,
recipientId: senderId,
recipientDhPub: resolvedDhPubKey,
ciphertext,
msgId,
timestamp: msgTimestamp,
});
const sent = await ensureDmOutboxReleased(
await sendOffLedgerConsentMessage({
apiBase: API_BASE,
identity: identity!,
recipientId: senderId,
recipientDhPub: resolvedDhPubKey,
ciphertext,
msgId,
timestamp: msgTimestamp,
connectIntent: acceptMeta.connectIntent,
lookupPeerUrl: acceptMeta.lookupPeerUrl,
}),
);
if (!sent.ok) {
throw new Error(sent.detail || 'access_granted_send_failed');
}
@@ -3878,11 +3895,6 @@ export function useMeshChatController({
const handleDenyRequest = (senderId: string) => {
void (async () => {
if (requiresVerifiedFirstContact(getContacts()[senderId])) {
setSendError('import a signed invite before denying an unverified request');
setTimeout(() => setSendError(''), 4000);
return;
}
try {
const req = accessRequests.find((r) => r.sender_id === senderId);
const existingContact = getContacts()[senderId];
@@ -3893,6 +3905,7 @@ export function useMeshChatController({
API_BASE,
senderId,
existingContact?.invitePinnedPrekeyLookupHandle,
{ lookupPeerUrl: existingContact?.invitePinnedLookupPeerUrl },
).catch(() => null);
if (identity && targetKey?.dh_pub_key) {
const denyPlaintext = buildContactDenyMessage('declined');
@@ -3935,6 +3948,20 @@ export function useMeshChatController({
})();
};
const handleSeverContact = async (agentId: string) => {
try {
await severContact(agentId);
setContacts(getContacts());
if (selectedContact === agentId) {
setDmView('contacts');
}
} catch (err) {
const detail = err instanceof Error ? err.message : 'end contact failed';
setSendError(detail);
setTimeout(() => setSendError(''), 4000);
}
};
const handleBlockDM = async (agentId: string) => {
blockContact(agentId);
setContacts(getContacts());
@@ -4751,6 +4778,7 @@ export function useMeshChatController({
handleAcceptRequest,
handleDenyRequest,
handleBlockDM,
handleSeverContact,
handleVouch,
handleAddContact,
openChat,
+49
View File
@@ -0,0 +1,49 @@
/**
* Signal-style DM connect helpers — paste a short address or full invite blob.
*/
export function isLikelyDmShortAddress(value: string): boolean {
const trimmed = value.trim();
return (
!trimmed.startsWith('{') &&
!trimmed.startsWith('[') &&
/^[a-zA-Z0-9_.:-]{16,}$/.test(trimmed)
);
}
export function parseDmInviteImportBlob(raw: string): Record<string, unknown> {
const trimmed = raw.trim();
if (!trimmed) {
throw new Error('Paste a contact address first.');
}
if (isLikelyDmShortAddress(trimmed)) {
return { short_address: trimmed };
}
try {
const parsed = JSON.parse(trimmed) as unknown;
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
throw new Error('Contact address must be a signed address object.');
}
return parsed as Record<string, unknown>;
} catch (error) {
if (error instanceof SyntaxError) {
throw new Error('That does not look like a contact address. Paste what they copied from Secure Messages.');
}
throw error;
}
}
export function inviteFromParsedBlob(parsed: Record<string, unknown>): Record<string, unknown> {
const nested = parsed.invite;
if (nested && typeof nested === 'object' && !Array.isArray(nested)) {
return nested as Record<string, unknown>;
}
return parsed;
}
export function shortHandle(peerId: string): string {
const value = String(peerId || '').trim();
if (!value) return 'unknown';
if (value.length <= 18) return value;
return `${value.slice(0, 10)}${value.slice(-6)}`;
}
+40
View File
@@ -0,0 +1,40 @@
import type { Contact } from '@/mesh/meshIdentity';
import type { DmSendResponse } from '@/mesh/meshDmClient';
import { updatePrivateDeliveryAction } from '@/mesh/wormholeClient';
export type DmConnectIntent =
| 'invite_short_address'
| 'invite_import'
| 'contact_request'
| 'contact_accept'
| 'contact_offer';
export function connectDeliveryMeta(options: {
intent: DmConnectIntent;
lookupPeerUrl?: string;
contact?: Partial<Contact> | null;
}): { connectIntent: DmConnectIntent; lookupPeerUrl?: string } {
const lookupPeerUrl = String(
options.lookupPeerUrl || options.contact?.invitePinnedLookupPeerUrl || '',
)
.trim()
.replace(/\/$/, '');
return {
connectIntent: options.intent,
...(lookupPeerUrl ? { lookupPeerUrl } : {}),
};
}
/** Fallback when the server queued connect traffic but UI still shows a manual relay step. */
export async function ensureDmOutboxReleased(sent: DmSendResponse): Promise<DmSendResponse> {
if (!sent.ok) return sent;
const outboxId = String(sent.outbox_id || '').trim();
if (!outboxId) return sent;
if (!sent.queued && !sent.private_transport_pending) return sent;
try {
await updatePrivateDeliveryAction(outboxId, 'relay');
} catch {
// Backend auto-release may have already approved this outbox item.
}
return sent;
}
+67 -3
View File
@@ -89,6 +89,13 @@ export type DmSendResponse = {
private_transport_pending?: boolean;
};
export type DmConnectIntent =
| 'invite_short_address'
| 'invite_import'
| 'contact_request'
| 'contact_accept'
| 'contact_offer';
export type DmSendRequest = {
apiBase: string;
identity: NodeIdentity;
@@ -102,6 +109,8 @@ export type DmSendRequest = {
useSealedSender?: boolean;
format?: 'mls1' | 'dm1';
sessionWelcome?: string;
connectIntent?: DmConnectIntent;
lookupPeerUrl?: string;
};
const KEY_DM_BUNDLE_FINGERPRINT = 'sb_dm_bundle_fingerprint';
@@ -373,14 +382,54 @@ export async function ensureRegisteredDmKey(
};
}
function prekeyBundleToPublicKey(data: Record<string, unknown>): DmPublicKeyBundle | null {
if (!data?.ok) return null;
const bundle = (data.bundle && typeof data.bundle === 'object' ? data.bundle : data) as Record<
string,
unknown
>;
const dhPubKey = String(
bundle.identity_dh_pub_key || data.identity_dh_pub_key || data.dh_pub_key || '',
).trim();
const agentId = String(data.agent_id || '').trim();
if (!dhPubKey || !agentId) return null;
return {
ok: true,
agent_id: agentId,
lookup_mode: String(data.lookup_mode || 'invite_lookup_handle'),
dh_pub_key: dhPubKey,
dh_algo: String(data.dh_algo || bundle.dh_algo || 'X25519'),
timestamp: Number(data.timestamp || 0) || undefined,
signature: String(data.signature || ''),
public_key: String(data.public_key || ''),
public_key_algo: String(data.public_key_algo || ''),
sequence: Number(data.sequence || 0) || undefined,
prekey_transparency_head: String(data.prekey_transparency_head || ''),
prekey_transparency_size: Number(data.prekey_transparency_size || 0) || undefined,
};
}
async function fetchDmPublicKeyFromPrekeyBundle(
apiBase: string,
lookupToken: string,
): Promise<DmPublicKeyBundle | null> {
const params = new URLSearchParams({ lookup_token: lookupToken });
const res = await fetch(`${apiBase}/api/mesh/dm/prekey-bundle?${params.toString()}`);
const data = (await res.json()) as Record<string, unknown>;
return prekeyBundleToPublicKey(data);
}
export async function fetchDmPublicKey(
apiBase: string,
agentId: string,
lookupToken?: string,
options?: { allowLegacyAgentId?: boolean },
options?: { allowLegacyAgentId?: boolean; lookupPeerUrl?: string },
): Promise<DmPublicKeyBundle | null> {
const normalizedLookupToken = String(lookupToken || '').trim();
const normalizedAgentId = String(agentId || '').trim();
const normalizedLookupPeerUrl = String(options?.lookupPeerUrl || '')
.trim()
.replace(/\/$/, '');
if (!normalizedLookupToken && !options?.allowLegacyAgentId) {
return null;
}
@@ -388,12 +437,25 @@ export async function fetchDmPublicKey(
if (normalizedLookupToken) {
params.set('lookup_token', normalizedLookupToken);
}
if (normalizedLookupPeerUrl) {
params.set('lookup_peer_url', normalizedLookupPeerUrl);
}
if (normalizedAgentId && !normalizedLookupToken && options?.allowLegacyAgentId) {
params.set('agent_id', normalizedAgentId);
}
const res = await fetch(`${apiBase}/api/mesh/dm/pubkey?${params.toString()}`);
const data = await res.json();
return data.ok ? data : null;
const data = (await res.json()) as DmPublicKeyBundle;
if (data.ok && data.dh_pub_key) {
if (!data.agent_id && normalizedLookupToken) {
const fromPrekey = await fetchDmPublicKeyFromPrekeyBundle(apiBase, normalizedLookupToken);
if (fromPrekey) return fromPrekey;
}
return data;
}
if (normalizedLookupToken) {
return fetchDmPublicKeyFromPrekeyBundle(apiBase, normalizedLookupToken);
}
return null;
}
function spreadClaimPositions(totalClaims: number, spreadClaims: number): Set<number> {
@@ -684,6 +746,8 @@ export async function sendDmMessage(request: DmSendRequest): Promise<DmSendRespo
signature: signed.signature,
sequence: signed.sequence,
protocol_version: senderSeal && senderToken ? '' : signed.protocolVersion,
...(request.connectIntent ? { connect_intent: request.connectIntent } : {}),
...(request.lookupPeerUrl ? { lookup_peer_url: request.lookupPeerUrl } : {}),
}),
});
return res.json();
+31
View File
@@ -1350,6 +1350,7 @@ export interface Contact {
invitePinnedDhPubKey?: string;
invitePinnedDhAlgo?: string;
invitePinnedPrekeyLookupHandle?: string;
invitePinnedLookupPeerUrl?: string;
invitePinnedRootFingerprint?: string;
invitePinnedRootManifestFingerprint?: string;
invitePinnedRootWitnessPolicyFingerprint?: string;
@@ -1441,6 +1442,7 @@ function sanitizeContact(contact: Partial<Contact> | undefined): Contact {
invitePinnedDhPubKey: String(contact?.invitePinnedDhPubKey || ''),
invitePinnedDhAlgo: String(contact?.invitePinnedDhAlgo || ''),
invitePinnedPrekeyLookupHandle: String(contact?.invitePinnedPrekeyLookupHandle || ''),
invitePinnedLookupPeerUrl: String(contact?.invitePinnedLookupPeerUrl || ''),
invitePinnedRootFingerprint: String(contact?.invitePinnedRootFingerprint || ''),
invitePinnedRootManifestFingerprint: String(contact?.invitePinnedRootManifestFingerprint || ''),
invitePinnedRootWitnessPolicyFingerprint: String(
@@ -1775,6 +1777,35 @@ export function removeContact(agentId: string): void {
}
}
export async function severContact(
agentId: string,
options: { block?: boolean } = {},
): Promise<void> {
const peerId = String(agentId || '').trim();
if (!peerId) return;
await controlPlaneJson(`/api/wormhole/dm/contact/${encodeURIComponent(peerId)}/sever`, {
method: 'POST',
requireAdminSession: false,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ block: Boolean(options.block) }),
});
const contacts = getContacts();
if (!(peerId in contacts)) return;
contacts[peerId] = sanitizeContact({
...contacts[peerId],
sharedAlias: undefined,
previousSharedAliases: [],
pendingSharedAlias: undefined,
sharedAliasGraceUntil: undefined,
sharedAliasRotatedAt: undefined,
...(options.block ? { blocked: true } : {}),
});
saveContacts(contacts);
if (shouldUseWormholeContacts()) {
await persistContactToWormhole(peerId, contacts[peerId]);
}
}
export function isBlocked(agentId: string): boolean {
return getContacts()[agentId]?.blocked || false;
}
@@ -2016,6 +2016,18 @@ export async function deleteWormholeDmContact(
});
}
export async function severWormholeDmContact(
peerId: string,
options: { block?: boolean } = {},
): Promise<{ ok: boolean; peer_id: string; severed?: boolean; blocked?: boolean }> {
return controlPlaneJson(`/api/wormhole/dm/contact/${encodeURIComponent(peerId)}/sever`, {
method: 'POST',
requireAdminSession: false,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ block: Boolean(options.block) }),
});
}
export async function getActiveSigningContext(): Promise<ActiveSigningContext | null> {
const secureRequired = await isWormholeSecureRequired();
if (await isWormholeReady()) {