ktg-plugin-marketplace/plugins/ms-ai-architect/scripts/kb-update/lib/auth-mode.mjs
Kjell Tore Guttormsen 2b3f544f86 feat(ms-ai-architect): add lib/auth-mode for cron-safe auth detection [skip-docs]
Foundation lib for v1.12.0 cron rewrite. Detects which Claude auth mode is
in scope and rejects modes that are architecturally incompatible with cron.

Resolution order:
- ANTHROPIC_API_KEY env-var → 'api-key'
- CLAUDE_CODE_OAUTH_TOKEN env-var → 'long-oauth'
- ~/.claude.json onboarded + runner exit 0 → 'subscription-browser-only'
- otherwise → 'unauthenticated'

Subscription browser-OAuth tokens expire ~15h and cannot survive cron — the
detector flags them explicitly so validateAuthForCron throws EAUTHCRON with
a remediation message pointing to `claude setup-token` or ANTHROPIC_API_KEY.

Both runner (subprocess invoker) and claudeJsonPath (~/.claude.json) are
dependency-injected. Tests stub them — no real subprocess spawn, no home-
directory reads.

15/15 tests pass: precedence, env-var detection, onboarded subscription,
non-onboarded fallback, validateAuthForCron throw paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 10:52:53 +02:00

99 lines
3.3 KiB
JavaScript

// auth-mode.mjs — Detect and validate Claude auth mode for cron-safe runs.
// Zero dependencies. The detector and validator are pure-testable: both
// `runner` (claude CLI invoker) and `claudeJsonPath` (~/.claude.json) are
// dependency-injected so tests stub them rather than spawning real subprocess
// or touching the user's home directory.
//
// Subscription browser-OAuth tokens expire ~15h and are architecturally
// incompatible with cron. This lib surfaces that case as a hard fail so the
// installer/cron-runner can refuse to proceed.
import { readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
import { execFileSync } from 'node:child_process';
/**
* Default subprocess runner — invokes a command and returns its exit code.
* Returns 0 on success, the actual exit code on failure, 127 on spawn error.
*/
function defaultRunner(cmd, args) {
try {
execFileSync(cmd, args, { stdio: 'ignore' });
return 0;
} catch (err) {
if (typeof err.status === 'number') return err.status;
return 127;
}
}
/**
* Safely read and parse a Claude config JSON file. Returns null on any error.
* @param {string} path
* @returns {object|null}
*/
export function readClaudeJson(path) {
try {
const text = readFileSync(path, 'utf8');
const obj = JSON.parse(text);
return obj && typeof obj === 'object' ? obj : null;
} catch {
return null;
}
}
/**
* Detect the active Claude authentication mode.
*
* Resolution order:
* 1. ANTHROPIC_API_KEY env-var → 'api-key'
* 2. CLAUDE_CODE_OAUTH_TOKEN env-var → 'long-oauth'
* 3. ~/.claude.json onboarded + `claude auth status` exits 0 → 'subscription-browser-only'
* 4. otherwise → 'unauthenticated'
*
* @param {object} [opts]
* @param {(cmd: string, args: string[]) => number} [opts.runner]
* @param {string} [opts.claudeJsonPath]
* @param {object} [opts.env] — defaults to process.env
* @returns {'api-key'|'long-oauth'|'subscription-browser-only'|'unauthenticated'}
*/
export function detectAuthMode(opts = {}) {
const env = opts.env ?? process.env;
const runner = opts.runner ?? defaultRunner;
const claudeJsonPath = opts.claudeJsonPath ?? join(homedir(), '.claude.json');
if (env.ANTHROPIC_API_KEY && env.ANTHROPIC_API_KEY.trim() !== '') {
return 'api-key';
}
if (env.CLAUDE_CODE_OAUTH_TOKEN && env.CLAUDE_CODE_OAUTH_TOKEN.trim() !== '') {
return 'long-oauth';
}
const claudeJson = readClaudeJson(claudeJsonPath);
if (!claudeJson || claudeJson.hasCompletedOnboarding !== true) {
return 'unauthenticated';
}
const exitCode = runner('claude', ['auth', 'status']);
return exitCode === 0 ? 'subscription-browser-only' : 'unauthenticated';
}
/**
* Throw a clear error if the detected mode is incompatible with cron.
* Subscription-browser-only OAuth dies after ~15h; unauthenticated has no
* credential. Both must be rejected before headless cron runs.
*
* @param {string} mode
* @throws {Error} with code 'EAUTHCRON' if mode is not safe for cron
*/
export function validateAuthForCron(mode) {
if (mode === 'api-key' || mode === 'long-oauth') return;
const e = new Error(
`Auth mode "${mode}" is not safe for cron. ` +
'Run `claude setup-token` to generate a long-lived OAuth, ' +
'or set ANTHROPIC_API_KEY in the cron environment.'
);
e.code = 'EAUTHCRON';
e.detectedMode = mode;
throw e;
}