fa_hub_client library

DAP/1 hub client for flutter_agent_harness.

Signed WebSocket messaging (channels, DMs, presence) with end-to-end encrypted payloads — the hub only ever sees ciphertext. Shaped as an upstreamable PR for IstiN/flutter_agent_harness (lib/src/hub/ there).

Classes

AgentInfo
Peer directory entry returned by whois/presence.
AgentMessage
One message between two agents (upstream shape).
ChannelKeys
One channel's X25519 keypair (base64).
ChannelStore
Runtime channel-key state for one client: seeded from the shared channels file, mutated by auto-keygen and accepted invites, persisted read-modify-write. The hub never sees any of these keys (only public keys via join).
DapSettings
Optional persisted settings: ~/.dap/config.json (all fields optional).
FahPlugin
Base interface for a fah plugin / package extension.
HubClient
HubConfig
HubIdentity
HubMessagingRepository
HubPlugin
HubStatus
Connection snapshot for the dap_status tool: who we are, where we are connected, what we can send to, and how the handshake is going.
InboundMessage
One inbound msg frame: decrypted when a key is available.
MailboxEntry
One entry in the messaging-fabric directory (upstream shape).
MessagingRepository
Isolated messaging backend for agent inboxes (upstream shape).
PendingInvite
A pending by-name invite in the machine-shared ~/.dap/config.json (invites: [{name, channel}]): armed by dap_invite <name> for a user not yet on the hub, removed once the chankey DM was delivered.
PendingInvites
Pending by-name invites — dap_invite <name> against a user not yet on the hub: persisted in the machine-shared ~/.dap/config.json under invites: [{name, channel}], delivered automatically once the name appears online. One presence query per interval (~15 s) plus an immediate check at arm time and after every welcome (restart redelivery). Mirrors the omp-extension pending-invite behavior.
PluginContext
Context passed to FahPlugin.register.
PluginIO
IO surface exposed to plugins for writing to the terminal.

Enums

DapSecretSource
Where resolveDapClientSecret found the dial credential.

Constants

defaultDapUrl → const String
envChannelsFile → const String
envClientSecret → const String
envConfigFile → const String
envMasterSecret → const String
unauthorizedMsg → const String
Frozen cross-adapter text for a hub bearer rejection (HTTP 401 before the websocket upgrade) — byte-identical in every DAP adapter; the hub answers unauthorized for a missing/unknown secret.

Functions

