import type { HqAlertRuleConfig } from './alerts.js'; import type { HqRedactionPolicy } from './protocol.js'; /** Current auth-file schema version. Bump on breaking shape changes. */ export declare const HQ_AUTH_FILE_VERSION: 1; /** Default HQ data directory: `~/.wrongstack/hq` (honors WRONGSTACK_HOME). */ export declare function defaultHqDataDir(): string; /** * Resolve an HQ data directory from an optional override. * * Resolution order: * 1. Explicit `override` (from `--data-dir`) — resolved against * `process.cwd()` if relative. * 2. `WRONGSTACK_HQ_DATA_DIR` env var (same resolution rules). * 3. `defaultHqDataDir()` — `~/.wrongstack/hq`. * * The env var exists so tests (and sandboxed runs) can redirect HQ state * away from the real user home without threading a CLI flag everywhere. */ export declare function resolveHqDataDir(override?: string, env?: NodeJS.ProcessEnv): string; /** * A generic HQ-issued token. Used for both browser tokens (validated on * `/ws/browser`) and client tokens (validated on `/ws/client`). The two * are stored in separate lists so a browser-only token cannot be replayed * against the client channel and vice versa. * * `capabilities` scopes what a token may do. When absent the token is * unrestricted (backward-compat with tokens minted before Phase 3). Known * capability strings: * - `control.enqueue` — browser token may enqueue commands to clients * - `control.execute` — client token may execute `run-command` commands * - `telemetry.publish` — client token may publish telemetry */ export interface HqToken { id: string; token: string; label?: string; createdAt: string; lastUsedAt?: string; /** * Optional capability scope. When absent, the token is unrestricted * (backward-compat). When present, only the listed capabilities are * granted. */ capabilities?: string[]; /** * Optional ISO timestamp after which the token is refused. When absent * (the pre-TTL default), the token never expires. The HQ server rejects * an expired token at both the HTTP and the WS-upgrade auth boundary, * so TTL rotation is enforced even for long-lived browser sessions. * * The timestamp is wall-clock (`new Date().toISOString()`). Clock skew * between the issuing HQ and the client is handled by passing a * `clockSkewMs` tolerance to {@link isTokenExpired} and * {@link tokenHasCapability} — callers that fetch tokens from a remote * HQ should use the server's clock (e.g. `Date.parse(expiresAt)`) * rather than the local wall clock. */ expiresAt?: string; } /** * True when a token has a wall-clock expiry that has already passed. * Returns `false` when `expiresAt` is absent (the pre-TTL default). * * The check is fail-open for malformed values: an unparseable timestamp * is treated as "no expiry" rather than "already expired", because the * latter would lock the operator out of their own HQ on a bad file edit. * Use {@link tokenHasCapability} at the auth boundary to also deny * capabilities for expired tokens. * * @param at - epoch milliseconds; defaults to `Date.now()`. Exposed so * tests can inject a fixed clock. * @param clockSkewMs - tolerance in milliseconds for clock skew between * the issuing node and the verifying node. When set, a token that has * technically expired by up to `clockSkewMs` is still accepted, so a * verifier whose clock is slightly ahead of the issuer does not * prematurely reject valid tokens. Defaults to 0 (no tolerance). */ export declare function isTokenExpired(token: Pick | undefined, at?: number, clockSkewMs?: number): boolean; /** * Check whether a token grants a capability. A token with no `capabilities` * field is unrestricted (backward-compat). Otherwise the capability must be * explicitly listed. **Expired tokens are refused regardless of capability** * — call this at the auth boundary so TTL rotation is enforced uniformly. * * @param clockSkewMs - passed through to {@link isTokenExpired}. When set, a * token that has expired by up to `clockSkewMs` is still accepted. Defaults * to 0. */ export declare function tokenHasCapability(token: HqToken | undefined, capability: string, clockSkewMs?: number): boolean; /** * Alias kept for backward-compat with Phase 3 callers/tests. New code * should prefer `HqToken`. */ export type HqBrowserToken = HqToken; /** On-disk shape of `/auth.json`. */ export interface HqRuntimeFile { url: string; updatedAt: string; pid?: number; } export interface HqAuthFile { version: typeof HQ_AUTH_FILE_VERSION; updatedAt: string; /** * Operator-configured redaction policy override. When present, the HQ * server applies these settings AFTER any publisher-declared policy — * i.e. the operator can always tighten, never loosen. */ redactionPolicy?: Partial; /** Browser tokens — validated on `/ws/browser` upgrades (Phase 3). */ browserTokens?: HqToken[]; /** Client tokens — validated on `/ws/client` upgrades (Phase 4). */ clientTokens?: HqToken[]; /** scrypt hash of the optional browser password login. */ passwordHash?: string; /** Secret used to sign browser session cookies. */ cookieSecret?: string; /** * Operator-configured alert-rule thresholds. When present, these override * the built-in defaults for the alert engine (cost ceiling, stale-machine * window, concurrency limit). Live-reloaded with the rest of the file. */ alertRules?: HqAlertRuleConfig; } /** An empty auth file — what a brand-new HQ install starts with. */ export declare function emptyHqAuthFile(): HqAuthFile; /** * Sentinel value `hqAuthContentHash` substitutes for every raw secret * (`HqToken.token`, `HqAuthFile.passwordHash`, `HqAuthFile.cookieSecret`) * before hashing. Exported so downstream consumers of the audit log can * recognize the redaction shape without hardcoding the literal — e.g. a * forensic tool that re-derives a `contentHash` from a known on-disk * `auth.json` can substitute this same sentinel and compare. */ export declare const HQ_AUTH_CONTENT_HASH_REDACTED = ""; /** * Compute a SHA-256 content hash over a *redacted* projection of an * `HqAuthFile`. The projection replaces every raw token string, the * `passwordHash`, and the `cookieSecret` with constant sentinels, so: * * - the audit log never holds derivable token material (a hash of a * redacted projection can't be reversed to recover the originals), * - two files that differ only in their secrets hash identically * (reissuing a token without changing its id/label/expiry does not * change the hash), and * - the hash still flips whenever the structural state the operator * cares about (version, token ids/labels/capabilities/expiries, * alert rules, redaction policy) changes. * * Returns `undefined` when hashing or serialization throws (e.g. a * future schema addition that isn't JSON-serializable) — callers * should pass that through as an absent `contentHash` field rather * than failing the audit append, matching the audit module's * best-effort contract. */ export declare function hqAuthContentHash(file: HqAuthFile): string | undefined; /** Path to `auth.json` under the given data directory. */ export declare function hqAuthFilePath(dataDir: string): string; /** Path to the best-effort runtime endpoint marker under the given data directory. */ export declare function hqRuntimeFilePath(dataDir: string): string; export declare function writeHqRuntimeFile(dataDir: string, file: Omit): Promise; export declare function readHqRuntimeFileSync(dataDir: string): HqRuntimeFile | undefined; /** * Read `auth.json` from disk. Returns `emptyHqAuthFile()` when the file does * NOT exist (ENOENT) — the only case treated as intentional open mode. * * Throws for all other read failures (EACCES, EIO, etc.), malformed JSON, * and unsupported schema versions. A corrupt or unreadable auth file must * never silently open the server. * * Callers that want to handle errors gracefully (e.g. the live-reload * watcher which should preserve the last-known-good state) should catch * the thrown error. */ export declare function readHqAuthFile(dataDir: string, _opts?: { warn?: (msg: string) => void; }): Promise; /** * Write `auth.json` atomically with mode 0o600. Creates the data directory * if needed. Throws on I/O failure — callers (CLI commands) should surface * the error to the operator. */ export declare function writeHqAuthFile(dataDir: string, file: HqAuthFile): Promise; export interface EnsureHqFirstRunAuthOptions { warn?: (msg: string) => void; /** Optional browser password login. Hashed with scrypt before storage. */ password?: string; /** * Optional time-to-live (milliseconds) stamped on the first-run browser * and client tokens. When set, both tokens carry an `expiresAt` and the * HQ server will refuse them once it passes. Default: no expiry. * * Only affects tokens minted by THIS call (the first run). Tokens added * later via `wstack hq token create` choose their own TTL. */ tokenTtlMs?: number | undefined; /** * Optional `actor` string recorded in `auth-audit.jsonl` for the two * `first-run` entries this function emits (one per minted token). The * CLI fills this with `user@host`; when omitted the entry carries no * `actor` field. Never contains the token string. */ actor?: string | undefined; } export interface EnsureHqFirstRunAuthResult { authFile: HqAuthFile; created: boolean; browserToken?: HqToken; clientToken?: HqToken; } /** * Ensure a brand-new HQ data directory has the auth required for safe * first-run operation. Only a missing auth.json is bootstrapped; an existing * file, including one with empty token arrays, is treated as operator intent. * * Caller surface: the sole production caller is `startHqServer` in * `packages/cli/src/hq-server.ts`, which threads `actor: resolveAuditActor()` * so both first-run audit entries carry `user@host`. Test callers live under * `packages/core/tests/hq/` (token-ttl, first-run-audit, auth-store). Any * new production caller MUST pass `actor` for the audit log to carry * forensic identity — see `EnsureHqFirstRunAuthOptions.actor`. */ export declare function ensureHqFirstRunAuthFile(dataDir: string, opts?: EnsureHqFirstRunAuthOptions): Promise; /** * Load → mutate → write. The mutator receives the current file (or an empty * one) and returns the next file. Use this for any read-modify-write cycle * to avoid clobbering concurrent edits from another CLI invocation. * * Note: this does NOT take a file lock. HQ auth edits are rare (operator * running `wstack --hq-token add` etc.) and the atomic rename protects * against torn writes; a race between two concurrent edits would be * last-write-wins, which is acceptable for this low-frequency file. */ export declare function mutateHqAuthFile(dataDir: string, mutator: (current: HqAuthFile) => HqAuthFile | Promise, opts?: { warn?: (msg: string) => void; }): Promise; /** * Hash a browser password for storage in `auth.json`. Uses scrypt with a * random salt; the returned string is `scrypt$$` in base64url. */ export declare function hashHqPassword(password: string): Promise; /** * Verify a browser password against a stored scrypt hash. */ export declare function verifyHqPassword(password: string, hash: string): Promise; /** Mint a secret used to sign browser session cookies. */ export declare function mintHqCookieSecret(): string; /** * Options for {@link mintHqToken}. * * - `label` — human-readable identifier persisted alongside the token. * - `ttlMs` — when set, the token is stamped with `expiresAt = now + ttlMs`. * Absent (the default) means the token never expires, preserving the * pre-TTL contract. * - `now` — epoch milliseconds override for deterministic tests. */ export interface MintHqTokenOptions { label?: string | undefined; ttlMs?: number | undefined; now?: number | undefined; } /** * Mint a fresh token. Used for both browser and client tokens; the caller * decides which list to append to. * * Pass an options object to stamp an `expiresAt`; the bare-string overload * (`mintHqToken('my-label')`) is preserved for backward-compat. */ export declare function mintHqToken(labelOrOptions?: string | MintHqTokenOptions): HqToken; /** * Backward-compat alias for `mintHqToken`. Phase 3 callers/tests use this. */ export declare const mintHqBrowserToken: typeof mintHqToken; /** * Watch `auth.json` for changes and invoke `onChange` with the freshly-read * file. Returns a `close()` function that stops watching. * * The watcher debounces events (default 200ms) because most editors do a * tmp+rename dance that emits multiple events. On any read failure (file * deleted, corrupt, etc.) the `warn` callback is invoked with the same * semantics as `readHqAuthFile`; the watcher stays active so a future * valid write re-triggers the callback. * * Notes: * - `fs.watch` is best-effort across platforms. On some network * filesystems events may not fire; the operator must restart the server * to pick up changes in that case. * - The watcher polls the parent directory (not the file itself) so that * atomic rename events surface reliably. */ export declare function watchHqAuthFile(dataDir: string, onChange: (file: HqAuthFile) => void, opts?: { warn?: (msg: string) => void; debounceMs?: number; }): { close: () => void; }; //# sourceMappingURL=auth-store.d.ts.map