eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1// Package client is the E2EE member client layer: everything a headless
2// member process needs to join a space, hold its keys, decrypt the threads
3// it's in, and send — used by the eleven CLI (eleven/) today; the bot sidecar
4// (internal/bot) keeps its own copy of this transport and moves onto this
5// package next. Like every client, it is pure outbound: the private key in
6// Session never leaves the process, and the server only ever sees ciphertext
7// (CLAUDE.md, the one rule).
8package client
9
10import (
11 "encoding/json"
12 "fmt"
13 "os"
14 "path/filepath"
15)
16
17// Session is a member identity: the device token and ECDH keypair. The JSON
18// shape is exactly the Node sidecar's session.json (agent/config.mjs), so bot
19// session files and CLI session files are interchangeable — same identity,
20// same keys. Base is a CLI-only convenience (bots pass -base) and is omitted
21// when empty, so bot session files round-trip byte-compatibly.
22type Session struct {
23 Base string `json:"base,omitempty"`
24 Token string `json:"token"`
25 Name string `json:"name"`
26 UserPub string `json:"userPub"`
27 UserPriv string `json:"userPriv"`
28 // LabelPublished: this member's name has reached the roster (PublishLabel
29 // succeeded). CLI-only bookkeeping — publishing needs the instance key,
30 // which a fresh member is granted asynchronously, so the CLI retries on
31 // later runs until it lands.
32 LabelPublished bool `json:"labelPublished,omitempty"`
33 // SpaceName/Starred mirror the member's home-vault entry for this space
34 // (its saved display name and star) — CLI display bookkeeping, refreshed
35 // on each signin sync.
36 SpaceName string `json:"spaceName,omitempty"`
37 Starred bool `json:"starred,omitempty"`
38}
39
40// LoadSession reads a session file. A missing file returns (nil, nil) — "not
41// provisioned yet" is a normal state, not an error.
42func LoadSession(path string) (*Session, error) {
43 raw, err := os.ReadFile(path)
44 if os.IsNotExist(err) {
45 return nil, nil
46 }
47 if err != nil {
48 return nil, err
49 }
50 var s Session
51 if err := json.Unmarshal(raw, &s); err != nil {
52 return nil, fmt.Errorf("client: bad session file %s: %w", path, err)
53 }
54 if s.Token == "" || s.UserPriv == "" {
55 return nil, fmt.Errorf("client: session file %s is missing token or key", path)
56 }
57 return &s, nil
58}
59
60// Save persists the session. 0600 (and a 0700 parent, created if missing):
61// the file is the member's private key.
62func (s *Session) Save(path string) error {
63 if dir := filepath.Dir(path); dir != "." && dir != "/" {
64 if err := os.MkdirAll(dir, 0o700); err != nil {
65 return err
66 }
67 }
68 raw, err := json.MarshalIndent(s, "", " ")
69 if err != nil {
70 return err
71 }
72 return os.WriteFile(path, append(raw, '\n'), 0o600)
73}
74