Documentation
¶
Index ¶
- Constants
- Variables
- func AddConsoleToken(path string, token AuthTokenWriteback) error
- func AddOperatorToken(path string, operatorToken AuthTokenWriteback, extra ...AuthTokenWriteback) error
- func AddOperatorTokenPreservingOpenMode(path string, operatorToken AuthTokenWriteback) error
- func DiscoverPath(explicit string) (string, bool, error)
- func GenerateRandomToken(bits int) (string, error)
- func HomeMoltnetDir() (string, error)
- func InviteReceiptPath(configPath, pairingID string) string
- func IsLoopbackListenAddr(addr string) bool
- func KeysDirForConfigPath(configPath string) string
- func ListNetworkIDs() ([]string, error)
- func LoadApplyFile(path string) (protocol.ApplyConfigRequest, string, error)
- func NetworkHomeDir(id string) (string, error)
- func NonLoopbackAnonymousWriteWarning(config Config) string
- func RemoveFollow(path, id string) error
- func RemoveInviteReceipt(configPath, pairingID string) error
- func RemoveRoomFromConfig(path string, roomID string) error
- func ResolveConfigPath(explicit string, id string) (path string, found bool, err error)
- func ResolveNodeConfigPath(explicit string, id string) (path string, found bool, err error)
- func UpdateRoomMembers(path string, roomID string, add []string, remove []string) error
- func ValidateListenAddr(addr string) error
- func WriteFollow(path string, follow protocol.Follow) error
- func WriteInviteReceipt(configPath string, receipt InviteReceipt) error
- func WritePairing(path string, pairing PairingWriteback, authToken AuthTokenWriteback, ...) error
- func WriteRoom(path string, room RoomWriteback) error
- type App
- type AuthTokenWriteback
- type Config
- type ConsoleAnalyticsConfig
- type ConsoleConfig
- type InviteReceipt
- type JSONStorageConfig
- type PairingRelayWriteback
- type PairingWriteback
- type PostgresStorageConfig
- type RevokeResult
- type RoomConfig
- type RoomCredentialConfig
- type RoomWriteback
- type SQLiteStorageConfig
- type StorageConfig
Constants ¶
const ( DefaultPath = defaultConfigFile DefaultSchema = defaultConfigSchema DefaultNetworkID = defaultNetworkID DefaultNetworkName = defaultNetworkName )
const InertAgentRegistrationAdviceSuffix = ` Set auth.agent_registration to "token" or "disabled", or bind to a loopback address, unless this is intentional.`
InertAgentRegistrationAdviceSuffix is the exact trailing sentence NonLoopbackAnonymousWriteWarning appends on the agent_registration: open branch, above. Exported as the one shared symbol rather than left as an inline literal so cmd/moltnet's initNonLoopbackWarning (init_listen.go) -- the one caller that needs to strip this exact sentence back off and substitute its own accurate authoring-time advice -- references this constant directly instead of keeping a second, hand-duplicated copy that could silently drift out of sync with this one (round 3's fix for a mutation-proven gap: two independent literals could reorder differently and TrimSuffix would just stop matching, leaving both the stock and the corrected advice printed side by side). Every other caller (`moltnet validate`, server start) prints the warning this constant is part of verbatim, advice included, and must keep doing so until the underlying inertness itself is fixed (see this function's own TODO(setup-p2) above).
const KeysDirEnv = "MOLTNET_KEYS_DIR"
KeysDirEnv names a durable keys directory explicitly. It wins over every derived location, and is the only way to sign with env-only memory or Postgres configurations.
Variables ¶
var DefaultDiscoveryOrder = []string{
defaultConfigFile,
"moltnet.yaml",
"moltnet.yml",
"moltnet.json",
}
var ErrAuthModeNotOpen = errors.New("auth.mode is not open")
ErrAuthModeNotOpen indicates AddOperatorTokenPreservingOpenMode was asked to repair a Moltnet config whose auth.mode is not "open", outside the one shape PLAN.md phase 7.0's repair path exists for, so it refuses rather than guess what a non-open network wants.
var ErrAuthTokenIDConflict = errors.New("pairing id collides with a non-pair auth token")
ErrAuthTokenIDConflict indicates a pairing's id collides with an existing auth.tokens[] entry that is not a plain pair-scoped credential.
F5 (confirmed): the pairing id that ends up as AuthTokenWriteback.ID is invite-controlled on both sides of a pairing -- `pair invite --id <id>` picks it directly, and `pair <code>` takes whatever pairing_id the invite names, with no round trip to confirm it first. upsertAuthToken (below) overwrites-by-id unconditionally, so an invite (or a hand-crafted one) naming an id that collides with an operator's own static token -- most dangerously "operator" itself -- used to silently replace that token's value and scopes with a bare [pair]-scoped credential, locking the real operator out of their own network. This is refused even under --force: --force is documented and tested as "overwrite an existing *pairing* with this id", never "overwrite an unrelated non-pair credential" -- those are different invariants and only the first one is what --force opts into.
var ErrAuthTokensExist = errors.New("auth.tokens already has entries")
ErrAuthTokensExist indicates auth.tokens[] in the target Moltnet config already has at least one entry that AddOperatorToken cannot safely upgrade in place (canUpgradeOperatorOnlyOpenConfig), so it refuses rather than risk creating an unwanted second admin-scoped credential or silently reshaping tokens an operator configured by hand.
var ErrConsoleTokenIDExists = errors.New("console token id already exists")
ErrConsoleTokenIDExists indicates AddConsoleToken's caller picked token.ID against a config it read at some earlier point (nextConsoleTokenID, cmd/moltnet/auth_token_writeback.go), but the config on disk, read again here, immediately before writing, already has a token using that same id by the time this function actually runs. AddConsoleToken refuses rather than silently overwrite it.
var ErrPairingExists = errors.New("pairing already exists")
ErrPairingExists indicates a pairings[] entry with the same id already exists in the target Moltnet config file.
var ErrPairingNotFound = errors.New("pairing not found")
ErrPairingNotFound indicates pairings[] in the target Moltnet config file has no entry with the given id -- RevokePairing has nothing to revoke.
var ErrRoomConfigExists = errors.New("room already exists in config")
ErrRoomConfigExists indicates rooms[] in the target Moltnet config file already has an entry with the same id as the room WriteRoom was asked to create.
var ErrRoomConfigNotFound = errors.New("room not found in config")
ErrRoomConfigNotFound indicates rooms[] in the target Moltnet config file has no entry with the given id, so UpdateRoomMembers has nothing to update. Unlike ErrRoomConfigExists (config_writeback_room.go), this is not necessarily a caller mistake: a room can exist on the live service without ever having been declared in this config file (created directly via POST /v1/rooms, the console, or `moltnet apply`, none of which touch rooms[]); see UpdateRoomMembers's doc comment for how callers are meant to treat this case.
Functions ¶
func AddConsoleToken ¶ added in v0.1.15
func AddConsoleToken(path string, token AuthTokenWriteback) error
AddConsoleToken appends a single observe-only-scoped auth.tokens[] entry into the Moltnet config file at path, reusing the same writeback machinery as AddOperatorToken. Unlike AddOperatorToken, it never refuses because auth.tokens already has entries: it exists for `moltnet console`'s self-heal path, which by definition only ever runs against a config that already has at least one token (the operator's) but none scoped to exactly [observe], refusing on "tokens already exist" would make it useless for its one caller.
It does set auth.mode to bearer, via the same writeAuthTokensToDoc body AddOperatorToken shares, but for AddConsoleToken's one caller this is never an enabling side effect: self-heal only ever runs against a config that is already in bearer mode (that is its own precondition), so this write leaves auth.mode exactly as it found it in practice. It is documented here only because writeAuthTokensToDoc always does it, unconditionally, for both callers.
The caller is responsible for first picking a non-colliding id (see the id-collision handling token.ID goes through before AddConsoleToken is called), but that pick is made against a config read earlier, which can go stale by the time this function's own read happens (a concurrent `moltnet console` invocation, or a hand edit, landing in between). This function closes that TOCTOU itself: it checks the freshly re-decoded document for token.ID and refuses (ErrConsoleTokenIDExists) rather than trust the caller's earlier pick, so it never silently overwrites a token it was not asked to overwrite.
func AddOperatorToken ¶ added in v0.1.15
func AddOperatorToken(path string, operatorToken AuthTokenWriteback, extra ...AuthTokenWriteback) error
AddOperatorToken sets auth.mode to bearer and appends operatorToken, plus any extra tokens (in one atomic write, never a second, separate write that could land only one of them), into the Moltnet config file at path, reusing the same plaintext-preserving writeback machinery as WritePairing (untyped map edit, atomic write, mode 0600, symlink-refusing target).
It refuses with ErrAuthTokensExist when auth.tokens already has entries it cannot safely upgrade, with one exception (PLAN.md phase 7.0's "unblock the --bearer upgrade" fix, added once plain `init` started minting its own operator token, below): when the config's *only* existing token shares operatorToken.ID, canUpgradeOperatorOnlyOpenConfig treats it as exactly the operator-only open config plain `init` writes, and this function upgrades it in place, flipping auth.mode to bearer (via writeAuthTokensToDoc, shared with AddConsoleToken) and writing only the extra tokens, while leaving the existing operator entry completely untouched. That is deliberate, not an oversight: re-writing it here would silently rotate the operator's secret and could widen its scopes to whatever the caller passed (bearerMoltnetConfig-shaped callers pass operatorToken with the "pair" scope plain `init`'s operator token must never carry; see defaultMoltnetConfig's doc comment, templates.go). Every other existing-tokens shape (more than one token, or a single token under a different id) still refuses outright, so `init --bearer` never reshapes a config an operator set up by hand.
`moltnet init --bearer` calls this with both the operator token ([observe, write, admin], unchanged) and a second observe-only "console" token as extra, so a freshly bearer-enabled network always has a token `moltnet console` can hand the browser, closing the gap where init minted a token the console command was never allowed to use. On the upgrade path above, only that console token actually lands; the operator token argument is discarded once its id is confirmed to already exist.
func AddOperatorTokenPreservingOpenMode ¶ added in v0.1.15
func AddOperatorTokenPreservingOpenMode(path string, operatorToken AuthTokenWriteback) error
AddOperatorTokenPreservingOpenMode inserts operatorToken into an existing Moltnet config's auth.tokens[] without ever touching auth.mode, unlike writeAuthTokensToDoc (shared by AddOperatorToken and AddConsoleToken), which always flips it to "bearer". It backs plain `init`'s PLAN.md phase 7.0 repair path: a config written before phase 7.0 shipped has auth.mode: open and no operator token at all, so its owner has no way to administer their own network; re-running plain `init` against it now fixes that in place, atomically, while leaving the network's local-first "open" posture exactly as the owner left it.
It only ever writes when the config is genuinely that shape: auth.mode already "open" and auth.tokens[] currently empty. Anything else refuses rather than guess:
- auth.mode not "open" returns ErrAuthModeNotOpen, outside this repair's stated scope (a hand-edited bearer/none config), and not plain `init`'s concern either way, since it has never rewritten an existing config's auth.mode.
- any existing auth.tokens[] entries return ErrAuthTokensExist, PLAN.md phase 7.0 leaves such a config untouched rather than guessing whether adding a second admin-scoped credential next to whatever an operator already configured by hand is safe; the caller (runInit) prints a note explaining that rather than silently doing nothing.
func GenerateRandomToken ¶ added in v0.1.15
GenerateRandomToken returns a base64url-encoded random value with at least bits worth of entropy (rounded up to a whole byte). It backs the relay pairing invite's room name and pairing token: the relay's RELAY_TOKEN is worker-wide, so this entropy is what keeps one invited friend out of another friend's relay room.
func HomeMoltnetDir ¶ added in v0.1.15
HomeMoltnetDir returns ~/.moltnet, the root `moltnet init` writes into for global per-network installs when --dir is not given (PLAN.md phase 4's "onboarding fast path").
func InviteReceiptPath ¶ added in v0.1.15
InviteReceiptPath returns the path WriteInviteReceipt/LoadInviteReceipt/ RemoveInviteReceipt use for pairingID's receipt, alongside the Moltnet config's other per-network runtime state (the .moltnet/ convention relaydeploy.CredentialsPath and register-agent's identity.json already use). It is exported for callers (tests, `pair invite show`) that want the absolute path without performing an operation; the three functions above derive the identical relative path themselves via inviteReceiptRelPath so they can resolve it through an os.Root (see atomicWriteSecretFileInRoot's doc comment) instead of the plain absolute path this function returns.
The filename is a SHA-256 of pairingID -- pairingID is hashed raw, not trimmed. Pairing ids are operator- or invite-supplied strings with no filename-safety grammar of their own -- WritePairing accepts any string as pairing.ID, and even *network* ids only recently gained a restrictive grammar (SETUP.md's Q2). Deriving a path straight from an unvalidated id would let a crafted --id (or an invite's pairing_id field, which `pair <code>` also feeds into a local pairing.ID with no round trip to confirm it) escape .moltnet/invites/ via "..", "/", or similar. Hashing sidesteps that without this package inventing a new pairing-id validation policy it does not own.
Trimming used to happen here, before hashing: that meant " friend " and "friend" hashed to the same file, so writing one clobbered the other's receipt and removing one deleted the other's -- reachable from the network side, since `pair <code>` writes an invite's pairing_id into pairings[] verbatim, with no trim of its own (unlike `pair invite --id`, which does trim before this function ever sees the id). Any forgiveness for an operator's own typo'd whitespace belongs at the CLI boundary that reads --id/positional pairing-id arguments (see runPairInviteShow/runPairRevoke, cmd/moltnet/pair_invite.go and pair_revoke.go), not inside the hash that two different callers' ids must not collide through.
func IsLoopbackListenAddr ¶ added in v0.1.15
IsLoopbackListenAddr reports whether addr (a server.listen_addr value, e.g. "127.0.0.1:8787", ":8787", "0.0.0.0:8787", "[::1]:8787") only ever accepts direct TCP connections originating on this machine. An address with no host component (":8787") or an unparseable host is treated as non-loopback, the same "binds every interface" behavior Go's net/http gives an empty host, and the conservative default when this can't be determined for certain: this function's only job is deciding whether to warn, so an ambiguous case should warn rather than stay silent.
A true result here is a narrower claim than "unreachable from off this machine": a loopback bind with a reverse proxy running on the same box (the topology guides/public-open-networks.md and guides/securing-remote-agents.md both recommend) is fully internet reachable through that proxy; every request the app process sees still carries a loopback RemoteAddr, on the proxy's behalf. This function has no request to inspect at server-start / `moltnet validate` time, so it can only ever reason about the bind; a live per-request decision (see internal/transport/discovery.go's requestIsDirectLoopback, used for the GET /install.md anonymous carve-out) has to check the request itself instead, precisely because this bind-only signal is not sufficient there.
func KeysDirForConfigPath ¶ added in v0.1.22
KeysDirForConfigPath is the keys directory the server uses for the config file at configPath: $MOLTNET_KEYS_DIR when set, else `<dir>/.moltnet/keys`. Every CLI command that reads or writes keys, pins, grants or pending joins resolves through it, so the CLI and the running server share one store.
func ListNetworkIDs ¶ added in v0.1.15
ListNetworkIDs returns the sorted subdirectory names under ~/.moltnet, each one a network id `moltnet init` created there, regardless of whether the network still contains a config file. Unlike resolveHomeNetworkConfig (which only counts a directory when it holds a specific filename), this is used by `moltnet uninstall` to enumerate every network home so a config file removed by hand does not hide a network's leftover service or data from the uninstall plan. A missing ~/.moltnet is not an error: it just means no networks were ever created there.
func LoadApplyFile ¶ added in v0.1.12
func LoadApplyFile(path string) (protocol.ApplyConfigRequest, string, error)
func NetworkHomeDir ¶ added in v0.1.15
NetworkHomeDir returns the per-network directory `moltnet init` creates under ~/.moltnet for network id.
func NonLoopbackAnonymousWriteWarning ¶ added in v0.1.15
NonLoopbackAnonymousWriteWarning returns a non-empty warning when config resolves to a listen address reachable from outside this machine while its effective auth posture leaves write/admin routes reachable anonymously (PLAN.md phase 6a, item 2, broadened by the phase 6a review's P2-3): that covers both agent self-registration being open ("any reachable host may register an agent") and auth.mode being "none" (every write and admin route is anonymous outright, with no registration step needed at all, the pre-phase-6a default, and strictly worse than open registration). It returns "" when the bind is loopback-only.
The original version of this check keyed only on agent_registration, so a non-loopback bind with the old `auth.mode: none` default, every write and admin route anonymous, warned about nothing, while a safer bearer network with open registration correctly warned. That inversion is the bug this broadened check fixes: mode: none is checked first, and on its own, before agent_registration is even considered.
config.Auth.AgentRegistration is read post-merge (config_load.go's mergeFileConfig already resolves it, including auth.mode: open's implicit override), so the registration-open half fires identically however registration ended up open: an explicit auth.agent_registration: open, or auth.mode: open.
Round 2's P2-4: the registration-open branch also names anonymous read, not just write, when it applies -- auth.mode: open forces auth.public_read: true alongside agent_registration: open (mergeFileConfig, config_load.go), and plain `init`'s own starter config (defaultMoltnetConfig, cmd/moltnet/templates.go) writes a "general" room with visibility: public. Before this fix, a widened bind on that exact default produced a warning naming only write-scoped self-registration -- silent about the same bind also serving that room's full history over REST and its live stream over SSE to any anonymous visitor, no token at all. The remedy is identical either way (tighten auth, or bind to loopback), so this does not change what an operator has to do about it; it only stops describing the exposure incompletely.
Callers: internal/app.New logs this at server start (app.go), and `moltnet validate` (cmd/moltnet/validate.go) prints it so an operator sees the exposure before ever starting the server.
func RemoveFollow ¶ added in v0.1.20
RemoveFollow removes only the subscription. Durable mirror history remains.
func RemoveInviteReceipt ¶ added in v0.1.15
RemoveInviteReceipt deletes pairingID's receipt file, if any. It is RevokePairing's (config_writeback_revoke.go) companion for the receipt the way removeAuthToken is for the auth.tokens[] entry: a revoked pairing must not leave a live-looking invite code recoverable on disk. Removing a receipt that never existed is a silent no-op, matching removeAuthToken/stripPairingFromRoomFederations's own "already-legitimately -absent state" handling.
func RemoveRoomFromConfig ¶ added in v0.1.15
RemoveRoomFromConfig deletes the rooms[] entry with the given id from the Moltnet config file at path, preserving every other plaintext secret already there.
P0-1 (confirmed live): store.RemoveRoomContext (internal/store/sql_store.go) is a soft delete -- it tombstones the row (deleted_at) but keeps its primary key. `room remove` used to only call DELETE /v1/rooms/{id} against the running service; the config's own rooms[] entry for that room survived untouched. On the next restart, applyRequestFromConfig (internal/app/app.go) replays every config-declared room: CreateRoomContext's plain INSERT hits the tombstoned primary key and fails with ErrRoomExists, so the apply path falls back to ReconcileRoomContext -- which never finds a live row for a soft-deleted id and returns not-found. The network fails to start. This was pre-existing for any hand-written config room, but `room create` (this same phase) started writing rooms into config, so it now applies to every room that command creates.
The fix mirrors UpdateRoomMembers (config_writeback_membership.go): same document-editing shape, same ErrRoomConfigNotFound for "this room was never declared here" (created directly via the API, the console, or `moltnet apply`, none of which touch rooms[] -- nothing to remove, and no restart hazard either, since applyRequestFromConfig only replays rooms this file actually lists). The write is atomic (temp file + rename), mode 0600, and refuses a symlinked target, matching every other config writeback in this package.
func ResolveConfigPath ¶ added in v0.1.15
ResolveConfigPath implements the shared Moltnet server config discovery order for start/pair/relay/admin/service:
- Explicit (--config, or MOLTNET_CONFIG) wins outright.
- Otherwise, when id is non-empty: ~/.moltnet/<id>/Moltnet is resolved first. If ~/.moltnet/<id>/ does not exist at all, this falls back to the same cwd discovery order step 3 uses, but ONLY when that cwd candidate sniffs as text AND its own network.id equals id (P0-2, a --dir install resolving by name from its own directory). This is a name match, never blind cwd precedence: a same-named file (or, on a case-insensitive filesystem such as APFS, a same-named compiled binary like ./moltnet) that doesn't self-identify as id is refused, erroring by name.
- Otherwise (no explicit, no id), the cwd discovery order (./Moltnet and the alternate filenames DiscoverPath recognizes) is tried, then the sole network directory under ~/.moltnet/, cwd participates only in this no-flag, no-id path (aside from the name-matched fallback in step 2).
found is false, with a nil error, only when nothing exists anywhere in that order; callers that treat a missing config as "use defaults" (start) and callers that treat it as an error ("run moltnet init first": pair, relay) both build on this same tri-state result.
func ResolveNodeConfigPath ¶ added in v0.1.15
ResolveNodeConfigPath applies the same fallback tier to MoltnetNode discovery, used by `moltnet node`: explicit wins outright; a non-empty id resolves ~/.moltnet/<id>/MoltnetNode first, falling back to a name-matched cwd candidate (moltnet.network_id == id) only when ~/.moltnet/<id>/ does not exist at all (P0-2); with neither explicit nor id, cwd is tried before the sole ~/.moltnet/ network. explicit here already carries MOLTNET_NODE_CONFIG, resolved by the caller before this is called.
func UpdateRoomMembers ¶ added in v0.1.15
UpdateRoomMembers applies add/remove membership changes to an existing rooms[] entry's members list in the Moltnet config file at path.
PLAN.md 7A.1 named the bug this closes: `admin room members add` PATCHes the live API only, but startup's applyRequestFromConfig (app.go) replaces every config-declared room's member list wholesale via ReconcileRoomContext, so membership granted only through the API is silently undone at the next restart -- reproduced live as "chat members = [alice] before restart, nil after". `admin room members add` is also named as the pairing-grant aftercare step in the same plan, so the two mechanisms were fighting each other. This gives membership one durable source of truth: the API call still updates the live service immediately (no restart needed to see the effect), and this writeback keeps the config file in sync so the *next* restart reconciles to the same state instead of wiping it.
A caller whose room id is not in rooms[] at all -- see ErrRoomConfigNotFound -- should treat that as "this room's membership cannot survive a restart yet" and warn, not fail: the live API mutation this writeback follows has already succeeded by the time this runs, and a config file that never declared the room in the first place is a pre-existing condition this single call cannot repair (the room itself would need `room create` or `pair invite --room` first).
add and remove are both normalized with protocol.UniqueTrimmedStrings, add is applied first (removal always wins when the same id appears in both, matching protocol.UpdateRoomMembersRequest's own semantics), and both are safe to pass empty. The write is atomic (temp file + rename), mode 0600, and refuses a symlinked target -- the same invariant every other config writeback in this package holds.
func ValidateListenAddr ¶ added in v0.1.15
ValidateListenAddr validates addr as a syntactically well-formed server.listen_addr value: a "host:port" pair in net.SplitHostPort's grammar (an empty host is fine -- ":8787" binds every interface, exactly matching how net/http.Server.ListenAndServe treats it -- but a missing or non-numeric port is not). It does not attempt to bind addr or resolve its host, and deliberately does not validate the host's own character content beyond what SplitHostPort itself requires (bracket balancing for an IPv6 literal): a stricter DNS-hostname grammar would risk rejecting a real, already-working listen_addr this validator has never seen before -- non-RFC-1123 hostnames (an underscore-containing Docker/Compose service name, for instance) are common enough in practice that guessing at that grammar here is more likely to break a live config than to catch a real typo. It only rejects values that could never be a valid listen_addr syntactically, so an operator sees the mistake immediately rather than only once the server tries and fails to bind (or, for `moltnet init --listen`, not at all until some later `moltnet start`).
Called both from validateServer (config_file.go, on every config load -- closing the "config_file.go validates none today" gap for listen_addr) and directly from `moltnet init --listen` (cmd/moltnet/init.go) before any file is written, so the same grammar is enforced whether a bad address arrives via a hand-edited config file or a flag. TODO(setup-p2): ":http" is a legal service-name port net.Listen itself accepts (it resolves service names via the system's services database) but strconv.Atoi below rejects outright, since it only ever accepts a numeric port. A net.LookupPort("tcp", port) fallback when Atoi fails would close the gap, but that call resolves against the local system's services database (getservbyname on Unix) -- environment-dependent and not guaranteed present in a minimal container, unlike everything else this validator checks -- so it is left as a known, narrow gap rather than making config validation itself non-portable.
mergeFileConfig (config_load.go) used to carry a second, related gap here -- this function trims addr before validating it, so " 127.0.0.1:8787 " (leading/trailing whitespace) passed validation, but assigned config.ListenAddr untrimmed, so the value that actually reached net.Listen at server start still carried the whitespace and failed to bind. Fixed: mergeFileConfig now trims server.listen_addr the same way mergeEnvConfig's envValue already trims MOLTNET_LISTEN_ADDR, so both config sources reach net.Listen identically trimmed.
func WriteFollow ¶ added in v0.1.20
WriteFollow appends a subscription without round-tripping existing secrets.
func WriteInviteReceipt ¶ added in v0.1.15
func WriteInviteReceipt(configPath string, receipt InviteReceipt) error
WriteInviteReceipt persists receipt at InviteReceiptPath(configPath, receipt.PairingID), atomically (temp file + rename in the same directory), mode 0600, refusing a symlinked target -- the same discipline WritePairing/atomicWriteConfigFile apply to the config file itself, since an invite code is exactly as sensitive as the pairing token it accompanies (protocol.Invite's doc comment: "the invite's entire purpose is to carry plaintext secrets").
func WritePairing ¶ added in v0.1.15
func WritePairing(path string, pairing PairingWriteback, authToken AuthTokenWriteback, roomIDs []string, force bool) error
WritePairing appends pairing and authToken into the Moltnet config file at path, preserving every other plaintext secret already there. It refuses to overwrite an existing pairings[] entry with the same id unless force is true. The write is atomic (temp file + rename in the same directory), mode 0600, and refuses a symlinked target, matching the server's private-config invariant for files that hold inline secrets.
When roomIDs is non-empty, each named room is created in rooms[] if absent, and its federation is extended to allow pairing.ID: an absent or "none" federation becomes a list naming just this pairing, an existing list gets this pairing appended (idempotently: re-running never duplicates the entry), and an existing "all" federation is left alone. This is the same single atomic write and post-write reload check as the pairing/auth-token writeback, so a room upsert never lands without the pairing it exists for, or vice versa.
func WriteRoom ¶ added in v0.1.15
func WriteRoom(path string, room RoomWriteback) error
WriteRoom appends room into the Moltnet config file at path, preserving every other plaintext secret already there. It refuses outright when rooms[] already has an entry with room.ID, unlike WritePairing's --force-overridable ErrPairingExists, `room create` has no in-place-update story yet (that is `admin room members add`/a future `room update`'s job), so a second `room create` for the same id is always a mistake to surface, never a silent overwrite. The write is atomic (temp file + rename in the same directory), mode 0600, and refuses a symlinked target; the same mechanism WritePairing uses, reused here rather than shared with it directly: a room create writes a much wider field set (name, visibility, write_policy, members, credential) than the {id, federation} shape WritePairing's own upsertSharedRoom knows how to extend (PLAN.md 7A.1; upsertSharedRoom is the wrong helper for this precisely because of that narrower shape).
room.Federation must never be nil: PLAN.md's 7A federation rule requires every config-time room to declare an explicit stance so validateRoomFederationStances never trips the first time a pairing is added. Callers that want the "no federation configured yet" default pass protocol.DefaultRoomFederation() explicitly rather than leaving this nil, WriteRoom itself does not substitute a default, so a nil here would write a room this same package's own federation rule then refuses to load.
Types ¶
type AuthTokenWriteback ¶ added in v0.1.15
AuthTokenWriteback is the plaintext pair-scoped auth.tokens[] entry a pair command appends alongside the pairing.
type Config ¶
type Config struct {
Clock *bridgeconfig.ClockConfig
AllowHumanIngress bool
CausalEventsPath string
Console ConsoleConfig
DebugEvents bool
DisableDirectMessages bool
Auth authn.Config
ListenAddr string
NetworkID string
NetworkName string
Pairings []protocol.Pairing
Follows []protocol.Follow
PublicBaseURL string
Rooms []RoomConfig
Storage StorageConfig
Version string
// contains filtered or unexported fields
}
func ConfigFromEnv ¶
func LoadConfigForPath ¶ added in v0.1.15
LoadConfigForPath loads and validates an explicit Moltnet config path through the same full load path the server uses at startup, including environment overrides. Unlike LoadFile (used by `moltnet validate`), it merges environment configuration, so it is the right check to run after a config writeback: LoadFile alone can pass a config that would fail once the server actually starts.
func (Config) KeysDir ¶ added in v0.1.22
KeysDir is where this network's signing key and peer pins live: $MOLTNET_KEYS_DIR when set, else next to the config file when there is one (`<dir>/.moltnet/keys`), else next to the durable store file. Empty means no durable location (env-only memory or Postgres without a config file).
type ConsoleAnalyticsConfig ¶ added in v0.1.11
type ConsoleConfig ¶ added in v0.1.11
type ConsoleConfig struct {
Analytics ConsoleAnalyticsConfig
}
type InviteReceipt ¶ added in v0.1.15
type InviteReceipt struct {
PairingID string `json:"pairing_id"`
Code string `json:"code"`
Exp time.Time `json:"exp"`
}
InviteReceipt is the plaintext record `pair invite` (cmd/moltnet/pair_invite.go) persists next to the Moltnet config so the exact code it is about to print survives an interruption between WritePairing's commit and the fmt.Fprintln that shows the code to the operator. Before this file existed, the code and its expiry were never written anywhere (SETUP.md's "no journal" claim, exception #1): the pairing landed in config, but the one secret the peer actually needs to consume it was gone the instant the process died before printing.
Fields are plain strings/time.Time, not protocol.SecretString, for the same reason PairingWriteback (config_writeback.go) uses plain strings: the receipt's only purpose is to hand the exact code back on request, and a redacted round-trip would defeat that.
func LoadInviteReceipt ¶ added in v0.1.15
func LoadInviteReceipt(configPath, pairingID string) (receipt InviteReceipt, ok bool, err error)
LoadInviteReceipt reads back the receipt WriteInviteReceipt stored for pairingID against the Moltnet config at configPath. ok is false with a nil error when nothing was ever written for this id -- a pairing generated with --print-only (which never writes a pairing at all), one generated before this feature existed, or one RemoveInviteReceipt already cleared (`pair revoke`).
A decoded receipt with an empty Code is treated as corrupt and returned as an error, not as ok=true: a `null` or `{}` body used to decode to a zero-value InviteReceipt with ok=true, so `pair invite show` returned err=nil and printed an empty line -- a wizard scraping stdout got "" back as "the code," indistinguishable from success. A zero Exp is left alone (still never treated as expired -- Expired's own doc comment): that shape is the documented, deliberate fallback for a hand-edited or pre-validation file, not evidence of corruption on its own.
func (InviteReceipt) Expired ¶ added in v0.1.15
func (receipt InviteReceipt) Expired(now time.Time) bool
Expired reports whether receipt's invite has already passed its expiry as of now. A zero Exp (should not happen for a receipt this package wrote, but defensive against a hand-edited or pre-validation file) is never treated as expired -- Validate on the decoded invite is still the authority that actually enforces expiry when the code is used.
type JSONStorageConfig ¶
type JSONStorageConfig struct {
Path string `json:"path,omitempty" yaml:"path,omitempty"`
}
type PairingRelayWriteback ¶ added in v0.1.15
PairingRelayWriteback is the plaintext pairings[].relay entry.
type PairingWriteback ¶ added in v0.1.15
type PairingWriteback struct {
ID string
RemoteNetworkID string
RemoteNetworkName string
Token string
Relay *PairingRelayWriteback
// Scope "rooms" writes a room link (shared rooms only).
Scope string
}
PairingWriteback is the plaintext pairings[] entry a pair command appends to a Moltnet config file. It uses plain strings instead of protocol.SecretString on purpose: SecretString marshals as [REDACTED], so round-tripping the loaded config structs through YAML/JSON would corrupt every secret already in the file. WritePairing instead edits the document as an untyped map, so no SecretString ever enters the write path.
type PostgresStorageConfig ¶
type PostgresStorageConfig struct {
DSN string `json:"dsn,omitempty" yaml:"dsn,omitempty"`
}
type RevokeResult ¶ added in v0.1.15
type RevokeResult struct {
TokenRemoved bool
}
RevokePairing removes pairingID from pairings[], removes the matching auth.tokens[] entry, and strips pairingID out of every room's federation list in the Moltnet config file at path. It is WritePairing's inverse: `pair invite`/`pair <code>` write the peer's inbound credential into auth.tokens[] under the exact same id as the pairing (config_writeback.go's PairingWriteback/AuthTokenWriteback), so a revoke that only removed the pairings[] entry and left that token behind would leave the peer fully authenticated on a pairing that no longer appears anywhere in pairings[]; UX.md and PLAN.md's 7B.3 both call this out explicitly as the part that must not be skipped.
Decision (7B.3's open question, since the plan asked for one and a reason): revoke DOES strip the room federation grant too, not just the pairing and its token. Leaving a room's federation list naming a revoked pairing id is a live grant waiting to be silently reactivated: generating a *new* pairing that happens to reuse the same id (`pair invite --id <id>` again) mints a fresh credential and, with the old federation entry untouched, instantly regains access to every room it names; no separate re-grant step, no visible signal the old grant survived. That makes revocation reversible by re-pairing alone, which defeats the point of a revoke command: an operator revoking access wants it gone until they deliberately grant it again. Stripping the federation entry means re-pairing under the same id starts from zero room access, exactly like a brand new pairing would; getting the old rooms back requires saying so again (`pair invite --room <id>` or `room create --federation`).
A federation list that becomes empty after stripping reverts to the scalar "none": protocol.ValidateRoomFederation rejects an explicit "list" naming zero pairings outright ("federation list must name at least one pairing"), so leaving an emptied list in place would make the room's own config un-reloadable, caught immediately by this call's post-write LoadConfigForPath check (via the caller, revokePairingWithRollback), which would then roll the whole revoke back. "none" is the correct steady state for a room with no remaining federation grants regardless: it is exactly what protocol.DefaultRoomFederation returns for a room that was never federated to begin with. Only "all" federation is left untouched, matching upsertSharedRoom's own asymmetric handling of that case (federatedRoomFederationValue); there is nothing to strip from a room that federates with everyone.
RevokeResult reports what RevokePairing actually found and removed, so a caller (cmd/moltnet/pair_revoke.go) can describe the real outcome instead of assuming every leg it tries to strip was actually present. Namely: a pairing whose auth.tokens[] entry uses a different id than the pairing itself (an operator hand-edited config, most plausibly, since every id `pair invite`/`pair <code>` ever writes matches by construction) leaves the peer's credential live even after a "successful" revoke, and TokenRemoved is how the caller learns that happened instead of printing the same "credential removed" line regardless.
func RevokePairing ¶ added in v0.1.15
func RevokePairing(path string, pairingID string) (RevokeResult, error)
Like WritePairing, this refuses a symlinked config target and writes atomically (temp file + rename), mode 0600. It does not itself verify the result reloads cleanly; callers wanting that (and a rollback on failure) use the same LoadConfigForPath-and-restore pattern writePairingWithRollback (cmd/moltnet/pair.go) already establishes for WritePairing.
type RoomConfig ¶
type RoomConfig struct {
ID string `json:"id" yaml:"id"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Members []string `json:"members,omitempty" yaml:"members,omitempty"`
Visibility string `json:"visibility,omitempty" yaml:"visibility,omitempty"`
WritePolicy string `json:"write_policy,omitempty" yaml:"write_policy,omitempty"`
Federation *protocol.RoomFederation `json:"federation,omitempty" yaml:"federation,omitempty"`
Credential *RoomCredentialConfig `json:"credential,omitempty" yaml:"credential,omitempty"`
}
type RoomCredentialConfig ¶ added in v0.1.14
type RoomCredentialConfig struct {
Token protocol.SecretString `json:"token,omitempty" yaml:"token,omitempty"`
TokenEnv string `json:"token_env,omitempty" yaml:"token_env,omitempty"`
TokenPath string `json:"token_path,omitempty" yaml:"token_path,omitempty"`
}
func (RoomCredentialConfig) HasTokenSource ¶ added in v0.1.14
func (c RoomCredentialConfig) HasTokenSource() bool
func (RoomCredentialConfig) ResolveToken ¶ added in v0.1.14
func (c RoomCredentialConfig) ResolveToken() (string, bool, error)
func (RoomCredentialConfig) ResolveTokenPath ¶ added in v0.1.14
func (c RoomCredentialConfig) ResolveTokenPath(baseDir string) RoomCredentialConfig
type RoomWriteback ¶ added in v0.1.15
type RoomWriteback struct {
ID string
Name string
Visibility string
WritePolicy string
Members []string
Federation *protocol.RoomFederation
Credential string
}
RoomWriteback is the plaintext rooms[] entry `moltnet room create` appends to a Moltnet config file. Like PairingWriteback (config_writeback.go), it carries Credential as a plain string instead of protocol.SecretString: a typed Config/RoomConfig round-trip would marshal an existing room's credential as [REDACTED] (SecretString's MarshalJSON/MarshalYAML), so WriteRoom edits the document as an untyped map exactly like WritePairing does, and this type mirrors that same plaintext-preserving shape for rooms.
type SQLiteStorageConfig ¶
type SQLiteStorageConfig struct {
Path string `json:"path,omitempty" yaml:"path,omitempty"`
}
type StorageConfig ¶
type StorageConfig struct {
Kind string `json:"kind" yaml:"kind"`
JSON JSONStorageConfig `json:"json,omitempty" yaml:"json,omitempty"`
SQLite SQLiteStorageConfig `json:"sqlite,omitempty" yaml:"sqlite,omitempty"`
Postgres PostgresStorageConfig `json:"postgres,omitempty" yaml:"postgres,omitempty"`
}
Source Files
¶
- app.go
- apply_config.go
- bind_warning.go
- config.go
- config_clock.go
- config_file.go
- config_follows.go
- config_load.go
- config_resolve.go
- config_room_credential.go
- config_secrets.go
- config_sniff.go
- config_writeback.go
- config_writeback_follows.go
- config_writeback_invite_receipt.go
- config_writeback_membership.go
- config_writeback_pairing_auth.go
- config_writeback_revoke.go
- config_writeback_room.go
- config_writeback_room_remove.go
- config_writeback_tokens.go
- network_keys.go
- pairing_bindings.go
- random.go