canonicalJson(Object? value) String
Recursively key-sorted, whitespace-free JSON encoding of value.
dapHostOf(String url) String
The hub address for paste-ready connect lines: scheme and trailing /ws stripped (ws://h:1/wsh:1).
decryptPayload({required SimpleKeyPair recipientDhKeyPair, required SimplePublicKey senderDhPubkey, required String frameId, required String aadTarget, required String ciphertextB64}) Future<String>
Decrypts a payload produced by encryptPayload. aadTarget must equal the sender's. Throws on wrong key or tampered ciphertext.
defaultDapConfigFile([String? home, Map<String, String>? environment]) String
~/.dap/config.json path — the single authority every reader/writer of that file goes through (readDapConfig, persistDapConfig, resolveDapSettings). DAP_CONFIG_FILE (from environment, default Platform.environment) wins outright; home overrides ~ (test seam).
defaultDapKeyPath(String? name, String home) String
Default identity file, derived from the agent name (or hostname): ~/.dap/keys/fah/<sanitized>.key.
defaultHome([Map<String, String> environment = const {}]) String
~ on POSIX and Windows alike (dart:io has no homedir).
encryptPayload({required SimpleKeyPair senderDhKeyPair, required SimplePublicKey recipientDhPubkey, required String frameId, required String aadTarget, required String plaintext}) Future<String>
Encrypts plaintext for the holder of the DH private key matching recipientDhPubkey. aadTarget is the channel name (channel send) or recipient agentId (DM).
hexEncode(List<int> bytes) String
hkdfSha256({required List<int> ikm, required List<int> salt, required List<int> info, required int length}) Future<Uint8List>
HKDF-SHA256 extract+expand per RFC 5869.
loadChannelKeys(String file) Future<Map<String, ChannelKeys>>
Channels file → keypairs. Missing or invalid file = no channels; only entries with both keys survive.
newChannelKeypair() Future<ChannelKeys>
Fresh channel X25519 keypair — generated by the channel's first user.
newFrameId() String
Opaque unique frame id, uuid-v4 shaped.
normalizeDapHost(String host) String
dap_connect host normalization: no scheme → ws://, no path → /ws (hub.example.comws://hub.example.com/ws, hub:8787ws://hub:8787/ws, an explicit ws(s)://…/path is kept as-is).
parseChankeyInvite(String text) → ({String channel, String priv, String pub})?
A DM whose decrypted text is exactly a channel-invite payload {"t":"chankey","channel","pub","priv"}. Regular chat that happens to start with { still fails the shape check and returns null.
persistChannelKeys(String file, String name, ChannelKeys keys) Future<void>
Read-modify-write: keeps the other channels; creates the parent dir (e.g. ~/.dap) on demand.
persistDapConfig({String? url, String? name, List<String>? channels, List<PendingInvite>? invites, String? clientSecret, bool clearClientSecret = false, String? file}) Future<void>
Read-modify-write of ~/.dap/config.json (dap_connect persistence): merges url/name/channels/clientSecret; clearClientSecret removes a stale cache entry (401 recovery — a secret the hub rejected must not win precedence over the master secret on later launches). The default-room list only grows (a union — rooms are never un-remembered). invites is the authoritative pending-invite list — an empty list removes them all (delivered entries are dropped by the caller). file is injectable for tests. Auto-join on later launches flows through the shared channels file (the store joins every channel it has keys for).
randomHex(int nChars) String
Random lowercase hex string of nChars characters (nonce, ids).
readDapConfig(String file) Map<String, dynamic>
Reads ~/.dap/config.json; a missing or invalid file counts as absent.
readPendingInvites(String file) List<PendingInvite>
The invites list from ~/.dap/config.json; a missing or non-array key counts as empty (back-compat with files written before invites existed), malformed entries are skipped.
resolveDapClientSecret({Map<String, String> environment = const {}, Map<String, dynamic>? config}) → ({bool enroll, String? master, DapSecretSource source, String? token})
Hub dial credential per the enrollment contract: DAP_CLIENT_SECRET (env) > clientSecret from ~/.dap/config.json (config) > DAP_MASTER_SECRET (env — enroll-mode: the connection enrolls once and the hub-issued secret replaces it). token: null dials anyway — the hub answers 401 and the client surfaces the frozen enrollment hint.
resolveDapSettings({HubConfig config = const HubConfig(), Map<String, String> environment = const {}, String? home}) DapSettings
Resolves the effective settings (see library doc for precedence). config is the already env-merged hub: section (HubConfig.fromMap).
signFrame(Map<String, dynamic> frame, SimpleKeyPair keyPair) Future<String>
Signs frame (without sig field) and returns the b64 signature.
signingPayload(Map<String, dynamic> frame) Future<String>
The DAP/1 signing payload for frame (which must not yet contain sig).
verifyFrame(Map<String, dynamic> frame, String sigB64, SimplePublicKey publicKey) Future<bool>
Verifies a DAP/1 signature over frame (without sig field).

Typedefs

DapConnection = ({String agentId, List<String> channels, String? name, bool ok, String url})
dap_connect result: the connection now in force (the new welcome's agentId, the normalized url, the display name, every joinable room).
ExternalSteeringSource = Future<List<AgentMessage>> Function()
The Agent.externalSteeringSource seam: called at every turn boundary (before the first turn and after each one); drained BEFORE the in-process steering queue. Contract: must not throw; return an empty list when nothing arrived.
InviteResult = ({String channel, String? connectLine, String? error, bool ok, bool pending, String to})
dap_invite result: ok: false, error is an honest failure (not connected, ambiguous name, DM failure); otherwise the chankey DM went out immediately (pending: false) or the invite was armed for a user not yet online (pending: true — delivered automatically when they connect; connectLine is the paste-ready line for the invited user).
SlashCommand = Future<void> Function(List<String> args)
A slash-command handler registered by a plugin (upstream shape).

Exceptions / Errors

HubError
A hub error frame.