/** * Login rules — per-harness patterns the login broker applies to terminal text. * * The broker runs a harness's own login command in a PTY and learns what is * happening purely from the screen: the sign-in URL, a one-time code, a * "paste the code" prompt, success and failure. Nothing here talks to a * harness API. * * Terminal text is messy, so it is normalized first: * - ANSI/OSC escapes are removed; OSC 8 hyperlink targets are kept as URL * candidates; cursor-forward (`CSI n C`) and cursor-to-column (`CSI n G`) * become spaces, which restores the gaps TUIs draw between words. * - TUIs such as Claude Code's may render words without spaces, so prompt, * success and failure patterns are matched against both the normalized text * and a copy with all spaces removed (patterns use `\s*` between words). * - A URL (or token) that a TUI wrapped at the terminal width continues on * the next line(s); {@link extractWrappedRuns} joins those back. * * Values are only accepted once they are complete — followed by more output — * so a URL that arrives split across two PTY chunks is never exposed half-way. * * Rule sets are built from real output: Claude Code 2.1.302 `claude auth login` * (captured 2026-10-17, up to the "Paste code here if prompted" prompt) and * codex-cli 0.156.1 `codex login --device-auth` (2026-09-25); see * login-rules.test.ts. * * Crewly never reads a credential off the screen: each harness's own login * command stores its login itself (Claude Code in its config dir or the * keychain), and Crewly only relays the sign-in link and the one-time code. * * @module services/harness/login-rules */ import type { HarnessId, LoginMethodId } from './harness.types.js'; /** Normalized terminal text. */ export interface NormalizedScreen { /** Text with escapes removed, `\n` line breaks, trailing spaces trimmed */ text: string; /** `text` with every space and tab removed (for TUIs that drop spaces) */ spaceless: string; /** Targets of OSC 8 hyperlinks seen in the raw output */ hyperlinks: string[]; } /** Patterns for one harness login method. */ export interface LoginRuleSet { harnessId: HarnessId; method: LoginMethodId; /** A URL candidate is the sign-in URL when this matches it */ urlPattern: RegExp; /** Capture group 1 is the one-time code the user types elsewhere */ userCodePattern?: RegExp; /** The harness is waiting for the user to type/paste something */ inputPromptPattern?: RegExp; /** Success text */ successPattern?: RegExp; /** Any match fails the login; the matched line becomes the message */ failurePatterns: readonly RegExp[]; /** Exit code 0 counts as success (Codex writes its own credentials) */ successOnExitZero: boolean; /** Confirm success with the harness's own status command before reporting it */ verifyAfterSuccess: boolean; } /** What the rules found on the screen. */ export interface LoginRuleMatch { url: string | null; userCode: string | null; needsInput: boolean; succeeded: boolean; /** Line that matched a failure pattern */ failureMessage: string | null; } /** * Patterns that look like credentials. Screen text exposed to front ends has * every match replaced with {@link HARNESS_CONSTANTS.LOGIN.REDACTED}. */ export declare const SECRET_PATTERNS: readonly RegExp[]; /** * Normalize raw PTY output for rule matching. * * @param raw - Raw bytes from the PTY, as a string * @returns Normalized text, a spaceless copy and OSC 8 hyperlink targets * * @example * ```ts * normalizeTerminalOutput('\x1b[1mPaste\x1b[1Ccode\x1b[0m\r\n').text; // 'Paste code\n' * ``` */ export declare function normalizeTerminalOutput(raw: string): NormalizedScreen; /** * Find runs (URLs, tokens) and join the ones a TUI wrapped across lines. * * A run that reaches the end of a line at least `minWrappedLineLength` long * continues on the next line when that line consists only of continuation * characters. Joining stops at the first line that is shorter than the * minimum (the last piece of a wrapped run) or that is not a continuation. * * A run is only returned when it is complete: it ends before the end of its * line, or a non-empty line follows it. A run still being written at the end * of the buffer (possibly continued by the next PTY chunk) is skipped — unless * `final` is set because the process has exited. * * @param text - Normalized text * @param runPattern - Pattern matching a run on one line (the `g` flag is added) * @param continuation - Pattern a whole continuation line must match * @param options - `minWrappedLineLength` (Infinity disables joining), `final` (no more output will come) * @returns Complete runs, in order of appearance * * @example * ```ts * extractWrappedRuns('https://x.io/' + 'a'.repeat(40) + '\nbcd\nnext line\n', /https:\/\/\S+/, /^[a-z]+$/); * // ['https://x.io/aaaa…abcd'] * ``` */ export declare function extractWrappedRuns(text: string, runPattern: RegExp, continuation: RegExp, options?: { minWrappedLineLength?: number; final?: boolean; }): string[]; /** * All complete URLs on the screen (wrapped ones joined), plus hyperlink targets. * * @param screen - Normalized screen * @param final - The process has exited: accept runs at the end of the buffer * @returns URL candidates */ export declare function extractUrlCandidates(screen: NormalizedScreen, final?: boolean): string[]; /** * Apply a rule set to the screen. * * @param rules - Rule set for the harness login method * @param screen - Whole session screen, normalized * @param options - `promptScreen`: screen since the user's last input (input prompt and failures are read * from it; defaults to `screen`); `final`: the process has exited * @returns What was found */ export declare function evaluateLoginRules(rules: LoginRuleSet, screen: NormalizedScreen, options?: { promptScreen?: NormalizedScreen; final?: boolean; }): LoginRuleMatch; /** * Replace anything that looks like a credential with a placeholder. * * Besides {@link SECRET_PATTERNS}, every known secret is removed — including * the pieces of one a TUI wrapped over several lines. * * @param text - Text to redact * @param knownSecrets - Secrets captured in this session * @returns Redacted text */ export declare function redactSecrets(text: string, knownSecrets?: readonly string[]): string; /** * Claude Code `claude auth login` (subscription login), Anthropic's own flow. * * It prints the sign-in link and "Paste code here if prompted >"; Claude Code * itself stores the login in its config dir (or the keychain). The login is * counted as done when the command exits 0 and a real Claude turn then works * (`verifyAfterSuccess`); Crewly does not need to know the success wording. */ export declare const CLAUDE_AUTH_LOGIN_RULES: LoginRuleSet; /** Codex `codex login --device-auth` (ChatGPT device-code login). */ export declare const CODEX_DEVICE_AUTH_RULES: LoginRuleSet; /** * The rule set for a harness login method. * * @param harnessId - Harness id * @param method - Login method id * @returns The rule set, or undefined when the method is not brokered */ export declare function getLoginRules(harnessId: string, method: string): LoginRuleSet | undefined; //# sourceMappingURL=login-rules.d.ts.map