mirror of
https://github.com/qwibitai/nanoclaw.git
synced 2026-07-06 18:52:03 +08:00
197faaaa14
`claude setup-token` runs under script(1) so the browser OAuth flow keeps a TTY while we capture the printed token. On terminals that wrap long lines (e.g. sbx), the token lands split across lines with padding spaces, and the old parser — which stripped only ANSI codes and newlines — matched just the first fragment and failed the trailing `AA` check. Login succeeded; only our parse of the human-oriented output failed (`No sk-ant-oat…AA token found`). Add setup/lib/captured-token.ts: normalize the capture (strip ANSI/control bytes and all whitespace, un-wrapping the token) then extract. The TS caller (claude-assist.ts) and the bash registration script now share it, so the normalization rules can't drift. Placeholder lines like `export CLAUDE_CODE_OAUTH_TOKEN=<token>` are ignored. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
74 lines
3.2 KiB
TypeScript
74 lines
3.2 KiB
TypeScript
/**
|
|
* Parse a provider auth token out of interactive CLI output captured through
|
|
* a PTY (`script(1)`).
|
|
*
|
|
* Secret this module hides: the menagerie of PTY-capture artifacts that
|
|
* corrupt an otherwise whitespace-free secret. A real terminal wraps long
|
|
* lines, pads with spaces, and interleaves ANSI/control sequences, so a token
|
|
* the CLI printed as one string lands in the capture split across lines with
|
|
* escape codes embedded. Provider login itself succeeds — only our parse of
|
|
* the human-oriented output fails.
|
|
*
|
|
* A normalize step strips the capture artifacts; the extractor matches the
|
|
* token shape against the clean string. A future provider adds its own
|
|
* extractor here rather than regexing raw `script(1)` output.
|
|
*
|
|
* Runnable as a CLI for the bash callers that can't import TS:
|
|
* tsx setup/lib/captured-token.ts claude <capture-file>
|
|
* Prints the token and exits 0, or exits 1 with nothing on stdout.
|
|
*/
|
|
import fs from 'fs';
|
|
import { pathToFileURL } from 'url';
|
|
|
|
/* eslint-disable no-control-regex -- these patterns exist precisely to match
|
|
the ESC/control bytes a PTY capture is full of. */
|
|
// CSI sequences (colors, cursor moves): ESC [ , optional private '?' /
|
|
// parameter bytes, optional intermediate bytes, one final byte. Stripped
|
|
// explicitly because a colour reset mid-token (sk…\x1b[0m…AA) would otherwise
|
|
// leave a `[` that breaks the token's character run.
|
|
const CSI = /\x1b\[[0-9;?]*[ -/]*[@-~]/g;
|
|
// Everything <= space (control bytes incl. any stray ESC, CR/LF, tabs, and the
|
|
// wrap-padding spaces inserted mid-token) plus DEL. Tokens contain none of these.
|
|
const CONTROL_AND_SPACE = /[\x00-\x20\x7f]/g;
|
|
/* eslint-enable no-control-regex */
|
|
|
|
/**
|
|
* Collapse PTY-capture artifacts so a whitespace-free secret printed across
|
|
* wrapped lines becomes a single contiguous string. Drops ALL whitespace by
|
|
* design — these captures exist only to recover a token, never prose.
|
|
*/
|
|
function normalizeCapturedTerminalOutput(raw: string): string {
|
|
return raw.replace(CSI, '').replace(CONTROL_AND_SPACE, '');
|
|
}
|
|
|
|
// Claude subscription OAuth tokens: sk-ant-oat<base64url>AA. Bounded length
|
|
// keeps a greedy match from running off the end of the token.
|
|
const CLAUDE_OAUTH_TOKEN = /sk-ant-oat[A-Za-z0-9_-]{80,500}AA/g;
|
|
|
|
/**
|
|
* Extract the Claude OAuth token from a PTY capture of `claude setup-token`,
|
|
* or `null` if none is present. Returns the LAST match — setup-token can echo
|
|
* partial/intermediate output before the final token. Placeholder strings like
|
|
* `<token>` never match (they lack the `sk-ant-oat` prefix).
|
|
*/
|
|
export function extractClaudeOAuthToken(raw: string): string | null {
|
|
const matches = normalizeCapturedTerminalOutput(raw).match(CLAUDE_OAUTH_TOKEN);
|
|
return matches ? matches[matches.length - 1] : null;
|
|
}
|
|
|
|
function runCli(argv: string[]): number {
|
|
const [provider, file] = argv;
|
|
if (provider !== 'claude' || !file) {
|
|
process.stderr.write('usage: captured-token.ts claude <capture-file>\n');
|
|
return 2;
|
|
}
|
|
const token = extractClaudeOAuthToken(fs.readFileSync(file, 'utf-8'));
|
|
if (!token) return 1;
|
|
process.stdout.write(token);
|
|
return 0;
|
|
}
|
|
|
|
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
process.exit(runCli(process.argv.slice(2)));
|
|
}
|