/** * Dashboard auth — exactly two modes (this file is the authority): * * Mode A — local-only (default): the server binds loopback only. Requests are * additionally checked for loopback source address AND an allowlisted Host * header (DNS-rebinding defense: a malicious website can point its own domain * at 127.0.0.1 and drive the API from the victim's browser unless Host is * validated). No login, no pairing. * * Mode B — remote (explicit opt-in): requires Tailscale. Enforcement layers, * all fail-closed: (1) Tailscale identity resolution of the peer address, * (2) identity allowlist (empty allowlist = deny all), (3) first-login * rotating pairing code (visible only from the host/local dashboard), * (4) signed per-device cookie thereafter. * * There is no LAN mode. Any auth-subsystem error denies the request. */ export declare const DEFAULT_PAIRING_TTL_DAYS = 180; export declare const MIN_PAIRING_TTL_DAYS = 1; export declare const MAX_PAIRING_TTL_DAYS = 3650; /** Normalize an address for comparison (strip IPv6-mapped IPv4 prefix and zone). */ export declare function normalizeAddress(address: string | undefined): string; /** True when the (normalized) address is a loopback address. */ export declare function isLoopbackAddress(address: string | undefined): boolean; /** * Validate a Host header against the loopback allowlist. Rejecting foreign * hosts breaks DNS rebinding: the attacker's page can reach 127.0.0.1, but its * requests carry the attacker's hostname in Host. */ export declare function isAllowedLocalHost(hostHeader: string | undefined): boolean; export interface TailscaleIdentity { /** Login name (e.g. "alice@example.com") — the allowlist unit. */ loginName: string; /** Device host name, when known. */ device?: string; } export interface TailscaleResolver { /** Resolve a peer IP to a Tailscale identity, or null when unknown. */ resolve(address: string): Promise; } export type TailscaleWhoisRunner = (address: string) => Promise<{ stdout: string; }>; export declare class TailscaleResolverError extends Error { readonly kind: "timeout" | "execution" | "parse" | "schema"; constructor(kind: "timeout" | "execution" | "parse" | "schema"); } /** Peer-specific Tailscale identity resolution with same-peer in-flight coalescing. */ export declare class TailscaleWhoisResolver implements TailscaleResolver { private readonly runWhois; private readonly inFlight; constructor(runWhois?: TailscaleWhoisRunner); resolve(address: string): Promise; private lookup; } /** @deprecated Use TailscaleWhoisResolver. Retained for API compatibility. */ export declare class TailscaleStatusResolver extends TailscaleWhoisResolver { } export interface PairedDevice { id: string; identity: string; device?: string; createdAt: string; expiresAt: string; } export interface StoredPairing extends PairedDevice { /** HMAC of the device token (raw token never stored). */ tokenHmac: string; /** UTC date of the last expiry warning claimed by this pairing. */ lastExpiryWarningUtcDate?: string; } export interface PairingState { pairings: StoredPairing[]; consumedPairingWindows: number[]; /** Whole-day lifetime for newly created pairings. Absent in legacy files. */ pairingTtlDays?: number; } export interface PairingExpiryStatus { warning?: { expiresAt: string; }; nextCheckAt?: string; } export interface PairingStorage { load(): Promise; save(state: PairingState): Promise; } /** In-memory storage — used in tests and as the base for the file store. */ export declare class MemoryPairingStorage implements PairingStorage { private state; load(): Promise; save(state: PairingState): Promise; } export interface DashboardAuthOptions { /** Remote (Tailscale) mode. Default false — loopback only. */ remoteEnabled?: boolean; /** Allowed Tailscale login names. Empty = deny all remote. */ allowedIdentities?: string[]; /** Test/backward-compatible default when no persisted day setting exists. Production defaults to 180 days. */ pairingTtlMs?: number; resolver?: TailscaleResolver; storage?: PairingStorage; /** HMAC/TOTP secret for device tokens and pairing codes. Production passes a per-install persisted secret. */ secret?: Buffer; /** Failed PIN attempts before temporary lockout. Default 5. */ pairingMaxAttempts?: number; /** Temporary lockout duration after too many failed PIN attempts. Default 60s. */ pairingLockoutMs?: number; /** Security/audit log sink for repeated failed pairing attempts. */ logger?: (line: string) => void; /** Clock override for tests. */ now?: () => number; } export type AuthDecision = { allowed: true; mode: "local"; } | { allowed: true; mode: "remote"; identity: TailscaleIdentity; pairing: PairedDevice; } | { allowed: false; status: number; reason: string; /** Set when an allowed identity needs pairing-code entry. */ needsPairing?: boolean; identity?: TailscaleIdentity; }; export interface AuthRequestInfo { remoteAddress: string | undefined; hostHeader: string | undefined; /** Origin header when present. Non-loopback origins are rejected on local requests. */ originHeader: string | undefined; /** Value of the dashboard device cookie, when present. */ deviceToken: string | undefined; } export declare class DashboardAuth { private readonly remoteEnabled; private readonly allowedIdentities; private readonly defaultPairingTtlMs; private readonly resolver; private readonly storage; private readonly secret; private readonly pairingMaxAttempts; private readonly pairingLockoutMs; private readonly logger; private readonly now; private pairingMutation; private readonly pairingFailures; constructor(options?: DashboardAuthOptions); get isRemoteEnabled(): boolean; private defaultPairingTtlDays; getPairingSettings(): Promise<{ pairingTtlDays: number; }>; setPairingSettings(pairingTtlDays: number): Promise<{ pairingTtlDays: number; }>; private hmac; private pairingCodeForWindow; /** Current RFC-6238-style rotating code for pairing new remote devices. */ currentPairingCode(): { code: string; expiresInMs: number; }; private currentPairingWindow; private matchingPairingCodeWindow; private pruneConsumedPairingWindows; private samePairingWindows; private pairingFailureKey; private assertPairingNotLocked; private recordPairingFailure; private clearPairingFailures; private withPairingMutation; /** * Authenticate a request. Fail-closed: any resolver/storage error results in * a deny, never a pass-through. */ authenticate(info: AuthRequestInfo): Promise; private authenticateInner; /** * Complete pairing for an allowed remote identity using the current rotating * code. Returns the device token to set as a cookie. Throws (with `status`) on * any failure. */ pair(info: AuthRequestInfo, code: string): Promise<{ token: string; device: PairedDevice; }>; private toPairedDevice; /** List paired devices (live only). */ listDevices(): Promise; /** Remove a paired device by id. Returns true when something was removed. */ unpair(deviceId: string): Promise; private findPairing; /** Atomically claim any due warning and return the next useful browser check time. */ claimPairingExpiryStatus(pairingId: string): Promise; private loadLive; private loadLiveState; } //# sourceMappingURL=auth.d.ts.map