cloud

package
v0.11.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 22, 2026 License: MIT Imports: 36 Imported by: 0

Documentation

Overview

auth.go holds the shared login/logout helpers used by both the CLI (`jcode login` / `jcode logout`) and the web API (POST /api/cloud/login, /api/cloud/logout) so the two entry points can never drift apart.

capabilities.go builds the device-capabilities mirror (M12): the compose facets the local control plane can offer a remotely-started session — projects (from the session index), models (from config + the model registry, mirroring GET /api/models), the supported reasoning-effort levels, and the available slash commands (from GET /api/slash-commands). The connector reports it as the top-level `capabilities` field of every sessions upsert; the orchestrator stores it in devices.capabilities.

connector.go implements the jcloud relay connector: the local jcode poses as an always-on runner toward the cloud, over purely outbound connections (long poll + POSTs). It forwards downlink commands to the local web control plane (127.0.0.1) and pumps local WS events uplink. Every failure is logged as a warning — the connector must never affect the local web server.

Package cloud implements the jcloud (device relay) client side: device-code login (RFC 8628), device registration, and the on-disk device credentials. See cloud/docs/17-jcode-device-relay.md §3 for the interface contract.

crypto.go implements the M5 E2E encryption layer (定稿契约,勿改 wire 格式):

  • CEK: one 32-byte AES-256-GCM key per account, stored base64 in ~/.jcode/cloud.json (`cek`, generation in `key_gen`). Lazily generated on first need (connector start / `jcode cloud` commands), atomically written back, cached in-process.
  • Envelope: {"enc":"aes-256-gcm","key_gen":N,"nonce":"<b64 12B>","ct":"<b64>"}. Sealed fields: uplink events/ephemeral payload, sessions upsert meta, ack result; downlink command payload. Grey rule: a JSON object payload with a string `enc` field is treated as an envelope and decrypted; anything else is read as plaintext (M3/M4 compatibility).
  • Pairing wrap (ECIES/P-256): ephemeral P-256 key pair → ECDH(ephemeral, requester pubkey) → HKDF-SHA256(shared, salt=nil, info="jcode-device-cek") → 32B wrap key → AES-256-GCM over {"cek":"<b64>","key_gen":N}.
  • Recovery: the CEK doubles as 256 bits of BIP39 entropy → 24-word phrase.

events.go is the connector's uplink event path: it subscribes to the local web server's WS event stream (/api/ws), classifies each event as durable or ephemeral, assigns per-session seq numbers, and uploads — durable events in batches (persisted by the orchestrator for offline replay), ephemeral events fire-and-forget (SSE fanout only).

inbox.go lands non-image chat attachments (M12) at the fixed per-session location ~/.jcode/inbox/<session_id>/<filename>. The message text then references them ("[附件] name → path") so the agent can read them with its file tools. Attachment bytes travel inside the E2E envelope, so they are only ever plaintext on-device.

pairing_inbox.go handles pairing.request downlink commands (M11-W1): a request carrying an offer_id comes from a QR-code claim and is approved automatically (scan-to-pair); any other request is parked in an in-memory pending inbox until the user approves or denies it from the web UI (the CLI `jcode cloud approve|deny` path keeps working against the orchestrator directly). The inbox lives on the Connector because pairings only arrive through its poll loop; the Supervisor delegates the web endpoints to it.

pairings.go implements the device-side pairing approval endpoints (M5 task book, device-token authenticated, under /internal/v1/device):

GET  /internal/v1/device/pairings?status=pending   → {pairings:[...]}
GET  /internal/v1/device/pairings/{id}             → pairing (incl. pubkey)
POST /internal/v1/device/pairings/{id}/respond     → {approve, wrap?}

The pairing requester (console browser / mobile) generated a P-256 key pair and sent its SPKI DER base64 as `pubkey`; on approval the device wraps the CEK for that key (see crypto.go WrapCEK) and returns it via respond.

relay.go implements the jcode ↔ orchestrator device-relay protocol (cloud/docs/17-jcode-device-relay.md §4): long-poll command delivery, command acks, heartbeats, session-index upserts and event uploads.

Payload fields are opaque to the server: since M5 they are E2E envelopes (see crypto.go) whenever the device's CEK cipher is active, plaintext during the pre-CEK grey period. Sealing/opening happens in the Connector (sealUplink / openDownlink), not in these transport helpers.

sessions.go mirrors the local session index (~/.jcode/sessions/session.json) to the cloud: one full upsert at startup, then an upsert whenever the index file changes (detected by mtime polling — deliberately no fsnotify). The upsert response carries each session's last_seq, which seeds the event pump's per-session seq allocator (续号).

