feat(client): multi-device fan-out + auto-link (v2.2.0-alpha2)
PR #2 of the multi-device roadmap — wires the messenger pipeline against the on-chain registry landed in v2.2.0-alpha1. lib/api.ts: - DeviceInfo type mirroring blockchain.DeviceInfo. - IdentityInfo.device_count (optional; populated from /api/identity). - fetchDevices(masterPub) → /api/devices/{master_pub}, returns []. Errors swallowed so a downed endpoint doesn't block messaging. - resolveRecipientKeys(masterPub) — the routing primitive. Returns devices[] if registered, else falls back to IdentityInfo.x25519_pub (pre-v2.2.0 path). Empty only when recipient has published nothing. - buildLinkDeviceTx / buildUnlinkDeviceTx — signed by master Ed25519, min-fee cost, canonical JSON payload matching the chain-side LinkDevicePayload / UnlinkDevicePayload. app/(app)/chats/[id].tsx: - sendCore now fans out: encrypts once per recipient device pub (Promise.all, any failure rejects the batch), falls back to the cached contact.x25519Pub if the registry lookup returns nothing. - Saved Messages short-circuit preserved; no devices lookup for self. app/(app)/_layout.tsx: - On every sign-in, auto-submit LINK_DEVICE for this device if its X25519 pub isn't already in the master's registry. Device name picks "iPhone" / "Android phone" / "Device" by Platform. Errors (insufficient balance / legacy chain without LINK_DEVICE support) are silent — next launch retries. Backward compatibility: senders fall back to identity.x25519_pub when the recipient has no registry entries, so pre-v2.2.0 clients still receive messages. Chain-side already gates new validation on the event types existing; old clients simply never emit LINK_DEVICE and keep working with a single X25519. Next — PR #3 (Settings → Devices screen + QR pairing flow + receive-side self-wipe on revoke detection).
This commit is contained in:
@@ -23,7 +23,7 @@ import * as Clipboard from 'expo-clipboard';
|
||||
import { useStore } from '@/lib/store';
|
||||
import { useMessages } from '@/hooks/useMessages';
|
||||
import { encryptMessage } from '@/lib/crypto';
|
||||
import { sendEnvelope } from '@/lib/api';
|
||||
import { sendEnvelope, resolveRecipientKeys } from '@/lib/api';
|
||||
import { getWSClient } from '@/lib/ws';
|
||||
import { appendMessage, loadMessages } from '@/lib/storage';
|
||||
import { randomId, safeBack } from '@/lib/utils';
|
||||
@@ -212,15 +212,34 @@ export default function ChatScreen() {
|
||||
// leaves the device, so no encryption/fee/network round-trip is needed.
|
||||
// Regular chats still go through the NaCl + relay pipeline below.
|
||||
if (hasText && !isSavedMessages) {
|
||||
const { nonce, ciphertext } = encryptMessage(
|
||||
actualText.trim(), keyFile.x25519_priv, contact.x25519Pub,
|
||||
);
|
||||
await sendEnvelope({
|
||||
senderPub: keyFile.x25519_pub,
|
||||
recipientPub: contact.x25519Pub,
|
||||
senderEd25519Pub: keyFile.pub_key,
|
||||
nonce, ciphertext,
|
||||
});
|
||||
// Multi-device fan-out (v2.2.0): resolve the recipient's active
|
||||
// device X25519 pubs via /api/devices. Legacy identities (no
|
||||
// devices registered) fall back to their published identity
|
||||
// x25519 pub, preserving the pre-v2.2.0 single-device path.
|
||||
// `contact.x25519Pub` stays the floor — if both network calls
|
||||
// fail we still attempt delivery to the cached pub so a flaky
|
||||
// connection doesn't block outgoing messages.
|
||||
let recipientPubs = await resolveRecipientKeys(contact.address);
|
||||
if (recipientPubs.length === 0 && contact.x25519Pub) {
|
||||
recipientPubs = [contact.x25519Pub];
|
||||
}
|
||||
if (recipientPubs.length === 0) {
|
||||
throw new Error('recipient has no encryption key published');
|
||||
}
|
||||
// One sealed envelope per recipient device. Parallel — slow
|
||||
// relays don't block each other; any individual failure
|
||||
// rejects the whole send (user retries).
|
||||
await Promise.all(recipientPubs.map(async (rpub) => {
|
||||
const { nonce, ciphertext } = encryptMessage(
|
||||
actualText.trim(), keyFile.x25519_priv, rpub,
|
||||
);
|
||||
await sendEnvelope({
|
||||
senderPub: keyFile.x25519_pub,
|
||||
recipientPub: rpub,
|
||||
senderEd25519Pub: keyFile.pub_key,
|
||||
nonce, ciphertext,
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
const msg: Message = {
|
||||
|
||||
Reference in New Issue
Block a user