/** * Persisted state of one `commandmate remote` session (Issue #1937, R9). * * Design: `docs/design/remote-qr-pairing-1937.md` §6.3. * * ## Why this file exists at all * * `remote stop` must undo exactly what `remote up` created and nothing else. * The Provider interface enforces that structurally — `stop()` takes a * `RemoteHandle` and there is no `reset()` / `cleanupAll()` to reach for — but * the two commands are separate processes, so the handle has to survive on * disk between them. This module is that hop, and nothing more: it stores, it * does not interpret. * * ## Why "unreadable" is not "guess" * * `readRemoteState()` answers `null` for absent, unparseable and structurally * wrong alike, and `remote stop` turns all three into "I do not know what to * clean up" and exits SUCCESS (§6.3-4). That is deliberate and is the whole * reason the reader validates rather than casts: a half-read state file must * never become a Provider teardown aimed at configuration the user created. * `stop.ts` treats a stale PID file the same way. * * ## Format * * Plain JSON with a `schemaVersion`, not `PidManager`'s hybrid * "bare PID line + JSON line" shape. That format exists so a CLI predating * #1354 can still `parseInt()` the first line; nothing has ever read this file, * so there is no such reader to stay compatible with, and a PID is not what * this record is about. * * The file is written 0600 like the pairing handoff. It holds no secret — the * pairing code and the session token are never in it — but it names a public * URL that reaches this machine, and that is not something to leave * world-readable. */ import type { RemoteHandle, RemoteProviderId } from '../../lib/remote'; /** File name inside the config directory (`~/.commandmate` for global installs). */ export declare const REMOTE_STATE_FILE_NAME = "remote.json"; /** Owner read/write only. Asserted by tests, not merely intended. */ export declare const REMOTE_STATE_FILE_MODE = 384; /** * Bumped when a field changes meaning. A reader that does not recognise the * version returns null, which routes to "nothing to clean up" rather than to a * teardown driven by a record it does not understand. */ export declare const REMOTE_STATE_SCHEMA_VERSION = 1; /** * How far authentication reached for one remote session (Issue #2489). * * - `all` — every listener authenticates. The pre-#2489 behaviour and the * default, and what an older state file (which has no such field) means. * - `remote-only` — the Provider's listener authenticates; the loopback * listener the user's own browser and CLI talk to does not. */ export type RemoteAuthScope = 'all' | 'remote-only'; /** What one remote session recorded for the commands that come after it. */ export interface RemoteState { schemaVersion: number; /** Which Provider is holding the door open. */ provider: RemoteProviderId; /** Public URL the Provider published. Not a secret; the pairing code is. */ url: string; /** ISO timestamp of when `remote up` finished. */ startedAt: string; /** * Epoch ms at which the outside door closes (`--expires`). * * This is the Provider's deadline only. The server's own token expiry is * fixed independently at startup by `CM_AUTH_EXPIRE` -> `computeExpireAt()`, * and the server is NOT stopped when this passes: killing it would take the * user's local session down with the remote one (§5.3). */ expiresAt: number; /** Where the pairing handoff went, and when the code dies. */ pairing: { /** Absolute path passed to the server as `CM_REMOTE_PAIRING_FILE`. */ filePath: string; /** Epoch ms after which the pairing code is refused (`--pairing-expires`). */ expiresAt: number; }; /** The Provider's own receipt. The only thing `stop()` is allowed to act on. */ handle: RemoteHandle; /** The server this session exposed, for `remote status`. */ server: { pid: number | null; port: number; /** * Issue #2489: the extra loopback port the Provider was pointed at, when * `--auth remote-only` was used. Null (or absent, in a file written before * #2489) when the Provider fronts `port` itself. * * Recorded for `remote status` only. `remote stop` does not close it: the * listener lives in the server process and there is no way to retract one * socket without restarting the server, which §5.3 forbids — stopping the * server would take the local session down with the remote one. Closing the * Provider is what makes it unreachable, and the listener itself is * loopback-only and authenticates every request, so what is left behind is a * socket no less protected than the main port under `--auth all`. */ remoteIngressPort?: number | null; }; /** * Issue #2489: the `--auth` value this session ran with. * * Optional so a state file written before #2489 still validates — `remote * stop` reading `null` for an unrecognised record would orphan a live tunnel, * which is a far worse failure than a missing display field. Absent reads as * `all`, which is what those sessions were. */ authScope?: RemoteAuthScope; } /** * Absolute path of the state file. * * @returns `/remote.json` */ export declare function getRemoteStatePath(): string; /** * Write the state file with mode 0600. * * An existing file is unlinked first: `writeFileSync`'s `mode` is only honoured * on creation, so writing over a file left world-readable by something else * would silently keep those permissions. Mirrors `writePairingHandoff()`. * * @param state - The session record to persist * @param filePath - Override for the destination, for tests */ export declare function writeRemoteState(state: RemoteState, filePath?: string): void; /** * Read and validate the state file. * * @param filePath - Override for the source, for tests * @returns The state, or null when it is absent, unreadable or not this schema */ export declare function readRemoteState(filePath?: string): RemoteState | null; /** * Delete the state file. Idempotent — a missing file is success. * * @param filePath - Override for the target, for tests */ export declare function removeRemoteState(filePath?: string): void; /** * Structural check for a parsed state file. * * Every field `remote stop` and `remote status` read is checked, because the * cost of being wrong is asymmetric: a rejected record costs the user one * "nothing to clean up" message, an accepted-but-wrong one aims a Provider * teardown at a handle nobody wrote. * * @param value - Parsed JSON * @returns true when the value is a state record of the current schema */ export declare function isRemoteState(value: unknown): value is RemoteState; //# sourceMappingURL=remote-state.d.ts.map