supervisor.go owns the cloud relay connector lifecycle so the relay can be stopped/started without a process restart (the settings auto_connect toggle hot-applies), and exposes the live Status served at GET /api/cloud/status.

sync_store.go persists the M19 per-session cloud-sync opt-in at ~/.jcode/cloud-sessions.json (0600): a flat {session_id: true|false} map. A session WITHOUT an entry is "unset" and never syncs (default OFF) — the global cloud.sync_default config is consulted only once, when the web layer stamps a brand-new session, so flipping the default never retroactively changes existing sessions.

The web API and the relay connector hold separate SyncStore instances on the same file; both transparently re-read it when its mtime/size changes, so a toggle via the API takes effect in the connector without any explicit wiring (event filtering on the next event, metadata upsert on the next session-sync tick).

Index

Constants

View Source
const (
	StateOffline    = "offline"
	StateConnecting = "connecting"
	StateOnline     = "online"
	StateError      = "error"
)

Connector states surfaced via Status (supervisor → GET /api/cloud/status).

View Source
const DefaultCloudURL = "https://cloud.j-code.net"

DefaultCloudURL is the public jcloud orchestrator address.

Variables

View Source
var (
	// ErrAuthorizationDenied is returned when the user denies the user_code on
	// the verification page.
	ErrAuthorizationDenied = errors.New("authorization denied by user")
	// ErrDeviceCodeExpired is returned when the device_code expires before the
	// user authorizes, or when the overall expiry deadline passes while polling.
	ErrDeviceCodeExpired = errors.New("device code expired")
)

Sentinel errors for terminal device-token polling outcomes.

View Source
var ErrUnknownPairing = errors.New("no such pending pairing")

ErrUnknownPairing is returned by ApprovePairing/DenyPairing when the id is not in the connector's pending inbox (already handled, or never received).

Functions

func CEKFromPhrase

func CEKFromPhrase(phrase string) ([]byte, error)

CEKFromPhrase decodes a 24-word BIP39 recovery phrase back into the CEK.

func CEKToPhrase

func CEKToPhrase(cek []byte) (string, error)

CEKToPhrase encodes a 32-byte CEK as its 24-word BIP39 recovery phrase.

func CredentialsPath

func CredentialsPath() (string, error)

CredentialsPath returns the full path to the credentials file (~/.jcode/cloud.json).

func DeleteCredentials

func DeleteCredentials() error

DeleteCredentials forgets the complete local device identity, including secrets in the system keyring. A missing file/key is not an error.

func FingerprintHash

func FingerprintHash(source string) string

FingerprintHash maps a fingerprint source to the ONLY form that ever leaves the machine: sha256 hex with a domain separator. The raw hardware id is never sent to the orchestrator.

func Forget

func Forget(ctx context.Context, warnf func(format string, args ...any)) error

Forget signs out and removes the full local identity. This is intentionally separate from Logout because forgetting keys requires other clients to pair with the device again.

func GenerateCEK

func GenerateCEK() ([]byte, error)

GenerateCEK returns a fresh random 32-byte CEK.

func GenerateIdentityKeyPair

func GenerateIdentityKeyPair() (publicKey, privateKey string, err error)

GenerateIdentityKeyPair creates a fresh X25519 device identity key pair and returns both keys base64-encoded (standard encoding, raw 32-byte keys).

func HardwareFingerprintSource

func HardwareFingerprintSource() string

HardwareFingerprintSource returns the OS-level machine id ("" when unavailable) — exported for callers (login --status, the connector) that must NOT generate a fresh fallback random on the spot.

func IsEnvelope

func IsEnvelope(raw json.RawMessage) bool

IsEnvelope applies the grey-scale detection rule: raw is a JSON object with a string `enc` field.

func Logout

func Logout(ctx context.Context, warnf func(format string, args ...any)) error

Logout signs the device out of jcloud while preserving its encryption identity. Only the revocable access token is cleared; the device key pair, CEK and fingerprint remain in the system keyring so the next login can resume without breaking paired browsers/mobile clients.

func ResetCEKCache

func ResetCEKCache()

ResetCEKCache clears the in-process CEK cache. Called on logout and by tests that swap the credentials file.

func ResolveFingerprintSource

func ResolveFingerprintSource() (string, error)

ResolveFingerprintSource returns the stable fingerprint source for this machine: the persisted cloud.json value if present (stable by definition), else the OS machine id, else a freshly generated fallback ("fallback:<hostname>:<random16B>").

The fallback is stable only once persisted: the caller MUST write the returned source into Credentials.Fingerprint when it saves the credentials file (both login paths do). Saving a hardware-derived source too is deliberate: it pins the identity against a later ioreg/machine-id failure.

