feat(mobile): reconstruir+ampliar binding gomobile para paridad con la web
El wrapper mobile/unibus.go se habia perdido del repo (solo quedaba compilado
en el .aar del 5 jun). Se reconstruye y amplia con:
- Wallet BIP39 determinista: NewMnemonic, ValidateMnemonic, DeriveAndSaveIdentity.
Deriva la MISMA identidad que uniweb (web/src/wallet/derive.ts): PBKDF2-BIP39 ->
HKDF-SHA256(info unibus-sign-v1 / unibus-kex-v1) -> Ed25519 + X25519. Test de
paridad contra el vector de oro (mnemonica abandon...about -> sign_pub
34302746...b3c8) garantiza misma cuenta web<->movil byte a byte.
- Selector de salas: Session.ListMyRooms() -> JSON [{id,subject,mode,role}].
- Nombres legibles: Session.Directory() + Client.Directory()/EndpointID() nuevos
en pkg/client (GET /directory firmado).
- HasIdentity/SignPubAt para el onboarding.
Aditivo; build/vet/test del modulo verdes (incluido TestDeriveParityWithWeb).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,8 @@ require (
|
||||
github.com/nats-io/nats.go v1.49.0
|
||||
github.com/nats-io/nkeys v0.4.15
|
||||
github.com/oklog/ulid/v2 v2.1.0
|
||||
github.com/tyler-smith/go-bip39 v1.1.0
|
||||
golang.org/x/crypto v0.51.0
|
||||
golang.org/x/time v0.15.0
|
||||
modernc.org/sqlite v1.47.0
|
||||
)
|
||||
@@ -26,7 +28,6 @@ require (
|
||||
github.com/nats-io/nuid v1.0.1 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/crypto v0.51.0 // indirect
|
||||
golang.org/x/mobile v0.0.0-20260602190626-68735029466e // indirect
|
||||
golang.org/x/mod v0.36.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
|
||||
@@ -35,18 +35,26 @@ github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNs
|
||||
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8=
|
||||
github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/mobile v0.0.0-20260602190626-68735029466e h1:YxPXu/HWDTcSSrzSX+sCltsfcNCa/ZYVG43oslMouNU=
|
||||
golang.org/x/mobile v0.0.0-20260602190626-68735029466e/go.mod h1:ltIbhcRzKgwHa4ZxKJeiv0nyzcXUUYCqMyO0Y+vPmXw=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package mobile
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestDeriveParityWithWeb pins the Go wallet derivation to the TypeScript one
|
||||
// (web/src/wallet/derive.ts). The canonical BIP39 test mnemonic must derive to this
|
||||
// exact Ed25519 sign_pub — the same value the uniweb client showed for this phrase.
|
||||
// If this fails, web and mobile would derive different identities from the same seed
|
||||
// and the "same account on both devices" guarantee breaks.
|
||||
func TestDeriveParityWithWeb(t *testing.T) {
|
||||
const mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
const wantSignPub = "34302746268e7370d35940e1bcef8c0b1c13a857ea6209e6ecc6e9b3af06b3c8"
|
||||
|
||||
id, err := deriveIdentity(mnemonic)
|
||||
if err != nil {
|
||||
t.Fatalf("deriveIdentity: %v", err)
|
||||
}
|
||||
if got := hex.EncodeToString(id.SignPub); got != wantSignPub {
|
||||
t.Fatalf("sign_pub mismatch:\n got %s\n want %s", got, wantSignPub)
|
||||
}
|
||||
if len(id.SignPriv) != 64 || len(id.KexPub) != 32 || len(id.KexPriv) != 32 {
|
||||
t.Fatalf("bad key lengths: signPriv=%d kexPub=%d kexPriv=%d",
|
||||
len(id.SignPriv), len(id.KexPub), len(id.KexPriv))
|
||||
}
|
||||
// sign_priv is Go's ed25519 layout: seed || pub, so its tail must equal sign_pub.
|
||||
if got := hex.EncodeToString(id.SignPriv[32:]); got != wantSignPub {
|
||||
t.Fatalf("sign_priv tail != sign_pub:\n got %s\n want %s", got, wantSignPub)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMnemonicRoundTrip checks a freshly generated phrase validates and derives.
|
||||
func TestMnemonicRoundTrip(t *testing.T) {
|
||||
m, err := NewMnemonic()
|
||||
if err != nil {
|
||||
t.Fatalf("NewMnemonic: %v", err)
|
||||
}
|
||||
if !ValidateMnemonic(m) {
|
||||
t.Fatalf("generated mnemonic failed validation: %q", m)
|
||||
}
|
||||
if _, err := deriveIdentity(m); err != nil {
|
||||
t.Fatalf("deriveIdentity(fresh): %v", err)
|
||||
}
|
||||
if ValidateMnemonic("not a real mnemonic at all please") {
|
||||
t.Fatal("garbage phrase validated as a mnemonic")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
// Package mobile is the gomobile binding surface for the unibus client. It wraps
|
||||
// pkg/client behind a flat API (strings, []byte, JSON, a small interface) that
|
||||
// gobind can export to Kotlin/Swift, so an Android or iOS app speaks the real bus
|
||||
// protocol — NATS data plane, signed HTTP control plane and end-to-end crypto —
|
||||
// with no reimplementation of either the protocol or the cryptography.
|
||||
//
|
||||
// The wrapper is intentionally thin: every method delegates to pkg/client and only
|
||||
// reshapes types into something gobind understands (gomobile cannot export slices
|
||||
// of structs, so list results come back as JSON the caller decodes).
|
||||
package mobile
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
bip39 "github.com/tyler-smith/go-bip39"
|
||||
"golang.org/x/crypto/curve25519"
|
||||
"golang.org/x/crypto/hkdf"
|
||||
|
||||
cs "fn-registry/functions/cybersecurity"
|
||||
"github.com/enmanuel/unibus/pkg/client"
|
||||
"github.com/enmanuel/unibus/pkg/frame"
|
||||
"github.com/enmanuel/unibus/pkg/room"
|
||||
)
|
||||
|
||||
// HKDF domain-separation labels. These MUST stay identical to the uniweb wallet
|
||||
// derivation (web/src/wallet/derive.ts) so the same BIP39 mnemonic yields the same
|
||||
// identity (same sign_pub) on web and mobile — that shared identity is what lets a
|
||||
// user reach the same rooms from either device. Changing a label forks the wallet.
|
||||
const (
|
||||
infoSign = "unibus-sign-v1"
|
||||
infoKex = "unibus-kex-v1"
|
||||
)
|
||||
|
||||
// FrameListener receives decrypted messages from a subscribed room. OnFrame is
|
||||
// invoked on a NATS delivery goroutine; the app must hop to its UI thread before
|
||||
// touching UI state.
|
||||
type FrameListener interface {
|
||||
OnFrame(roomID, sender, msgID, text string)
|
||||
}
|
||||
|
||||
// NewMnemonic returns a fresh 12-word BIP39 recovery phrase (128 bits of entropy
|
||||
// from the system CSPRNG). Show it to the user exactly once and never persist it:
|
||||
// it is the only way to recover the identity on another device.
|
||||
func NewMnemonic() (string, error) {
|
||||
ent, err := bip39.NewEntropy(128)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return bip39.NewMnemonic(ent)
|
||||
}
|
||||
|
||||
// ValidateMnemonic reports whether m is a valid 12-word BIP39 phrase (every word in
|
||||
// the wordlist and a correct checksum). A phrase that fails this must not be used
|
||||
// to derive an identity.
|
||||
func ValidateMnemonic(m string) bool {
|
||||
return bip39.IsMnemonicValid(m)
|
||||
}
|
||||
|
||||
// hkdfExpand derives 32 bytes from ikm under an info label with an empty salt,
|
||||
// matching the web's hkdf(sha256, seed, undefined, info, 32).
|
||||
func hkdfExpand(ikm []byte, info string) ([]byte, error) {
|
||||
r := hkdf.New(sha256.New, ikm, nil, []byte(info))
|
||||
out := make([]byte, 32)
|
||||
if _, err := io.ReadFull(r, out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// deriveIdentity reproduces the uniweb wallet derivation byte for byte:
|
||||
//
|
||||
// seed = BIP39_seed(mnemonic) (PBKDF2, salt "mnemonic", 64 bytes)
|
||||
// signSeed = HKDF-SHA256(seed, salt="", info=sign, 32)
|
||||
// kexSeed = HKDF-SHA256(seed, salt="", info=kex, 32)
|
||||
// Ed25519 signing key from signSeed (priv = seed||pub, Go's layout)
|
||||
// X25519 key-exchange key from kexSeed
|
||||
//
|
||||
// The result is exactly a cs.Identity (sign_pub 32, sign_priv 64, kex_pub 32,
|
||||
// kex_priv 32), so the bus accepts the derived keys as a first-class peer.
|
||||
func deriveIdentity(mnemonic string) (cs.Identity, error) {
|
||||
if !bip39.IsMnemonicValid(mnemonic) {
|
||||
return cs.Identity{}, fmt.Errorf("invalid mnemonic")
|
||||
}
|
||||
seed := bip39.NewSeed(mnemonic, "") // 64-byte BIP39 seed (salt "mnemonic", no passphrase)
|
||||
signSeed, err := hkdfExpand(seed, infoSign)
|
||||
if err != nil {
|
||||
return cs.Identity{}, err
|
||||
}
|
||||
kexSeed, err := hkdfExpand(seed, infoKex)
|
||||
if err != nil {
|
||||
return cs.Identity{}, err
|
||||
}
|
||||
signPriv := ed25519.NewKeyFromSeed(signSeed) // 64 bytes = signSeed || sign_pub
|
||||
kexPub, err := curve25519.X25519(kexSeed, curve25519.Basepoint)
|
||||
if err != nil {
|
||||
return cs.Identity{}, err
|
||||
}
|
||||
return cs.Identity{
|
||||
SignPub: append([]byte(nil), signPriv[32:]...),
|
||||
SignPriv: append([]byte(nil), signPriv...),
|
||||
KexPub: kexPub,
|
||||
KexPriv: append([]byte(nil), kexSeed...),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeriveAndSaveIdentity derives the deterministic identity from a BIP39 mnemonic
|
||||
// and writes it to path, overwriting any previous identity on this device — which
|
||||
// is exactly what recovering on a new device (or after a reset) needs. It returns
|
||||
// the sign_pub hex so the UI can show which identity the phrase reconstructs. The
|
||||
// mnemonic itself is never stored; only the derived keypair is persisted (0600).
|
||||
func DeriveAndSaveIdentity(path, mnemonic string) (string, error) {
|
||||
id, err := deriveIdentity(mnemonic)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// WriteNewIdentity refuses to overwrite an existing file; recover/re-create must
|
||||
// replace the device's identity, so drop the old one first.
|
||||
_ = os.Remove(path)
|
||||
if err := client.WriteNewIdentity(path, id); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(id.SignPub), nil
|
||||
}
|
||||
|
||||
// HasIdentity reports whether a saved identity already exists at path, so the UI can
|
||||
// decide between the onboarding screen and going straight to connect.
|
||||
func HasIdentity(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// SignPubAt returns the sign_pub hex of the identity saved at path (for showing the
|
||||
// current account), or an error if none is saved.
|
||||
func SignPubAt(path string) (string, error) {
|
||||
id, err := client.LoadIdentity(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(id.SignPub), nil
|
||||
}
|
||||
|
||||
// GenerateIdentity creates a fresh random identity at path if none exists yet.
|
||||
// Retained for callers that want a throwaway local identity instead of a
|
||||
// recoverable BIP39 wallet.
|
||||
func GenerateIdentity(path string) error {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return nil
|
||||
}
|
||||
id, err := cs.GenerateIdentity()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return client.WriteNewIdentity(path, id)
|
||||
}
|
||||
|
||||
// Session is a connected unibus peer. Create it with NewSession and release it with
|
||||
// Close when the app stops.
|
||||
type Session struct {
|
||||
c *client.Client
|
||||
endpointID string
|
||||
}
|
||||
|
||||
// NewSession loads the identity at idPath and connects to the bus. natsURL is the
|
||||
// data plane (e.g. nats://host:4250) and ctrlURL is the control-plane HTTP endpoint
|
||||
// (e.g. http://host:8470).
|
||||
func NewSession(idPath, natsURL, ctrlURL string) (*Session, error) {
|
||||
id, err := client.LoadIdentity(idPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c, err := client.New(natsURL, ctrlURL, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Session{c: c, endpointID: frame.EndpointID(id.SignPub)}, nil
|
||||
}
|
||||
|
||||
// Close disconnects the peer from the bus.
|
||||
func (s *Session) Close() error { return s.c.Close() }
|
||||
|
||||
// EndpointID returns this peer's stable endpoint id (the sender stamped on frames).
|
||||
func (s *Session) EndpointID() string { return s.endpointID }
|
||||
|
||||
// CreateRoom opens a room on the given subject. mode is "matrix" for the encrypted,
|
||||
// persisted and signed policy, or "nats" for plain cleartext. Returns the room id
|
||||
// used by Join, Publish and Subscribe.
|
||||
func (s *Session) CreateRoom(subject, mode string) (string, error) {
|
||||
p := room.ModeNATS
|
||||
if mode == "matrix" {
|
||||
p = room.ModeMatrix
|
||||
}
|
||||
return s.c.CreateRoom(subject, p)
|
||||
}
|
||||
|
||||
// Join prepares the session to publish to and receive from a room, fetching the
|
||||
// room key when the room is encrypted.
|
||||
func (s *Session) Join(roomID string) error { return s.c.Join(roomID) }
|
||||
|
||||
// Publish sends a UTF-8 text message to the room.
|
||||
func (s *Session) Publish(roomID, text string) error { return s.c.Publish(roomID, []byte(text)) }
|
||||
|
||||
// Subscribe streams decrypted messages of the room to the listener until the
|
||||
// session is closed. For persisted (matrix) rooms this also replays the room's
|
||||
// history (JetStream DeliverAll) before live messages, so opening a room shows past
|
||||
// messages — the same behaviour as the web client.
|
||||
func (s *Session) Subscribe(roomID string, l FrameListener) error {
|
||||
_, err := s.c.Subscribe(roomID, func(f frame.Frame, plaintext []byte) {
|
||||
l.OnFrame(roomID, f.Sender, f.MsgID, string(plaintext))
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// ListMyRooms returns every room this peer is a member of, as a JSON array of
|
||||
// objects {id, subject, mode, role} where mode is "matrix" or "nats". Returned as
|
||||
// JSON because gomobile cannot export a slice of structs.
|
||||
func (s *Session) ListMyRooms() (string, error) {
|
||||
rooms, err := s.c.ListMyRooms()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
type roomJSON struct {
|
||||
ID string `json:"id"`
|
||||
Subject string `json:"subject"`
|
||||
Mode string `json:"mode"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
out := make([]roomJSON, 0, len(rooms))
|
||||
for _, r := range rooms {
|
||||
mode := "nats"
|
||||
if r.Policy.Encrypt {
|
||||
mode = "matrix"
|
||||
}
|
||||
out = append(out, roomJSON{ID: r.RoomID, Subject: r.Subject, Mode: mode, Role: r.Role})
|
||||
}
|
||||
b, err := json.Marshal(out)
|
||||
return string(b), err
|
||||
}
|
||||
|
||||
// Directory returns the cluster member directory as a JSON array of objects
|
||||
// {sign_pub, endpoint, handle, role}, so the UI can map a frame's endpoint id to a
|
||||
// readable handle. Returns "[]" semantics via JSON; callers degrade to short ids if
|
||||
// a handle is missing.
|
||||
func (s *Session) Directory() (string, error) {
|
||||
entries, err := s.c.Directory()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
type dirJSON struct {
|
||||
SignPub string `json:"sign_pub"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Handle string `json:"handle"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
out := make([]dirJSON, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
out = append(out, dirJSON{SignPub: e.SignPub, Endpoint: e.Endpoint, Handle: e.Handle, Role: e.Role})
|
||||
}
|
||||
b, err := json.Marshal(out)
|
||||
return string(b), err
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package client
|
||||
|
||||
// This file exposes two read helpers the mobile binding needs but that the core
|
||||
// client did not surface yet: the peer's own endpoint id, and the cluster member
|
||||
// directory (endpoint id -> human handle). Both are additive and read-only.
|
||||
|
||||
// EndpointID returns this peer's stable endpoint id, base64url(sha256(signPub)),
|
||||
// the value the bus stamps as the sender of every frame this peer publishes.
|
||||
func (c *Client) EndpointID() string { return c.endpoint }
|
||||
|
||||
// DirectoryEntry maps a member's stable endpoint id to a human handle, as served
|
||||
// by the control plane's GET /directory. A client uses it to render readable names
|
||||
// instead of raw endpoint ids on incoming frames.
|
||||
type DirectoryEntry struct {
|
||||
SignPub string // 64-hex Ed25519 public key
|
||||
Endpoint string // base64url-nopad, == EndpointID(signPub)
|
||||
Handle string
|
||||
Role string
|
||||
}
|
||||
|
||||
type directoryMemberWire struct {
|
||||
SignPub string `json:"sign_pub"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Handle string `json:"handle"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
|
||||
type directoryResp struct {
|
||||
Members []directoryMemberWire `json:"members"`
|
||||
}
|
||||
|
||||
// Directory fetches the cluster-wide member directory (endpoint id -> handle). Any
|
||||
// active user may read it; the request is signed like every other control-plane
|
||||
// call. Returns the active members only (the server filters to status=active).
|
||||
func (c *Client) Directory() ([]DirectoryEntry, error) {
|
||||
var resp directoryResp
|
||||
if err := c.doJSON("GET", "/directory", nil, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]DirectoryEntry, 0, len(resp.Members))
|
||||
for _, m := range resp.Members {
|
||||
out = append(out, DirectoryEntry{
|
||||
SignPub: m.SignPub,
|
||||
Endpoint: m.Endpoint,
|
||||
Handle: m.Handle,
|
||||
Role: m.Role,
|
||||
})
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
Reference in New Issue
Block a user