aboutsummaryrefslogtreecommitdiffstats
path: root/src/lib
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-03-05 23:01:15 -0800
committerAdam Soltys <[email protected]>2026-03-05 23:01:15 -0800
commit3b18232d52957dbd1a5a0c12891020d2a6e3295e (patch)
treeebb2d2673b89e71b0eaaa0ab829cab0d707ce3ee /src/lib
parent7471599082e053cabea19df25c867c654cb7445c (diff)
downloadcoinos-ui-3b18232d52957dbd1a5a0c12891020d2a6e3295e.tar.gz
coinos-ui-3b18232d52957dbd1a5a0c12891020d2a6e3295e.zip
messaging fixes
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/messaging.ts7
-rw-r--r--src/lib/mls-relay.ts62
-rw-r--r--src/lib/nip07.ts14
-rw-r--r--src/lib/nip17.ts9
4 files changed, 68 insertions, 24 deletions
diff --git a/src/lib/messaging.ts b/src/lib/messaging.ts
index fdb03f90..c4d4b211 100644
--- a/src/lib/messaging.ts
+++ b/src/lib/messaging.ts
@@ -33,22 +33,27 @@ async function checkMlsSupport(pubkey: string): Promise<boolean> {
if (mlsSupportCache.has(pubkey)) return mlsSupportCache.get(pubkey)!;
try {
const supported = await recipientSupportsMls(pubkey);
+ console.log("[messaging] MLS support for", pubkey.slice(0, 12) + "…:", supported);
mlsSupportCache.set(pubkey, supported);
return supported;
- } catch {
+ } catch (e) {
+ console.warn("[messaging] MLS support check failed:", e);
return false;
}
}
/** Send a message — MLS if both parties support it, NIP-17 otherwise. */
export const send = async (message: string, user: any, recipient: any, expiryDays?: number) => {
+ console.log("[messaging] send to", recipient.pubkey?.slice(0, 12) + "…", { self: user.pubkey === recipient.pubkey, expiry: !!expiryDays });
if (user.pubkey !== recipient.pubkey && !expiryDays) {
try {
const peerSupports = await checkMlsSupport(recipient.pubkey);
if (peerSupports) {
+ console.log("[messaging] → MLS path");
await sendMlsMessage(user, recipient.pubkey, message);
return;
}
+ console.log("[messaging] → NIP-17 fallback (no MLS support)");
} catch (e) {
console.warn("[messaging] MLS send failed, falling back to NIP-17:", e);
}
diff --git a/src/lib/mls-relay.ts b/src/lib/mls-relay.ts
index bc24b953..4b0001e3 100644
--- a/src/lib/mls-relay.ts
+++ b/src/lib/mls-relay.ts
@@ -20,27 +20,33 @@ const groupPeerMap = new Map<string, string>(); // mlsGroupIdHex -> pubkey
const nostrIdToMlsId = new Map<string, string>(); // nostrGroupIdHex -> mlsGroupIdHex
let mlsRumourCache: any[] = [];
const processedEventIds = new Set<string>();
+let _cacheUserPubkey: string | null = null;
-/** Load cached rumours and processed event IDs from localStorage */
-function loadLocalCache(): void {
+/** Load cached rumours and processed event IDs from localStorage (per-user) */
+function loadLocalCache(userPubkey?: string): void {
+ if (userPubkey) _cacheUserPubkey = userPubkey;
+ const pk = _cacheUserPubkey;
+ if (!pk) return;
try {
- const cached = localStorage.getItem("mls:rumours");
+ const cached = localStorage.getItem(`mls:rumours:${pk}`);
if (cached) {
const arr = JSON.parse(cached);
if (Array.isArray(arr)) mlsRumourCache = arr;
}
- const ids = localStorage.getItem("mls:processedEvents");
+ const ids = localStorage.getItem(`mls:processedEvents:${pk}`);
if (ids) {
for (const id of JSON.parse(ids)) processedEventIds.add(id);
}
} catch {}
}
-/** Save rumour cache and processed event IDs to localStorage */
+/** Save rumour cache and processed event IDs to localStorage (per-user) */
function saveLocalCache(): void {
+ const pk = _cacheUserPubkey;
+ if (!pk) return;
try {
- localStorage.setItem("mls:rumours", JSON.stringify(mlsRumourCache));
- localStorage.setItem("mls:processedEvents", JSON.stringify([...processedEventIds]));
+ localStorage.setItem(`mls:rumours:${pk}`, JSON.stringify(mlsRumourCache));
+ localStorage.setItem(`mls:processedEvents:${pk}`, JSON.stringify([...processedEventIds]));
} catch {}
}
@@ -76,6 +82,7 @@ let _activeSub: {
/** Publish our MLS key package to relays so others can invite us to groups. */
export async function publishKeyPackage(user: any, relays: string[] = DM_RELAYS): Promise<void> {
+ console.log("[mls] publishKeyPackage to", relays);
const client = getMarmotClient(user.pubkey);
await client.keyPackages.create({ relays, client: "coinos/marmot-ts" });
}
@@ -125,6 +132,7 @@ export async function startMlsConversation(
// Use relays from the key package event so the recipient can see our commits/welcomes
const kpRelaysTag = kpEvent.tags.find((t: string[]) => t[0] === "relays");
const groupRelays = kpRelaysTag ? kpRelaysTag.slice(1) : relays;
+ console.log("[mls] startConversation with", recipientPubkey.slice(0, 12) + "…", "relays:", groupRelays);
// Create group and invite recipient
const client = getMarmotClient(user.pubkey);
@@ -136,6 +144,8 @@ export async function startMlsConversation(
// Register IDs and store peer mapping
registerGroupIds(group);
const mlsGroupIdHex = bytesToHex(group.id);
+ const nostrGroupIdHex = getNostrGroupId(group);
+ console.log("[mls] created group mls:", mlsGroupIdHex.slice(0, 16), "nostr:", nostrGroupIdHex.slice(0, 16));
storePeerGroupMapping(recipientPubkey, mlsGroupIdHex);
// Subscribe to this new group's messages using nostr group ID
@@ -164,6 +174,7 @@ export async function sendMlsMessage(
const client = getMarmotClient(user.pubkey);
const group = await client.getGroup(mlsGroupIdHex!);
+ console.log("[mls] sendChatMessage to group", mlsGroupIdHex!.slice(0, 16));
await group.sendChatMessage(message);
// Add to local cache as a rumour-like object
@@ -184,6 +195,7 @@ function subscribeToGroup(nostrGroupIdHex: string): void {
_activeSub.subscribedGroups.add(nostrGroupIdHex);
const { seen, onNewRumour, closers, connectedRelays, user } = _activeSub;
const client = getMarmotClient(user.pubkey);
+ console.log("[mls] subscribeToGroup", nostrGroupIdHex.slice(0, 16), "on", connectedRelays.length, "relays");
for (const relay of connectedRelays) {
try {
@@ -224,8 +236,14 @@ export async function subscribeToMlsMessages(
// Set up active subscription state for dynamic group adds
_activeSub = { user, relays, seen, onNewRumour, subscribedGroups, closers, connectedRelays };
+ // Initialize per-user caches
+ _cacheUserPubkey = user.pubkey;
+ restorePeerMappings(user.pubkey);
+ loadLocalCache(user.pubkey);
+
// Load all known groups and register their ID mappings
await client.loadAllGroups();
+ console.log("[mls] subscribe: loaded", client.groups.length, "groups, relays:", relays);
const allRelayUrls = new Set(relays);
for (const g of client.groups) {
registerGroupIds(g);
@@ -235,16 +253,18 @@ export async function subscribeToMlsMessages(
// Listen for new invites (welcomes via NIP-59)
inviteReader.on("newInvite", async (invite) => {
+ console.log("[mls] received welcome from", invite.pubkey?.slice(0, 12) + "…");
try {
const { group } = await client.joinGroupFromWelcome({ welcomeRumor: invite });
registerGroupIds(group);
const mlsGroupIdHex = bytesToHex(group.id);
const senderPubkey = invite.pubkey;
+ console.log("[mls] joined group", mlsGroupIdHex.slice(0, 16), "from", senderPubkey.slice(0, 12));
storePeerGroupMapping(senderPubkey, mlsGroupIdHex);
// Subscribe to this newly joined group's messages
subscribeToGroup(getNostrGroupId(group));
} catch (e) {
- console.error("[mls-relay] Failed to join from welcome:", e);
+ console.error("[mls] Failed to join from welcome:", e);
}
});
@@ -324,7 +344,10 @@ async function handleGroupMessageEvent(
// Resolve nostr group ID to MLS group ID for client.getGroup()
const mlsGroupIdHex = nostrIdToMlsId.get(nostrGroupIdHex);
- if (!mlsGroupIdHex) return null;
+ if (!mlsGroupIdHex) {
+ console.warn("[mls] unknown nostr group ID:", nostrGroupIdHex.slice(0, 16));
+ return null;
+ }
let group;
try {
@@ -335,6 +358,7 @@ async function handleGroupMessageEvent(
// Use MarmotGroup.ingest to process the event
for await (const result of group.ingest([event])) {
+ console.log("[mls] ingest result:", result.kind, result.result?.kind ?? "");
if (result.kind === "processed" && result.result.kind === "applicationMessage") {
const raw = new TextDecoder().decode(result.result.message);
let content: string;
@@ -386,8 +410,8 @@ async function handleGroupMessageEvent(
/** Fetch MLS messages the user has received. Returns rumour-like objects. */
export async function getMlsMessageRumours(user: any, relays: string[] = DM_RELAYS): Promise<any[]> {
- // Load from localStorage on first call
- if (mlsRumourCache.length === 0) loadLocalCache();
+ // Load from localStorage on first call (per-user)
+ if (mlsRumourCache.length === 0) loadLocalCache(user.pubkey);
if (mlsRumourCache.length > 0) return mlsRumourCache;
const client = getMarmotClient(user.pubkey);
@@ -434,34 +458,34 @@ export async function getMlsMessageRumours(user: any, relays: string[] = DM_RELA
function storePeerGroupMapping(peerPubkey: string, mlsGroupId: string): void {
peerGroupMap.set(peerPubkey, mlsGroupId);
groupPeerMap.set(mlsGroupId, peerPubkey);
+ const pk = _cacheUserPubkey;
+ if (!pk) return;
try {
- const key = "mls:peer2group";
+ const key = `mls:peer2group:${pk}`;
const map = JSON.parse(localStorage.getItem(key) || "{}");
map[peerPubkey] = mlsGroupId;
localStorage.setItem(key, JSON.stringify(map));
- const rkey = "mls:group2peer";
+ const rkey = `mls:group2peer:${pk}`;
const rmap = JSON.parse(localStorage.getItem(rkey) || "{}");
rmap[mlsGroupId] = peerPubkey;
localStorage.setItem(rkey, JSON.stringify(rmap));
} catch {}
}
-// Restore peer mappings from localStorage on load
-function restorePeerMappings(): void {
+// Restore peer mappings from localStorage (per-user, called when user is known)
+function restorePeerMappings(userPubkey: string): void {
try {
- const map = JSON.parse(localStorage.getItem("mls:peer2group") || "{}");
+ const map = JSON.parse(localStorage.getItem(`mls:peer2group:${userPubkey}`) || "{}");
for (const [k, v] of Object.entries(map)) {
peerGroupMap.set(k, v as string);
}
- const rmap = JSON.parse(localStorage.getItem("mls:group2peer") || "{}");
+ const rmap = JSON.parse(localStorage.getItem(`mls:group2peer:${userPubkey}`) || "{}");
for (const [k, v] of Object.entries(rmap)) {
groupPeerMap.set(k, v as string);
}
} catch {}
}
-restorePeerMappings();
-loadLocalCache();
// ---------------------------------------------------------------------------
// Helpers
diff --git a/src/lib/nip07.ts b/src/lib/nip07.ts
index e90d13a7..691e3bcd 100644
--- a/src/lib/nip07.ts
+++ b/src/lib/nip07.ts
@@ -32,15 +32,23 @@ const tryNsecFromStorage = async (): Promise<Uint8Array | null> => {
// Ensures we have access to a secret key, triggering the signer modal if needed.
// Returns the sk as Uint8Array, or null if using NIP-07 extension.
export const ensureSigner = async (userPubkey: string): Promise<Uint8Array | null> => {
- // Check if signer already has a valid sk (64 hex chars = 32 bytes)
+ // Check if signer already has a valid sk that matches the user's pubkey
const currentSigner = get(signer);
if (currentSigner?.ready && currentSigner?.params?.sk && currentSigner.params.sk.length === 64) {
- return hexToBytes(currentSigner.params.sk);
+ const sk = hexToBytes(currentSigner.params.sk);
+ if (getPublicKey(sk) === userPubkey) return sk;
+ // Stale signer — clear it so we re-derive below
+ signer.set(undefined);
}
// Try to load from localStorage nsec before triggering the modal
const skFromStorage = await tryNsecFromStorage();
- if (skFromStorage) return skFromStorage;
+ if (skFromStorage) {
+ if (getPublicKey(skFromStorage) === userPubkey) return skFromStorage;
+ // localStorage nsec doesn't match — clear it
+ localStorage.removeItem("nsec");
+ signer.set(undefined);
+ }
// Try to derive nostr key from cached canonical key (e.g. passkey accounts)
try {
diff --git a/src/lib/nip17.ts b/src/lib/nip17.ts
index 7ef93315..62e1a8b3 100644
--- a/src/lib/nip17.ts
+++ b/src/lib/nip17.ts
@@ -190,6 +190,9 @@ const fetchMessageRumours = async (user: any): Promise<any[]> => {
try {
const rumour: any = await decrypt(event, user);
+ // Skip non-DM rumours (e.g. MLS welcomes that share kind 1059 gift wraps)
+ if (rumour.kind && rumour.kind !== 14) continue;
+
const expires = expiration(event);
if (expires) {
rumour.expiration = expires;
@@ -373,15 +376,19 @@ export const subscribeToMessages = async (
if (expired(event)) return;
try {
const rumour: any = await decrypt(event, user);
+ // Skip non-DM rumours (e.g. MLS welcomes that share kind 1059 gift wraps)
+ if (rumour.kind && rumour.kind !== 14) return;
const expires = expiration(event);
if (expires) rumour.expiration = expires;
+ console.log("[nip17] new rumour from", rumour.pubkey?.slice(0, 12) + "…:", rumour.content?.slice(0, 40));
onNewRumour(rumour);
} catch (e) {
- console.error("Failed to decrypt subscription event:", event.id, e);
+ console.error("[nip17] Failed to decrypt subscription event:", event.id, e);
}
},
oneose: () => {
eoseCount++;
+ console.log("[nip17] EOSE", eoseCount + "/" + relayUrls.length, "from", url);
},
},
);