func SaveCredentials

func SaveCredentials(creds *Credentials) error

SaveCredentials writes credentials atomically to ~/.jcode/cloud.json with owner-only permissions (file 0600, directory 0700), mirroring the pattern used by config.saveConfig.

func ShouldConnect

func ShouldConnect(autoConnect bool, creds *Credentials) bool

ShouldConnect reports whether the relay connector should start: the device must be logged in (creds present with a token) and auto_connect must not be explicitly disabled. Kept pure so the startup gate is unit-testable.

func SyncStorePath

func SyncStorePath() (string, error)

SyncStorePath returns the default store path (~/.jcode/cloud-sessions.json).

func UnwrapCEK

func UnwrapCEK(wrap *CEKWrap, requesterPriv *ecdh.PrivateKey) (cek []byte, keyGen int, err error)

UnwrapCEK reverses WrapCEK with the requester's P-256 private key. The device side never calls this in production — it exists for the requester side and for round-trip tests.

func UpdateConfigCloud

func UpdateConfigCloud(url string, enabled bool) error

UpdateConfigCloud sets config.cloud while preserving the stored url (when the url argument is empty, i.e. logout) and the user's auto_connect/e2ee preferences. Login/logout must not require a fully configured provider set, so a LoadConfig failure falls back to a best-effort raw read of the file (unknown fields may be dropped in that case).

func ValidateCloudURL

func ValidateCloudURL(raw string) (string, error)

ValidateCloudURL normalizes a --cloud flag value and enforces the scheme policy from docs/17 §3.3: https everywhere, except that localhost / 127.0.0.1 / [::1] (any port) may use http for development. The returned URL has no trailing slash.

Types

type APIError

type APIError struct {
	StatusCode int    `json:"-"`
	Code       string `json:"code"`
	Message    string `json:"message"`
}

APIError is the orchestrator's error envelope {error:{code,message}}.

func (*APIError) Error

func (e *APIError) Error() string

type Backoff

type Backoff struct {
	Min time.Duration
	Max time.Duration
	// contains filtered or unexported fields
}

Backoff is a thread-safe exponential backoff with a cap, used by every connector retry loop (register, poll, WS reconnect). It is injectable so tests can shrink the delays.

func NewBackoff

func NewBackoff(min, max time.Duration) *Backoff

NewBackoff returns a Backoff starting at min and capped at max.

func (*Backoff) Next

func (b *Backoff) Next() time.Duration

Next returns the current delay and doubles it (capped at Max).

func (*Backoff) Reset

func (b *Backoff) Reset()

Reset clears the backoff after a success.

func (*Backoff) Wait

func (b *Backoff) Wait(ctx context.Context) error

Wait sleeps for the next backoff delay or until ctx is cancelled (in which case it returns ctx.Err()).

type CEKWrap

type CEKWrap struct {
	EphemeralPubKey string `json:"ephemeral_pubkey"` // base64 SPKI DER, P-256
	Nonce           string `json:"nonce"`            // base64, 12 bytes
	CT              string `json:"ct"`               // base64 ciphertext
}

CEKWrap is the ECIES-wrapped CEK sent back to an approved pairing requester.

func WrapCEK

func WrapCEK(requesterPubKeyB64 string, cek []byte, keyGen int) (*CEKWrap, error)

WrapCEK encrypts the CEK for a pairing requester: requesterPubKeyB64 is the requester's P-256 public key (base64 SPKI DER, as generated by WebCrypto).

type CapabilityModel

type CapabilityModel struct {
	Provider string `json:"provider"`
	ID       string `json:"id"`
	Label    string `json:"label"`
}

CapabilityModel is one selectable model of a configured provider.

type CapabilityProject

type CapabilityProject struct {
	Path string `json:"path"`
	Name string `json:"name"`
}

CapabilityProject is one known project directory.

type CapabilitySlashCommand

type CapabilitySlashCommand struct {
	Slash       string `json:"slash"`
	Description string `json:"description"`
	Type        string `json:"type"` // "skill" | "flow"
}

CapabilitySlashCommand mirrors one item of the web control plane's GET /api/slash-commands response (skill- and workflow-provided commands).

type Client

type Client struct {
	BaseURL    string
	HTTPClient *http.Client
}

Client talks to the jcloud orchestrator's device-auth and internal device endpoints.

func NewClient

func NewClient(baseURL string) *Client

NewClient returns a Client for baseURL (already validated). A nil HTTPClient means a default one with a 30s timeout.

func (*Client) AckCommand

func (c *Client) AckCommand(ctx context.Context, token, id string, ack CommandAck) error

