Add yaw2 invite interoperability

Protocol:
- invite.go: include full 64-char `net` hash in waste: invite blob
  (matches yaw2's `net` field — any client parsing the base64 JSON can
  join without knowing the plaintext name). Expose NetHash() helper.
- netmgr: add JoinByHash() — join via full 64-char hex hash alone,
  storing the short ID as display name. Enables joining yaw2 networks
  from a URL that only carries the hash.
- anchor: expose RunByHash() so netmgr can pass a pre-computed hash
  directly without a name→hash roundtrip.
- ipc/proto: add network_hash field to join_network — routes to
  JoinByHash when present and network_name is absent.

Web UI:
- Parse ?net=<64hex> (yaw2 URL param) and ?a=<anchor> in addition to
  existing ?n= / ?invite= params. Hash-only joins send network_hash.
- Sidebar shows yaw: contact card (yaw:<masterID>?n=<alias>) using the
  master identity — compatible with yaw2 contact card format. Click to
  copy to clipboard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-06-22 23:29:45 +02:00
parent 7fe02e9463
commit add7c5fea8
8 changed files with 193 additions and 33 deletions

View File

@@ -147,6 +147,88 @@ func (mgr *Manager) Join(name, shareDir string) (string, error) {
return netID, nil
}
// JoinByHash joins a network using its pre-computed full 64-char hex hash
// (yaw2 `net` field) instead of the plaintext name. The network is stored
// with an empty name; the network_id (first 8 bytes) is used for display.
// This enables joining networks whose names are unknown — e.g. from a yaw2
// invite URL that only contains the hash.
func (mgr *Manager) JoinByHash(netHash64, shareDir string) (string, error) {
if len(netHash64) != 64 {
return "", fmt.Errorf("netHash must be 64 hex chars, got %d", len(netHash64))
}
netID := netHash64[:16] // first 8 bytes = 16 hex chars
mgr.mu.Lock()
if _, exists := mgr.networks[netID]; exists {
mgr.mu.Unlock()
return netID, nil
}
mgr.mu.Unlock()
derived, err := crypto.DeriveForNetwork(mgr.cfg.MasterIdentity, netHash64)
if err != nil {
return "", fmt.Errorf("derive identity for net %s: %w", netID, err)
}
dbPath := filepath.Join(mgr.cfg.StoreDir, "messages-"+netID+".db")
st, err := store.Open(dbPath)
if err != nil {
return "", fmt.Errorf("open store for net %s: %w", netID, err)
}
m := mesh.New(derived, st)
if shareDir != "" {
m.ShareDir = shareDir
} else if mgr.cfg.ShareDir != "" {
m.ShareDir = mgr.cfg.ShareDir
}
m.DownloadDir = filepath.Join(mgr.cfg.StoreDir, "downloads-"+netID)
meshEvents := m.Subscribe()
go func() {
for evt := range meshEvents {
evt.NetworkID = netID
mgr.emit(evt)
}
}()
ctx, cancel := context.WithCancel(context.Background())
net := &Network{
ID: netID,
Name: netID, // display as short hash when name is unknown
Hash: netHash64,
Identity: derived,
Mesh: m,
Store: st,
cancel: cancel,
}
mgr.mu.Lock()
mgr.networks[netID] = net
mgr.order = append(mgr.order, netID)
mgr.mu.Unlock()
log.Printf("netmgr: joining network by hash id=%s peer=%s", netID, derived.PeerID().Short())
if mgr.cfg.AnchorURL != "" {
go func() {
anchor.RunByHash(ctx, mgr.cfg.AnchorURL, netHash64, derived, m)
log.Printf("netmgr: left network %s", netID)
}()
}
mgr.emit(proto.IpcMessage{
Type: proto.EvtNetworkJoined,
NetworkID: netID,
NetworkName: netID,
LocalPeer: peerPtr(derived.PeerInfo()),
ShareDir: m.ShareDir,
})
return netID, nil
}
// Leave cancels a network context by ID. Closes its store.
func (mgr *Manager) Leave(netID string) {
mgr.mu.Lock()