router.go raw

   1  package main
   2  
   3  import (
   4  	"common/helpers"
   5  	"common/jsbridge/sw"
   6  )
   7  
   8  // Subscription Router — thin forwarder to relay SW via bus.
   9  // All relay operations, subscriptions, and caching are handled by the relay SW.
  10  
  11  // pendingSentDMs holds MLS_SEND saves deferred because myPubkey wasn't set yet.
  12  // Replayed when SET_PUBKEY arrives.
  13  type pendingSentDM struct {
  14  	recipient string
  15  	content   string
  16  }
  17  
  18  var pendingSentDMs []pendingSentDM
  19  var mlsRelays []string
  20  var marmotSubs map[string]bool // tracks active marmot proxy subscriptions for cleanup
  21  
  22  // sentDMDedup tracks recently sent DM content to filter self-echoes.
  23  // MLS handleGroupMessage always attributes messages to PeerPub, so when
  24  // our own outbound echoes back (seenIDs lost on WASM restart), it appears
  25  // as a message from the peer. This ring buffer catches that.
  26  var sentDMDedup [32]string
  27  var sentDMIdx int
  28  
  29  func markSentDM(peer, content string) {
  30  	sentDMDedup[sentDMIdx%32] = peer + "\x00" + content
  31  	sentDMIdx++
  32  }
  33  
  34  func isSentDMEcho(peer, content string) bool {
  35  	needle := peer + "\x00" + content
  36  	for _, s := range sentDMDedup {
  37  		if s == needle {
  38  			return true
  39  		}
  40  	}
  41  	return false
  42  }
  43  
  44  func flushPendingSentDMs() {
  45  	if myPubkey == "" || len(pendingSentDMs) == 0 {
  46  		return
  47  	}
  48  	for _, p := range pendingSentDMs {
  49  		now := sw.NowSeconds()
  50  		rec := makeDMRecord(p.recipient, myPubkey, p.content, now, "marmot", "")
  51  		busSend("relay", "[\"SAVE_DM_QUIET\","+rec.ToJSON()+"]")
  52  	}
  53  	pendingSentDMs = nil
  54  }
  55  
  56  func routeMessage(clientID string, w *mw, msgType string) {
  57  	sw.Log("shell: page→" + msgType)
  58  	switch msgType {
  59  	// Identity — handle locally + forward to relay SW.
  60  	case "SET_PUBKEY":
  61  		pk := w.str()
  62  		identitySetPubkey(pk)
  63  		busSend("relay", "[\"SET_PUBKEY\","+jstr(pk)+"]")
  64  		flushPendingSentDMs()
  65  	case "CLEAR_KEY":
  66  		identityClearKey()
  67  		busSend("relay", "[\"CLEAR_KEY\"]")
  68  
  69  	// Relay operations — forward to relay SW.
  70  	case "REQ":
  71  		subID := w.str()
  72  		filterRaw := w.raw()
  73  		busSend("relay", "[\"REQ\","+jstr(clientID)+","+jstr(subID)+","+filterRaw+"]")
  74  	case "CLOSE":
  75  		subID := w.str()
  76  		busSend("relay", "[\"CLOSE\","+jstr(subID)+"]")
  77  	case "EVENT":
  78  		eventRaw := w.raw()
  79  		busSend("relay", "[\"EVENT\","+jstr(clientID)+","+eventRaw+"]")
  80  	case "PROXY":
  81  		subID := w.str()
  82  		filterRaw := w.raw()
  83  		relayURLs := w.strs()
  84  		busSend("relay", "[\"PROXY\","+jstr(clientID)+","+jstr(subID)+","+filterRaw+","+strsJSON(relayURLs)+"]")
  85  	case "RELAY_INFO":
  86  		relayURL := w.str()
  87  		busSend("relay", "[\"RELAY_INFO\","+jstr(clientID)+","+jstr(relayURL)+"]")
  88  	case "SET_WRITE_RELAYS":
  89  		busSend("relay", "[\"SET_WRITE_RELAYS\","+strsJSON(w.strs())+"]")
  90  	case "SIGN":
  91  		requestID := w.str()
  92  		eventRaw := w.raw()
  93  		busSend("relay", "[\"SIGN\","+jstr(clientID)+","+jstr(requestID)+","+eventRaw+"]")
  94  	case "BROADCAST":
  95  		pubkey := w.str()
  96  		relayURLs := w.strs()
  97  		busSend("relay", "[\"BROADCAST\","+jstr(clientID)+","+jstr(pubkey)+","+strsJSON(relayURLs)+"]")
  98  
  99  	// DM operations — forward to relay SW for IDB.
 100  	case "DM_LIST":
 101  		busSend("relay", "[\"DM_LIST\","+jstr(clientID)+"]")
 102  	case "DM_HISTORY":
 103  		peer := w.str()
 104  		limit := int(w.num())
 105  		until := w.num()
 106  		busSend("relay", "[\"DM_HISTORY\","+jstr(clientID)+","+jstr(peer)+","+helpers.Itoa(int64(limit))+","+helpers.Itoa(until)+"]")
 107  	case "CLEAR_DM_HISTORY":
 108  		peer := w.str()
 109  		busSend("relay", "[\"CLEAR_DM_HISTORY\","+jstr(peer)+"]")
 110  
 111  	// MLS — proxy through page to signer extension (marmot WASM runs inside signer).
 112  	case "MLS_INIT":
 113  		relays := w.strs()
 114  		mlsRelays = relays
 115  		sendToClient(clientID, "[\"MLS_PROXY\",\"init\","+strsJSON(relays)+"]")
 116  	case "MLS_SEND":
 117  		recipient := w.str()
 118  		content := w.str()
 119  		markSentDM(recipient, content)
 120  		sendToClient(clientID, "[\"MLS_PROXY\",\"sendDM\","+jstr(recipient)+","+jstr(content)+"]")
 121  		// Save sent DM to relay's IDB (quiet — no DM_RECEIVED broadcast).
 122  		if myPubkey == "" {
 123  			pendingSentDMs = append(pendingSentDMs, pendingSentDM{recipient, content})
 124  		} else {
 125  			now := sw.NowSeconds()
 126  			rec := makeDMRecord(recipient, myPubkey, content, now, "marmot", "")
 127  			busSend("relay", "[\"SAVE_DM_QUIET\","+rec.ToJSON()+"]")
 128  		}
 129  	case "MLS_SUB":
 130  		sendToClient(clientID, "[\"MLS_PROXY\",\"subscribe\"]")
 131  	case "MLS_PUBLISH_KP":
 132  		sendToClient(clientID, "[\"MLS_PROXY\",\"publishKP\"]")
 133  	case "MLS_LIST_GROUPS":
 134  		sendToClient(clientID, "[\"MLS_PROXY\",\"listGroups\"]")
 135  	case "MLS_BACKUP":
 136  		sendToClient(clientID, "[\"MLS_PROXY\",\"backupGroups\"]")
 137  	case "MLS_RESTORE":
 138  		sendToClient(clientID, "[\"MLS_PROXY\",\"restoreGroups\"]")
 139  	case "MLS_RATCHET":
 140  		peer := w.str()
 141  		sendToClient(clientID, "[\"MLS_PROXY\",\"ratchetGroup\","+jstr(peer)+"]")
 142  
 143  	// MLS results from page (mls-bridge.mjs routes signer extension outputs here).
 144  	// Relay URLs may come from mlsRelays (set by MLS_INIT) or inline in the message
 145  	// (set by mls-bridge.mjs). The inline URLs ensure routing works even before the
 146  	// Go WASM app sends MLS_INIT.
 147  	case "MLS_PUBLISH":
 148  		eventRaw := w.str()
 149  		relays := w.strs()
 150  		if len(relays) == 0 {
 151  			relays = mlsRelays
 152  		}
 153  		if len(relays) > 0 {
 154  			busSend("relay", "[\"MLS_RELAY_PUBLISH\","+eventRaw+","+strsJSON(relays)+"]")
 155  		} else {
 156  			busSend("relay", "[\"EVENT\",\"\","+eventRaw+"]")
 157  		}
 158  	case "MLS_SUBSCRIBE":
 159  		subID := w.str()
 160  		filterRaw := w.raw()
 161  		relays := w.strs()
 162  		if len(relays) == 0 {
 163  			relays = mlsRelays
 164  		}
 165  		// Pass filters as-is (array or single object) — relay SW's parseFilters handles both.
 166  		mSubID := "marmot-sub-" + subID
 167  		if marmotSubs == nil {
 168  			marmotSubs = make(map[string]bool)
 169  		}
 170  		marmotSubs[mSubID] = true
 171  		if len(relays) > 0 {
 172  			busSend("relay", "[\"PROXY\",\"\","+jstr(mSubID)+","+filterRaw+","+strsJSON(relays)+"]")
 173  		} else {
 174  			busSend("relay", "[\"REQ\",\"\","+jstr(mSubID)+","+filterRaw+"]")
 175  		}
 176  	case "MLS_DM":
 177  		dmJSON := w.raw()
 178  		// mls-bridge sends {peer, sender, content, ts, source, eventId}
 179  		// but IDB expects {id, peer, from, content, created_at, protocol, eventId}.
 180  		peer := jsonField(dmJSON, "peer")
 181  		sender := jsonField(dmJSON, "sender")
 182  		content := jsonField(dmJSON, "content")
 183  		// Self-echo filter: MLS handleGroupMessage always sets sender=PeerPub.
 184  		// If we recently sent this exact content to this peer, it's our own echo.
 185  		if isSentDMEcho(peer, content) {
 186  			return
 187  		}
 188  		ts := parseTS(jsonFieldRaw(dmJSON, "ts"))
 189  		source := jsonField(dmJSON, "source")
 190  		eventID := jsonField(dmJSON, "eventId")
 191  		rec := makeDMRecord(peer, sender, content, ts, source, eventID)
 192  		recJSON := rec.ToJSON()
 193  		busSend("relay", "[\"SAVE_DM_QUIET\","+recJSON+"]")
 194  		fwdDM(recJSON)
 195  	case "MLS_GROUPS":
 196  		groupsJSON := w.raw()
 197  		broadcastToClients("[\"MLS_GROUPS\"," + groupsJSON + "]")
 198  	case "MLS_STATUS":
 199  		statusMsg := w.str()
 200  		broadcastToClients("[\"MLS_STATUS\"," + jstr(statusMsg) + "]")
 201  
 202  	// Crypto result from page — dispatch to waiting callback.
 203  	case "CRYPTO_RESULT":
 204  		id := int(w.num())
 205  		result := w.str()
 206  		errMsg := w.str()
 207  		if fn, ok := cryptoCBs[id]; ok {
 208  			delete(cryptoCBs, id)
 209  			fn(result, errMsg)
 210  		}
 211  	}
 212  }
 213  
 214  // fwdDM broadcasts a DM_RECEIVED message to all page clients.
 215  func fwdDM(dmJSON string) {
 216  	broadcastToClients("[\"DM_RECEIVED\"," + dmJSON + "]")
 217  }
 218