AckCommand reports a command's execution result: POST /internal/v1/device/commands/{id}/ack.

func (*Client) GetPairing

func (c *Client) GetPairing(ctx context.Context, token, id string) (*Pairing, error)

GetPairing fetches one pairing request (including the requester pubkey): GET /internal/v1/device/pairings/{id}.

func (*Client) Heartbeat

func (c *Client) Heartbeat(ctx context.Context, token string) error

Heartbeat keeps the device marked online: POST /internal/v1/device/heartbeat (empty body).

func (*Client) ListPairings

func (c *Client) ListPairings(ctx context.Context, token, status string) ([]Pairing, error)

ListPairings queries pairing requests, optionally filtered by status (empty = all): GET /internal/v1/device/pairings[?status=pending].

func (*Client) PollCommands

func (c *Client) PollCommands(ctx context.Context, token string, wait time.Duration) (cmds []DeviceCommand, ok bool, err error)

PollCommands long-polls the downlink queue: GET /internal/v1/device/poll?wait=<wait>. ok is false on a 204 (no commands), in which case the caller should poll again immediately.

func (*Client) PollDeviceToken

func (c *Client) PollDeviceToken(ctx context.Context, deviceCode, fingerprint string) (*DeviceTokenResponse, error)

PollDeviceToken makes a single token poll attempt: POST /auth/device/token. fingerprint is the sha256 machine-fingerprint hash (M16; "" for none) — the orchestrator uses it to dedup a re-login onto the existing devices row. Pending authorization surfaces as an *APIError with Code "authorization_pending" (or "slow_down"); terminal failures use "access_denied" / "expired_token".

func (*Client) PollForToken

func (c *Client) PollForToken(ctx context.Context, deviceCode, fingerprint string, interval, expiresIn time.Duration) (*DeviceTokenResponse, error)

PollForToken polls POST /auth/device/token every interval until the user authorizes, the deadline (now+expiresIn) passes, or ctx is cancelled. Zero interval/expiresIn fall back to RFC 8628 defaults. fingerprint rides every poll (M16, see PollDeviceToken).

func (*Client) RegisterDevice

func (c *Client) RegisterDevice(ctx context.Context, token string, req RegisterDeviceRequest) error

RegisterDevice registers (or refreshes) this device with the orchestrator: POST /internal/v1/device/register (Bearer device token).

func (*Client) RequestDeviceCode

func (c *Client) RequestDeviceCode(ctx context.Context, clientName string) (*DeviceCodeResponse, error)

RequestDeviceCode starts the device authorization flow (RFC 8628 §3.1): POST {url}/auth/device/code.

func (*Client) RespondPairing

func (c *Client) RespondPairing(ctx context.Context, token, id string, approve bool, wrap *CEKWrap) error

RespondPairing approves or denies a pairing request: POST /internal/v1/device/pairings/{id}/respond. wrap carries the ECIES- wrapped CEK and is required on approval, nil on denial.

func (*Client) RevokeDevice

func (c *Client) RevokeDevice(ctx context.Context, token string) error

RevokeDevice asks the orchestrator to revoke this device's token: POST /internal/v1/device/revoke. A 404 (endpoint not deployed yet on the server side) is treated as success — local cleanup proceeds regardless.

func (*Client) SendEphemeral

func (c *Client) SendEphemeral(ctx context.Context, token, sid, kind string, payload json.RawMessage) error

SendEphemeral forwards one ephemeral (token-level) event: POST /internal/v1/device/sessions/{sid}/ephemeral. Fire-and-forget — failures are dropped by the caller, never retried.

func (*Client) UploadEvents

func (c *Client) UploadEvents(ctx context.Context, token, sid string, events []EventUpload) (*EventsUploadResponse, error)

UploadEvents appends a batch of durable events: POST /internal/v1/device/sessions/{sid}/events. Idempotent — the server skips (sid, seq) pairs it already holds and reports them as conflicted.

func (*Client) UpsertSessions

func (c *Client) UpsertSessions(ctx context.Context, token string, sessions []SessionUpsert, capabilities json.RawMessage) (*SessionsUpsertResponse, error)

UpsertSessions mirrors the local session index to the cloud: POST /internal/v1/device/sessions. The response carries each session's last_seq for event-pump seq resumption. capabilities is the M12 device-capabilities mirror (DeviceCapabilities JSON, sealed when the CEK cipher is active), stored by the orchestrator in devices.capabilities; nil omits the field.

type CommandAck

type CommandAck struct {
	Status string `json:"status"` // "ok" | "error"
	Result any    `json:"result,omitempty"`
}

CommandAck is the uplink execution receipt for one command.

type Connector

