Multi-network foundation: netmgr, derived identities, additive IPC protocol
YAW/2 peer wire protocol is unchanged. Changes are local only.
internal/crypto:
- DeriveForNetwork(master, networkHash) — HKDF-SHA256 from master seed + network hash;
same master + same network always produces the same Ed25519 keypair (stable peer ID)
internal/proto:
- NetworkInfo type (network_id, network_name, local_peer)
- NetworkID field on IpcMessage (optional; commands default to first network when absent)
- Networks []NetworkInfo on state_snapshot (additive alongside existing local_peer)
- EvtNetworkJoined / EvtNetworkLeft events
internal/netmgr (new):
- Manager holds N independent Network contexts (derived identity, mesh, store, anchor)
- Join(name) creates context, derives identity, opens per-network DB, starts anchor client
- Leave(id) / LeaveAll() cancel contexts and close stores
- Resolve(netID) returns named network, or Default() when netID is empty (backward compat)
- Fan-out: Manager.Subscribe() receives tagged events from all networks
- Network IDs are the first 8 hex chars of SHA-256("yaw2-net:"+name) — stable and short
internal/ipc:
- Run(mgr, port) replaces Run(m *mesh.Mesh, port, anchorURL, joinFn)
- Commands without network_id route to mgr.Default() (backward compat)
- state_snapshot includes Networks array; local_peer/connected_peers still populated from first network
- generate_invite, get_file_list, send_message all respect network_id routing
cmd/daemon:
- Creates netmgr.Manager instead of mesh.Mesh directly
- --join and --share-dir pass through Config
- Auto-join via mgr.Join() before IPC starts
test-network.sh:
- Fix peer_name bash bug: $() with && inside triggers set -e; use if/elif/else instead
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 19:13:54 +02:00
|
|
|
// Package netmgr manages multiple concurrent network contexts.
|
|
|
|
|
// Each network gets its own derived identity, mesh, store, and anchor connection.
|
|
|
|
|
// The Manager fans out events from all networks to IPC subscribers, tagging each
|
|
|
|
|
// event with the network_id so clients can route them appropriately.
|
|
|
|
|
package netmgr
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"fmt"
|
|
|
|
|
"log"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"sync"
|
|
|
|
|
|
|
|
|
|
"github.com/waste-go/internal/anchor"
|
|
|
|
|
"github.com/waste-go/internal/crypto"
|
|
|
|
|
"github.com/waste-go/internal/mesh"
|
|
|
|
|
"github.com/waste-go/internal/proto"
|
|
|
|
|
"github.com/waste-go/internal/store"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Config holds the Manager's startup configuration.
|
|
|
|
|
type Config struct {
|
|
|
|
|
MasterIdentity *crypto.Identity
|
|
|
|
|
StoreDir string // base directory for per-network SQLite files
|
|
|
|
|
AnchorURL string // WebSocket anchor URL used for all networks
|
2026-06-22 15:13:26 +02:00
|
|
|
ShareDir string // default share directory; overridden per network via Join or SetShareDir
|
Multi-network foundation: netmgr, derived identities, additive IPC protocol
YAW/2 peer wire protocol is unchanged. Changes are local only.
internal/crypto:
- DeriveForNetwork(master, networkHash) — HKDF-SHA256 from master seed + network hash;
same master + same network always produces the same Ed25519 keypair (stable peer ID)
internal/proto:
- NetworkInfo type (network_id, network_name, local_peer)
- NetworkID field on IpcMessage (optional; commands default to first network when absent)
- Networks []NetworkInfo on state_snapshot (additive alongside existing local_peer)
- EvtNetworkJoined / EvtNetworkLeft events
internal/netmgr (new):
- Manager holds N independent Network contexts (derived identity, mesh, store, anchor)
- Join(name) creates context, derives identity, opens per-network DB, starts anchor client
- Leave(id) / LeaveAll() cancel contexts and close stores
- Resolve(netID) returns named network, or Default() when netID is empty (backward compat)
- Fan-out: Manager.Subscribe() receives tagged events from all networks
- Network IDs are the first 8 hex chars of SHA-256("yaw2-net:"+name) — stable and short
internal/ipc:
- Run(mgr, port) replaces Run(m *mesh.Mesh, port, anchorURL, joinFn)
- Commands without network_id route to mgr.Default() (backward compat)
- state_snapshot includes Networks array; local_peer/connected_peers still populated from first network
- generate_invite, get_file_list, send_message all respect network_id routing
cmd/daemon:
- Creates netmgr.Manager instead of mesh.Mesh directly
- --join and --share-dir pass through Config
- Auto-join via mgr.Join() before IPC starts
test-network.sh:
- Fix peer_name bash bug: $() with && inside triggers set -e; use if/elif/else instead
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 19:13:54 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Network is a single joined network context.
|
|
|
|
|
type Network struct {
|
|
|
|
|
ID string // first 8 hex chars of networkHash — stable, short, opaque
|
|
|
|
|
Name string
|
|
|
|
|
Hash string // full SHA-256("yaw2-net:"+name), hex
|
|
|
|
|
|
|
|
|
|
Identity *crypto.Identity
|
|
|
|
|
Mesh *mesh.Mesh
|
|
|
|
|
Store *store.Store
|
|
|
|
|
|
|
|
|
|
cancel context.CancelFunc
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 15:13:26 +02:00
|
|
|
// ShareDir returns the share directory for this network (from the mesh).
|
|
|
|
|
func (n *Network) ShareDir() string { return n.Mesh.ShareDir }
|
|
|
|
|
|
Multi-network foundation: netmgr, derived identities, additive IPC protocol
YAW/2 peer wire protocol is unchanged. Changes are local only.
internal/crypto:
- DeriveForNetwork(master, networkHash) — HKDF-SHA256 from master seed + network hash;
same master + same network always produces the same Ed25519 keypair (stable peer ID)
internal/proto:
- NetworkInfo type (network_id, network_name, local_peer)
- NetworkID field on IpcMessage (optional; commands default to first network when absent)
- Networks []NetworkInfo on state_snapshot (additive alongside existing local_peer)
- EvtNetworkJoined / EvtNetworkLeft events
internal/netmgr (new):
- Manager holds N independent Network contexts (derived identity, mesh, store, anchor)
- Join(name) creates context, derives identity, opens per-network DB, starts anchor client
- Leave(id) / LeaveAll() cancel contexts and close stores
- Resolve(netID) returns named network, or Default() when netID is empty (backward compat)
- Fan-out: Manager.Subscribe() receives tagged events from all networks
- Network IDs are the first 8 hex chars of SHA-256("yaw2-net:"+name) — stable and short
internal/ipc:
- Run(mgr, port) replaces Run(m *mesh.Mesh, port, anchorURL, joinFn)
- Commands without network_id route to mgr.Default() (backward compat)
- state_snapshot includes Networks array; local_peer/connected_peers still populated from first network
- generate_invite, get_file_list, send_message all respect network_id routing
cmd/daemon:
- Creates netmgr.Manager instead of mesh.Mesh directly
- --join and --share-dir pass through Config
- Auto-join via mgr.Join() before IPC starts
test-network.sh:
- Fix peer_name bash bug: $() with && inside triggers set -e; use if/elif/else instead
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 19:13:54 +02:00
|
|
|
// Manager owns all joined networks and fans out their events to IPC subscribers.
|
|
|
|
|
type Manager struct {
|
|
|
|
|
cfg Config
|
|
|
|
|
|
|
|
|
|
mu sync.RWMutex
|
|
|
|
|
networks map[string]*Network // keyed by Network.ID
|
|
|
|
|
order []string // insertion order for "default" lookups
|
|
|
|
|
|
|
|
|
|
subsMu sync.Mutex
|
|
|
|
|
subs []chan proto.IpcMessage
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// New creates a Manager from the given config.
|
|
|
|
|
func New(cfg Config) *Manager {
|
|
|
|
|
return &Manager{
|
|
|
|
|
networks: make(map[string]*Network),
|
|
|
|
|
cfg: cfg,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Join creates or rejoins a named network. Returns the network ID.
|
|
|
|
|
// If the network is already joined the existing ID is returned immediately.
|
2026-06-22 15:13:26 +02:00
|
|
|
// shareDir overrides the global default for this network; pass "" to use the default.
|
|
|
|
|
func (mgr *Manager) Join(name, shareDir string) (string, error) {
|
Multi-network foundation: netmgr, derived identities, additive IPC protocol
YAW/2 peer wire protocol is unchanged. Changes are local only.
internal/crypto:
- DeriveForNetwork(master, networkHash) — HKDF-SHA256 from master seed + network hash;
same master + same network always produces the same Ed25519 keypair (stable peer ID)
internal/proto:
- NetworkInfo type (network_id, network_name, local_peer)
- NetworkID field on IpcMessage (optional; commands default to first network when absent)
- Networks []NetworkInfo on state_snapshot (additive alongside existing local_peer)
- EvtNetworkJoined / EvtNetworkLeft events
internal/netmgr (new):
- Manager holds N independent Network contexts (derived identity, mesh, store, anchor)
- Join(name) creates context, derives identity, opens per-network DB, starts anchor client
- Leave(id) / LeaveAll() cancel contexts and close stores
- Resolve(netID) returns named network, or Default() when netID is empty (backward compat)
- Fan-out: Manager.Subscribe() receives tagged events from all networks
- Network IDs are the first 8 hex chars of SHA-256("yaw2-net:"+name) — stable and short
internal/ipc:
- Run(mgr, port) replaces Run(m *mesh.Mesh, port, anchorURL, joinFn)
- Commands without network_id route to mgr.Default() (backward compat)
- state_snapshot includes Networks array; local_peer/connected_peers still populated from first network
- generate_invite, get_file_list, send_message all respect network_id routing
cmd/daemon:
- Creates netmgr.Manager instead of mesh.Mesh directly
- --join and --share-dir pass through Config
- Auto-join via mgr.Join() before IPC starts
test-network.sh:
- Fix peer_name bash bug: $() with && inside triggers set -e; use if/elif/else instead
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 19:13:54 +02:00
|
|
|
netHash := hashNetName(name)
|
|
|
|
|
netID := netHash[:8]
|
|
|
|
|
|
|
|
|
|
mgr.mu.Lock()
|
|
|
|
|
if _, exists := mgr.networks[netID]; exists {
|
|
|
|
|
mgr.mu.Unlock()
|
|
|
|
|
return netID, nil
|
|
|
|
|
}
|
|
|
|
|
mgr.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
// Derive a network-specific identity.
|
|
|
|
|
netID_full := netID
|
|
|
|
|
derived, err := crypto.DeriveForNetwork(mgr.cfg.MasterIdentity, netHash)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", fmt.Errorf("derive identity for %q: %w", name, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Open per-network store.
|
|
|
|
|
dbPath := filepath.Join(mgr.cfg.StoreDir, "messages-"+netID_full+".db")
|
|
|
|
|
st, err := store.Open(dbPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return "", fmt.Errorf("open store for %q: %w", name, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m := mesh.New(derived, st)
|
2026-06-22 15:13:26 +02:00
|
|
|
// Per-network share dir takes precedence over the global default.
|
|
|
|
|
if shareDir != "" {
|
|
|
|
|
m.ShareDir = shareDir
|
|
|
|
|
} else if mgr.cfg.ShareDir != "" {
|
Multi-network foundation: netmgr, derived identities, additive IPC protocol
YAW/2 peer wire protocol is unchanged. Changes are local only.
internal/crypto:
- DeriveForNetwork(master, networkHash) — HKDF-SHA256 from master seed + network hash;
same master + same network always produces the same Ed25519 keypair (stable peer ID)
internal/proto:
- NetworkInfo type (network_id, network_name, local_peer)
- NetworkID field on IpcMessage (optional; commands default to first network when absent)
- Networks []NetworkInfo on state_snapshot (additive alongside existing local_peer)
- EvtNetworkJoined / EvtNetworkLeft events
internal/netmgr (new):
- Manager holds N independent Network contexts (derived identity, mesh, store, anchor)
- Join(name) creates context, derives identity, opens per-network DB, starts anchor client
- Leave(id) / LeaveAll() cancel contexts and close stores
- Resolve(netID) returns named network, or Default() when netID is empty (backward compat)
- Fan-out: Manager.Subscribe() receives tagged events from all networks
- Network IDs are the first 8 hex chars of SHA-256("yaw2-net:"+name) — stable and short
internal/ipc:
- Run(mgr, port) replaces Run(m *mesh.Mesh, port, anchorURL, joinFn)
- Commands without network_id route to mgr.Default() (backward compat)
- state_snapshot includes Networks array; local_peer/connected_peers still populated from first network
- generate_invite, get_file_list, send_message all respect network_id routing
cmd/daemon:
- Creates netmgr.Manager instead of mesh.Mesh directly
- --join and --share-dir pass through Config
- Auto-join via mgr.Join() before IPC starts
test-network.sh:
- Fix peer_name bash bug: $() with && inside triggers set -e; use if/elif/else instead
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 19:13:54 +02:00
|
|
|
m.ShareDir = mgr.cfg.ShareDir
|
|
|
|
|
}
|
2026-06-22 14:22:59 +02:00
|
|
|
m.DownloadDir = filepath.Join(mgr.cfg.StoreDir, "downloads-"+netID_full)
|
Multi-network foundation: netmgr, derived identities, additive IPC protocol
YAW/2 peer wire protocol is unchanged. Changes are local only.
internal/crypto:
- DeriveForNetwork(master, networkHash) — HKDF-SHA256 from master seed + network hash;
same master + same network always produces the same Ed25519 keypair (stable peer ID)
internal/proto:
- NetworkInfo type (network_id, network_name, local_peer)
- NetworkID field on IpcMessage (optional; commands default to first network when absent)
- Networks []NetworkInfo on state_snapshot (additive alongside existing local_peer)
- EvtNetworkJoined / EvtNetworkLeft events
internal/netmgr (new):
- Manager holds N independent Network contexts (derived identity, mesh, store, anchor)
- Join(name) creates context, derives identity, opens per-network DB, starts anchor client
- Leave(id) / LeaveAll() cancel contexts and close stores
- Resolve(netID) returns named network, or Default() when netID is empty (backward compat)
- Fan-out: Manager.Subscribe() receives tagged events from all networks
- Network IDs are the first 8 hex chars of SHA-256("yaw2-net:"+name) — stable and short
internal/ipc:
- Run(mgr, port) replaces Run(m *mesh.Mesh, port, anchorURL, joinFn)
- Commands without network_id route to mgr.Default() (backward compat)
- state_snapshot includes Networks array; local_peer/connected_peers still populated from first network
- generate_invite, get_file_list, send_message all respect network_id routing
cmd/daemon:
- Creates netmgr.Manager instead of mesh.Mesh directly
- --join and --share-dir pass through Config
- Auto-join via mgr.Join() before IPC starts
test-network.sh:
- Fix peer_name bash bug: $() with && inside triggers set -e; use if/elif/else instead
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 19:13:54 +02:00
|
|
|
|
|
|
|
|
// Forward all mesh events to the Manager's fan-out, tagging with network_id.
|
|
|
|
|
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: name,
|
|
|
|
|
Hash: netHash,
|
|
|
|
|
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 %q (id=%s peer=%s)", name, netID, derived.PeerID().Short())
|
|
|
|
|
|
|
|
|
|
if mgr.cfg.AnchorURL != "" {
|
|
|
|
|
go func() {
|
|
|
|
|
anchor.Run(ctx, mgr.cfg.AnchorURL, name, derived, m)
|
|
|
|
|
log.Printf("netmgr: left network %q", name)
|
|
|
|
|
}()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mgr.emit(proto.IpcMessage{
|
2026-06-22 15:13:26 +02:00
|
|
|
Type: proto.EvtNetworkJoined,
|
|
|
|
|
NetworkID: netID,
|
Multi-network foundation: netmgr, derived identities, additive IPC protocol
YAW/2 peer wire protocol is unchanged. Changes are local only.
internal/crypto:
- DeriveForNetwork(master, networkHash) — HKDF-SHA256 from master seed + network hash;
same master + same network always produces the same Ed25519 keypair (stable peer ID)
internal/proto:
- NetworkInfo type (network_id, network_name, local_peer)
- NetworkID field on IpcMessage (optional; commands default to first network when absent)
- Networks []NetworkInfo on state_snapshot (additive alongside existing local_peer)
- EvtNetworkJoined / EvtNetworkLeft events
internal/netmgr (new):
- Manager holds N independent Network contexts (derived identity, mesh, store, anchor)
- Join(name) creates context, derives identity, opens per-network DB, starts anchor client
- Leave(id) / LeaveAll() cancel contexts and close stores
- Resolve(netID) returns named network, or Default() when netID is empty (backward compat)
- Fan-out: Manager.Subscribe() receives tagged events from all networks
- Network IDs are the first 8 hex chars of SHA-256("yaw2-net:"+name) — stable and short
internal/ipc:
- Run(mgr, port) replaces Run(m *mesh.Mesh, port, anchorURL, joinFn)
- Commands without network_id route to mgr.Default() (backward compat)
- state_snapshot includes Networks array; local_peer/connected_peers still populated from first network
- generate_invite, get_file_list, send_message all respect network_id routing
cmd/daemon:
- Creates netmgr.Manager instead of mesh.Mesh directly
- --join and --share-dir pass through Config
- Auto-join via mgr.Join() before IPC starts
test-network.sh:
- Fix peer_name bash bug: $() with && inside triggers set -e; use if/elif/else instead
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 19:13:54 +02:00
|
|
|
NetworkName: name,
|
2026-06-22 15:13:26 +02:00
|
|
|
LocalPeer: peerPtr(derived.PeerInfo()),
|
|
|
|
|
ShareDir: m.ShareDir,
|
Multi-network foundation: netmgr, derived identities, additive IPC protocol
YAW/2 peer wire protocol is unchanged. Changes are local only.
internal/crypto:
- DeriveForNetwork(master, networkHash) — HKDF-SHA256 from master seed + network hash;
same master + same network always produces the same Ed25519 keypair (stable peer ID)
internal/proto:
- NetworkInfo type (network_id, network_name, local_peer)
- NetworkID field on IpcMessage (optional; commands default to first network when absent)
- Networks []NetworkInfo on state_snapshot (additive alongside existing local_peer)
- EvtNetworkJoined / EvtNetworkLeft events
internal/netmgr (new):
- Manager holds N independent Network contexts (derived identity, mesh, store, anchor)
- Join(name) creates context, derives identity, opens per-network DB, starts anchor client
- Leave(id) / LeaveAll() cancel contexts and close stores
- Resolve(netID) returns named network, or Default() when netID is empty (backward compat)
- Fan-out: Manager.Subscribe() receives tagged events from all networks
- Network IDs are the first 8 hex chars of SHA-256("yaw2-net:"+name) — stable and short
internal/ipc:
- Run(mgr, port) replaces Run(m *mesh.Mesh, port, anchorURL, joinFn)
- Commands without network_id route to mgr.Default() (backward compat)
- state_snapshot includes Networks array; local_peer/connected_peers still populated from first network
- generate_invite, get_file_list, send_message all respect network_id routing
cmd/daemon:
- Creates netmgr.Manager instead of mesh.Mesh directly
- --join and --share-dir pass through Config
- Auto-join via mgr.Join() before IPC starts
test-network.sh:
- Fix peer_name bash bug: $() with && inside triggers set -e; use if/elif/else instead
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 19:13:54 +02:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return netID, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 23:29:45 +02:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
Multi-network foundation: netmgr, derived identities, additive IPC protocol
YAW/2 peer wire protocol is unchanged. Changes are local only.
internal/crypto:
- DeriveForNetwork(master, networkHash) — HKDF-SHA256 from master seed + network hash;
same master + same network always produces the same Ed25519 keypair (stable peer ID)
internal/proto:
- NetworkInfo type (network_id, network_name, local_peer)
- NetworkID field on IpcMessage (optional; commands default to first network when absent)
- Networks []NetworkInfo on state_snapshot (additive alongside existing local_peer)
- EvtNetworkJoined / EvtNetworkLeft events
internal/netmgr (new):
- Manager holds N independent Network contexts (derived identity, mesh, store, anchor)
- Join(name) creates context, derives identity, opens per-network DB, starts anchor client
- Leave(id) / LeaveAll() cancel contexts and close stores
- Resolve(netID) returns named network, or Default() when netID is empty (backward compat)
- Fan-out: Manager.Subscribe() receives tagged events from all networks
- Network IDs are the first 8 hex chars of SHA-256("yaw2-net:"+name) — stable and short
internal/ipc:
- Run(mgr, port) replaces Run(m *mesh.Mesh, port, anchorURL, joinFn)
- Commands without network_id route to mgr.Default() (backward compat)
- state_snapshot includes Networks array; local_peer/connected_peers still populated from first network
- generate_invite, get_file_list, send_message all respect network_id routing
cmd/daemon:
- Creates netmgr.Manager instead of mesh.Mesh directly
- --join and --share-dir pass through Config
- Auto-join via mgr.Join() before IPC starts
test-network.sh:
- Fix peer_name bash bug: $() with && inside triggers set -e; use if/elif/else instead
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 19:13:54 +02:00
|
|
|
// Leave cancels a network context by ID. Closes its store.
|
|
|
|
|
func (mgr *Manager) Leave(netID string) {
|
|
|
|
|
mgr.mu.Lock()
|
|
|
|
|
net, ok := mgr.networks[netID]
|
|
|
|
|
if ok {
|
|
|
|
|
delete(mgr.networks, netID)
|
|
|
|
|
mgr.order = removeStr(mgr.order, netID)
|
|
|
|
|
}
|
|
|
|
|
mgr.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
net.cancel()
|
|
|
|
|
net.Store.Close()
|
|
|
|
|
net.Mesh.Unsubscribe(net.Mesh.Subscribe()) // drain + close subscription
|
|
|
|
|
log.Printf("netmgr: left network %q (id=%s)", net.Name, netID)
|
|
|
|
|
mgr.emit(proto.IpcMessage{Type: proto.EvtNetworkLeft, NetworkID: netID})
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 15:13:26 +02:00
|
|
|
// SetShareDir updates the share directory for an already-joined network.
|
|
|
|
|
// Changes take effect immediately for subsequent file-list requests and offers.
|
|
|
|
|
func (mgr *Manager) SetShareDir(netID, path string) bool {
|
|
|
|
|
mgr.mu.RLock()
|
|
|
|
|
net, ok := mgr.networks[netID]
|
|
|
|
|
mgr.mu.RUnlock()
|
|
|
|
|
if !ok {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
net.Mesh.ShareDir = path
|
|
|
|
|
log.Printf("netmgr: share dir for %q set to %q", net.Name, path)
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
Multi-network foundation: netmgr, derived identities, additive IPC protocol
YAW/2 peer wire protocol is unchanged. Changes are local only.
internal/crypto:
- DeriveForNetwork(master, networkHash) — HKDF-SHA256 from master seed + network hash;
same master + same network always produces the same Ed25519 keypair (stable peer ID)
internal/proto:
- NetworkInfo type (network_id, network_name, local_peer)
- NetworkID field on IpcMessage (optional; commands default to first network when absent)
- Networks []NetworkInfo on state_snapshot (additive alongside existing local_peer)
- EvtNetworkJoined / EvtNetworkLeft events
internal/netmgr (new):
- Manager holds N independent Network contexts (derived identity, mesh, store, anchor)
- Join(name) creates context, derives identity, opens per-network DB, starts anchor client
- Leave(id) / LeaveAll() cancel contexts and close stores
- Resolve(netID) returns named network, or Default() when netID is empty (backward compat)
- Fan-out: Manager.Subscribe() receives tagged events from all networks
- Network IDs are the first 8 hex chars of SHA-256("yaw2-net:"+name) — stable and short
internal/ipc:
- Run(mgr, port) replaces Run(m *mesh.Mesh, port, anchorURL, joinFn)
- Commands without network_id route to mgr.Default() (backward compat)
- state_snapshot includes Networks array; local_peer/connected_peers still populated from first network
- generate_invite, get_file_list, send_message all respect network_id routing
cmd/daemon:
- Creates netmgr.Manager instead of mesh.Mesh directly
- --join and --share-dir pass through Config
- Auto-join via mgr.Join() before IPC starts
test-network.sh:
- Fix peer_name bash bug: $() with && inside triggers set -e; use if/elif/else instead
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 19:13:54 +02:00
|
|
|
// LeaveAll leaves every joined network.
|
|
|
|
|
func (mgr *Manager) LeaveAll() {
|
|
|
|
|
mgr.mu.RLock()
|
|
|
|
|
ids := make([]string, len(mgr.order))
|
|
|
|
|
copy(ids, mgr.order)
|
|
|
|
|
mgr.mu.RUnlock()
|
|
|
|
|
for _, id := range ids {
|
|
|
|
|
mgr.Leave(id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get returns a network by ID.
|
|
|
|
|
func (mgr *Manager) Get(netID string) (*Network, bool) {
|
|
|
|
|
mgr.mu.RLock()
|
|
|
|
|
defer mgr.mu.RUnlock()
|
|
|
|
|
n, ok := mgr.networks[netID]
|
|
|
|
|
return n, ok
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Default returns the first joined network, or nil if none are joined.
|
|
|
|
|
// Used to route commands that carry no network_id (backward compat).
|
|
|
|
|
func (mgr *Manager) Default() *Network {
|
|
|
|
|
mgr.mu.RLock()
|
|
|
|
|
defer mgr.mu.RUnlock()
|
|
|
|
|
if len(mgr.order) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return mgr.networks[mgr.order[0]]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Resolve returns the network for netID, or the default if netID is empty.
|
|
|
|
|
func (mgr *Manager) Resolve(netID string) *Network {
|
|
|
|
|
if netID == "" {
|
|
|
|
|
return mgr.Default()
|
|
|
|
|
}
|
|
|
|
|
n, _ := mgr.Get(netID)
|
|
|
|
|
return n
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// All returns a snapshot of all joined networks in join order.
|
|
|
|
|
func (mgr *Manager) All() []*Network {
|
|
|
|
|
mgr.mu.RLock()
|
|
|
|
|
defer mgr.mu.RUnlock()
|
|
|
|
|
out := make([]*Network, 0, len(mgr.order))
|
|
|
|
|
for _, id := range mgr.order {
|
|
|
|
|
out = append(out, mgr.networks[id])
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// AnchorURL returns the configured anchor URL.
|
|
|
|
|
func (mgr *Manager) AnchorURL() string { return mgr.cfg.AnchorURL }
|
|
|
|
|
|
2026-06-22 21:44:48 +02:00
|
|
|
// MasterIdentity returns the master identity (not network-derived).
|
|
|
|
|
func (mgr *Manager) MasterIdentity() *crypto.Identity { return mgr.cfg.MasterIdentity }
|
|
|
|
|
|
Multi-network foundation: netmgr, derived identities, additive IPC protocol
YAW/2 peer wire protocol is unchanged. Changes are local only.
internal/crypto:
- DeriveForNetwork(master, networkHash) — HKDF-SHA256 from master seed + network hash;
same master + same network always produces the same Ed25519 keypair (stable peer ID)
internal/proto:
- NetworkInfo type (network_id, network_name, local_peer)
- NetworkID field on IpcMessage (optional; commands default to first network when absent)
- Networks []NetworkInfo on state_snapshot (additive alongside existing local_peer)
- EvtNetworkJoined / EvtNetworkLeft events
internal/netmgr (new):
- Manager holds N independent Network contexts (derived identity, mesh, store, anchor)
- Join(name) creates context, derives identity, opens per-network DB, starts anchor client
- Leave(id) / LeaveAll() cancel contexts and close stores
- Resolve(netID) returns named network, or Default() when netID is empty (backward compat)
- Fan-out: Manager.Subscribe() receives tagged events from all networks
- Network IDs are the first 8 hex chars of SHA-256("yaw2-net:"+name) — stable and short
internal/ipc:
- Run(mgr, port) replaces Run(m *mesh.Mesh, port, anchorURL, joinFn)
- Commands without network_id route to mgr.Default() (backward compat)
- state_snapshot includes Networks array; local_peer/connected_peers still populated from first network
- generate_invite, get_file_list, send_message all respect network_id routing
cmd/daemon:
- Creates netmgr.Manager instead of mesh.Mesh directly
- --join and --share-dir pass through Config
- Auto-join via mgr.Join() before IPC starts
test-network.sh:
- Fix peer_name bash bug: $() with && inside triggers set -e; use if/elif/else instead
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-21 19:13:54 +02:00
|
|
|
// Subscribe returns a channel that receives tagged events from all networks.
|
|
|
|
|
func (mgr *Manager) Subscribe() <-chan proto.IpcMessage {
|
|
|
|
|
ch := make(chan proto.IpcMessage, 128)
|
|
|
|
|
mgr.subsMu.Lock()
|
|
|
|
|
mgr.subs = append(mgr.subs, ch)
|
|
|
|
|
mgr.subsMu.Unlock()
|
|
|
|
|
return ch
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Unsubscribe removes and closes a subscription channel.
|
|
|
|
|
func (mgr *Manager) Unsubscribe(ch <-chan proto.IpcMessage) {
|
|
|
|
|
mgr.subsMu.Lock()
|
|
|
|
|
defer mgr.subsMu.Unlock()
|
|
|
|
|
for i, s := range mgr.subs {
|
|
|
|
|
if s == ch {
|
|
|
|
|
mgr.subs = append(mgr.subs[:i], mgr.subs[i+1:]...)
|
|
|
|
|
close(s)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (mgr *Manager) emit(msg proto.IpcMessage) {
|
|
|
|
|
mgr.subsMu.Lock()
|
|
|
|
|
defer mgr.subsMu.Unlock()
|
|
|
|
|
for _, ch := range mgr.subs {
|
|
|
|
|
select {
|
|
|
|
|
case ch <- msg:
|
|
|
|
|
default:
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
func hashNetName(name string) string {
|
|
|
|
|
h := sha256.Sum256([]byte("yaw2-net:" + name))
|
|
|
|
|
return hex.EncodeToString(h[:])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func removeStr(ss []string, s string) []string {
|
|
|
|
|
out := ss[:0]
|
|
|
|
|
for _, v := range ss {
|
|
|
|
|
if v != s {
|
|
|
|
|
out = append(out, v)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func peerPtr(p proto.PeerInfo) *proto.PeerInfo { return &p }
|