type Connector struct {
	// contains filtered or unexported fields
}

Connector is the cloud relay client. Construct with NewConnector, then Run.

func NewConnector

func NewConnector(cfg ConnectorConfig) *Connector

NewConnector builds a Connector from cfg.

func (*Connector) ApprovePairing

func (c *Connector) ApprovePairing(ctx context.Context, id string) error

ApprovePairing wraps the CEK for a pending requester and responds to the orchestrator (web endpoint path; manual approval).

func (*Connector) DenyPairing

func (c *Connector) DenyPairing(ctx context.Context, id string) error

DenyPairing responds denial to the orchestrator for a pending request.

func (*Connector) LastPaired

func (c *Connector) LastPaired() (PairedInfo, bool)

LastPaired reports the most recent approval, ok=false when none happened since the connector started.

func (*Connector) PendingPairings

func (c *Connector) PendingPairings() []PendingPairing

PendingPairings snapshots the pending inbox (oldest first).

func (*Connector) Run

func (c *Connector) Run(ctx context.Context)

Run starts the connector and blocks until ctx is cancelled (web server shutdown). It never returns an error — all failures are logged as warnings.

func (*Connector) Status

func (c *Connector) Status() (state string, lastErr string)

Status returns the current connection state and the last error message (empty when none). Safe to call from any goroutine, including before Run.

type ConnectorConfig

type ConnectorConfig struct {
	CloudURL    string       // orchestrator base URL (config.cloud.url, else credentials)
	Credentials *Credentials // device identity from ~/.jcode/cloud.json
	LocalBase   string       // local web control plane, e.g. http://127.0.0.1:8080
	LocalToken  string       // web auth token (empty when the server doesn't require auth)
	Version     string       // jcode version reported at register

	// Optional knobs (tests inject these; zero values use defaults).
	HTTPClient        *http.Client
	Hostname          string
	HeartbeatInterval time.Duration
	PollWait          time.Duration
	IndexPollInterval time.Duration
	BatchWindow       time.Duration
	BatchMax          int
	Backoff           *Backoff
	// IndexPathFn / ListSessionsFn default to the real session index; tests
	// override them to point at a temp dir.
	IndexPathFn    func() (string, error)
	ListSessionsFn func() (map[string][]session.SessionMeta, error)

	// InboxDir is the root under which chat attachments land
	// (<InboxDir>/<session_id>/<filename>). Empty → ~/.jcode/inbox.
	InboxDir string
	// ModelCapabilitiesFn overrides the model/effort facet of the capabilities
	// mirror (default: config + model registry); tests inject a fixed list.
	ModelCapabilitiesFn func() ([]CapabilityModel, []string, error)
	// SlashCommandsFn overrides the slash-commands facet of the capabilities
	// mirror (default: GET /api/slash-commands on the local control plane);
	// tests inject a fixed list.
	SlashCommandsFn func() ([]CapabilitySlashCommand, error)

	// Cipher, when non-nil, seals uplink payloads (events/ephemeral payload,
	// sessions meta, ack result) and opens downlink command payloads. Nil
	// means plaintext uplink (pre-CEK grey period; see crypto.go). Run lazily
	// initializes it from ~/.jcode/cloud.json when left nil; tests inject one
	// explicitly (or set CipherDisabled to force the plaintext path).
	Cipher         *EnvelopeCipher
	CipherDisabled bool

	// SyncStore is the M19 per-session cloud-sync gate (~/.jcode/cloud-sessions.json):
	// only sessions with an explicit opt-in are upserted / have their events
	// uploaded. Nil lazily loads the default path on first use; a load failure
	// fails CLOSED (nothing syncs). Tests inject a temp-file store.
	SyncStore *SyncStore
}

ConnectorConfig carries everything the connector needs. The zero durations fall back to production defaults; tests inject short ones.

type Credentials

type Credentials struct {
	CloudURL    string `json:"cloud_url"`
	DeviceID    string `json:"device_id"`
	DeviceToken string `json:"device_token"`
	DeviceName  string `json:"device_name"`
	// PublicKey / PrivateKey are the X25519 device identity key pair, base64
	// (standard encoding) encoded raw keys. The public key is registered with
	// the orchestrator for later E2E key exchange (CEK wrapping).
	PublicKey  string `json:"public_key"`
	PrivateKey string `json:"private_key"`
	// KeyGen is the current CEK generation this device holds.
	KeyGen int `json:"key_gen"`
	// CEK is the account-level AES-256-GCM content encryption key, base64.
	// Empty means "not initialized yet" (pre-M5 credentials file); it is
	// lazily generated on first need (see crypto.go EnsureCEK).
	CEK string `json:"cek,omitempty"`
	// Fingerprint is the stable machine fingerprint SOURCE (M16): the OS
	// machine id, or a "fallback:<hostname>:<random>" string generated once.
	// It never leaves the machine — only its sha256 (FingerprintHash) is sent
	// to the orchestrator for login dedup. Empty on pre-M16 files; resolved
	// on the fly then (see ResolveFingerprintSource).
	Fingerprint string `json:"fingerprint,omitempty"`
}

Credentials is the device identity. Non-sensitive metadata is persisted at ~/.jcode/cloud.json; DeviceToken, PrivateKey and CEK live in the operating system keyring. A missing metadata file means "not configured".

func LoadCredentials

func LoadCredentials() (*Credentials, error)

LoadCredentials reads ~/.jcode/cloud.json. A missing file is not an error: it returns (nil, nil) to mean "not logged in".

type DeviceCapabilities

type DeviceCapabilities struct {
	Projects      []CapabilityProject      `json:"projects"`
	Models        []CapabilityModel        `json:"models"`
	CurrentModel  *CapabilityModel         `json:"current_model,omitempty"`
	Efforts       []string                 `json:"efforts"`
	SlashCommands []CapabilitySlashCommand `json:"slash_commands"`
}

DeviceCapabilities is the capabilities payload stored by the orchestrator and consumed by the console/mobile compose UI.

type DeviceCodeResponse

type DeviceCodeResponse struct {
	DeviceCode      string `json:"device_code"`
	UserCode        string `json:"user_code"`
	VerificationURI string `json:"verification_uri"`
	ExpiresIn       int    `json:"expires_in"` // seconds
	Interval        int    `json:"interval"`   // seconds
}

DeviceCodeResponse is the answer of POST /auth/device/code.

type DeviceCommand

type DeviceCommand struct {
	ID        string          `json:"id"`
	Kind      string          `json:"kind"` // chat.send | chat.stop | approval.respond | …
	SessionID string          `json:"session_id,omitempty"`
	Payload   json.RawMessage `json:"payload,omitempty"`
}

DeviceCommand is one downlink command delivered by the poll endpoint.

type DeviceTokenResponse

type DeviceTokenResponse struct {
	AccessToken string `json:"access_token"`
	TokenType   string `json:"token_type"`
	DeviceID    string `json:"device_id"`
	// Deduped is true when the orchestrator recognized the machine fingerprint
	// and reused the existing devices row (M16) instead of minting a new one.
	Deduped bool `json:"deduped"`
}

DeviceTokenResponse is the success answer of POST /auth/device/token.

type Envelope

type Envelope struct {
	Enc    string `json:"enc"`
	KeyGen int    `json:"key_gen"`
	Nonce  string `json:"nonce"` // base64, 12 bytes
	CT     string `json:"ct"`    // base64 ciphertext
}

Envelope is the sealed-payload wire format.

type EnvelopeCipher

type EnvelopeCipher struct {
	// contains filtered or unexported fields
}

EnvelopeCipher seals/opens payloads with one CEK generation.

func EnsureCEK

func EnsureCEK() (*EnvelopeCipher, error)

EnsureCEK returns the account CEK cipher, lazily generating and persisting the CEK on first use (atomic write-back to ~/.jcode/cloud.json). The cipher is cached in-process afterwards. Requires the device to be logged in.

func NewEnvelopeCipher

func NewEnvelopeCipher(cek []byte, keyGen int) (*EnvelopeCipher, error)

NewEnvelopeCipher builds a cipher from a raw 32-byte CEK and its generation.

func RecoverCEK

func RecoverCEK(phrase string) (*EnvelopeCipher, error)

RecoverCEK rebuilds the CEK from a 24-word recovery phrase and persists it (keeping the stored key_gen, default 1), overwriting any existing CEK. The caller is responsible for warning and confirmation before calling.

func RotateCEK

func RotateCEK() (*EnvelopeCipher, error)

RotateCEK generates a new CEK at generation key_gen+1, persists it, and updates the in-process cache. Already-paired clients keep the old CEK and must re-pair to read new content.

func (*EnvelopeCipher) CEK

func (e *EnvelopeCipher) CEK() []byte

CEK returns a copy of the raw CEK bytes.

func (*EnvelopeCipher) KeyGen

func (e *EnvelopeCipher) KeyGen() int

KeyGen reports the CEK generation this cipher seals with.

func (*EnvelopeCipher) Open

func (e *EnvelopeCipher) Open(raw json.RawMessage) ([]byte, error)

Open decrypts an envelope payload. The caller must have applied IsEnvelope first (or use OpenMaybe); passing plaintext here is an error.

func (*EnvelopeCipher) OpenMaybe

func (e *EnvelopeCipher) OpenMaybe(raw json.RawMessage) ([]byte, bool, error)

OpenMaybe decrypts raw when it is an envelope, otherwise returns it unchanged (plaintext grey-scale passthrough). The boolean reports whether decryption happened.

func (*EnvelopeCipher) Seal

func (e *EnvelopeCipher) Seal(plaintext []byte) (json.RawMessage, error)

Seal encrypts plaintext into the envelope wire format (as JSON).

type EventUpload

type EventUpload struct {
	Seq     int64           `json:"seq"`
	Kind    string          `json:"kind"`
	Payload json.RawMessage `json:"payload"`
}

EventUpload is one durable event in a batch upload.

type EventsUploadResponse

type EventsUploadResponse struct {
	Accepted   []int64 `json:"accepted"`
	Conflicted []int64 `json:"conflicted"`
	MaxSeq     int64   `json:"max_seq"`
}

EventsUploadResponse is the answer of POST /internal/v1/device/sessions/{sid}/events. Conflicted lists seqs the server already had (skipped); MaxSeq is the server's high-water mark for the session and is used to resync the local allocator.

type PairedInfo

type PairedInfo struct {
	PairingID string    `json:"pairing_id"`
	Label     string    `json:"label"`
	Auto      bool      `json:"auto"`
	PairedAt  time.Time `json:"paired_at"`
}

PairedInfo records the most recent approval so the web UI can notify ("device X paired via QR code"). Auto is true for offer-based (QR scan) approvals, false for manual approvals.

type Pairing

type Pairing struct {
	ID        string `json:"id"`
	Label     string `json:"label"`
	PubKey    string `json:"pubkey"` // requester P-256 public key, base64 SPKI DER
	Status    string `json:"status"` // "pending" | "approved" | "denied"
	CreatedAt string `json:"created_at"`
}

Pairing is one pairing request as seen by the device.

type PendingPairing

type PendingPairing struct {
	PairingID  string    `json:"pairing_id"`
	Label      string    `json:"label"`
	ReceivedAt time.Time `json:"received_at"`
	PubKey     string    `json:"-"`
}

PendingPairing is one pairing.request parked for user approval. PubKey is kept in memory only (needed to wrap the CEK on approve) and never serialized.

type RegisterDeviceRequest

type RegisterDeviceRequest struct {
	Name         string `json:"name"`
	Hostname     string `json:"hostname"`
	JcodeVersion string `json:"jcode_version"`
	PubKey       string `json:"pubkey"` // X25519 public key, base64
	// Platform is how this jcode instance was launched ("desktop" | "cli");
	// see detectPlatform in connector.go.
	Platform string `json:"platform,omitempty"`
	// E2EE reports the connector's ACTUAL encryption state (M13): true only
	// when the CEK cipher is active and cloud.e2ee did not disable it. The
	// orchestrator gates plaintext downlink on it (docs/17 §6.7).
	E2EE bool `json:"e2ee,omitempty"`
	// Fingerprint is the sha256 of the machine fingerprint (M16): it backfills
	// rows minted without one so a later login dedups onto this device.
	Fingerprint string `json:"fingerprint,omitempty"`
}

RegisterDeviceRequest is the body of POST /internal/v1/device/register.

type SessionSeqInfo

type SessionSeqInfo struct {
	SessionID string `json:"session_id"`
	LastSeq   int64  `json:"last_seq"`
}

SessionSeqInfo is the server's per-session high-water mark, returned by the sessions upsert so the event pump can resume seq numbering after a restart.

type SessionUpsert

type SessionUpsert struct {
	SessionID string          `json:"session_id"`
	Status    string          `json:"status"` // "running" | "idle"
	Meta      json.RawMessage `json:"meta"`   // SessionMeta JSON, as-is
}

SessionUpsert mirrors one local session's index entry to the cloud.

type SessionsUpsertResponse

type SessionsUpsertResponse struct {
	Sessions []SessionSeqInfo `json:"sessions"`
}

SessionsUpsertResponse is the answer of POST /internal/v1/device/sessions.

type Status

type Status struct {
	LoggedIn    bool   `json:"logged_in"`
	AutoConnect bool   `json:"auto_connect"`
	State       string `json:"state"` // "offline" | "connecting" | "online" | "error"
	DeviceName  string `json:"device_name"`
	CloudURL    string `json:"cloud_url"`
	Error       string `json:"error,omitempty"`
}

Status is the JSON snapshot of the cloud relay served by the web API.

type Supervisor

type Supervisor struct {

	// Version is the jcode version reported at device register. It lives in
	// the command package (import cycle), so callers set it before Start.
	Version string
	// contains filtered or unexported fields
}

Supervisor owns the relay Connector lifecycle: Start launches the connector when the device is logged in with cloud.auto_connect enabled, SetAutoConnect persists and hot-applies the toggle, and Status reports the live snapshot. Like the connector itself it is strictly best-effort — failures are logged warnings, never errors that could affect the web server. All methods are safe for concurrent use.

func NewSupervisor

func NewSupervisor(cfg *config.Config, port int, webToken string) *Supervisor

NewSupervisor returns a Supervisor bound to cfg (mutated live by SetAutoConnect), the local web control plane port and its auth token.

func (*Supervisor) ApprovePairing

func (s *Supervisor) ApprovePairing(ctx context.Context, id string) error

ApprovePairing approves a pending pairing through the live connector.

func (*Supervisor) BuildConnector

func (s *Supervisor) BuildConnector(creds *Credentials) *Connector

BuildConnector is the pure start decision + construction behind Start: nil when the connector should not run (not logged in, or auto_connect explicitly disabled). Exported so the command package reuses one code path.

func (*Supervisor) DenyPairing

func (s *Supervisor) DenyPairing(ctx context.Context, id string) error

DenyPairing denies a pending pairing through the live connector.

func (*Supervisor) LastPaired

func (s *Supervisor) LastPaired() (PairedInfo, bool)

LastPaired reports the live connector's most recent pairing approval.

func (*Supervisor) PendingPairings

func (s *Supervisor) PendingPairings() []PendingPairing

PendingPairings returns the live connector's pending pairing inbox, empty when the connector is not running.

func (*Supervisor) SetAutoConnect

func (s *Supervisor) SetAutoConnect(enabled bool) error

SetAutoConnect persists cloud.auto_connect (preserving the stored Enabled/URL/E2EE fields, same read/modify/write pattern as `jcode login`) and hot-applies it: enabling starts the connector when credentials are valid, disabling stops it. On a SaveConfig failure the in-memory config is rolled back and the error returned.

func (*Supervisor) Start

func (s *Supervisor) Start(ctx context.Context)

Start applies the current config/credentials: when the device is logged in and auto_connect is enabled the connector is launched under a child of ctx (web server shutdown tears it down); otherwise the supervisor stays idle.

func (*Supervisor) Status

func (s *Supervisor) Status() Status

Status snapshots the relay state. Credentials are re-read from disk so a login/logout from another process is reflected without a restart.

func (*Supervisor) SyncCredentials

func (s *Supervisor) SyncCredentials()

SyncCredentials re-reads the on-disk credentials after a login/logout (web API or another process) and reconciles the connector: logged out (or auto_connect off) stops it; fresh or replaced credentials (re)start it. Like everything cloud-side it is best-effort — a credentials read failure is logged and leaves the current state untouched.

type SyncStore

type SyncStore struct {
	// contains filtered or unexported fields
}

SyncStore is the per-session cloud-sync state backed by one JSON file. All methods are safe for concurrent use.

func LoadSyncStore

func LoadSyncStore(path string) (*SyncStore, error)

LoadSyncStore opens the store at path. A missing file is not an error — it means "no session opted in yet". A parse/IO error IS returned: callers decide whether to fail closed (connector: nothing syncs) or surface it (web API: 500).

func (*SyncStore) Delete

func (s *SyncStore) Delete(sessionID string) error

Delete forgets the local per-session preference after the session itself is deleted. The connector observes the file change and sends a replacement snapshot that removes the corresponding cloud mirror.

func (*SyncStore) Enabled

func (s *SyncStore) Enabled(sessionID string) bool

Enabled reports whether the session has an explicit sync opt-in. Unset sessions report false (default OFF).

func (*SyncStore) Has

func (s *SyncStore) Has(sessionID string) bool

Has reports whether the session has an explicit entry (set vs unset).

func (*SyncStore) Path

func (s *SyncStore) Path() string

Path returns the store's backing file path (the connector's session-sync loop watches it for mtime changes).

func (*SyncStore) Set

func (s *SyncStore) Set(sessionID string, enabled bool) error

Set records the session's sync state and persists the store atomically (0600), overwriting any existing entry.

func (*SyncStore) SetIfAbsent

func (s *SyncStore) SetIfAbsent(sessionID string, enabled bool) (bool, error)

SetIfAbsent records the session's sync state only when no explicit entry exists yet (session-creation stamping must never clobber a user toggle). It reports whether an entry was written.

func (*SyncStore) Snapshot

func (s *SyncStore) Snapshot() map[string]bool

Snapshot returns a copy of the explicit per-session map.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL