diff --git a/src/cli/commands/auth.ts b/src/cli/commands/auth.ts deleted file mode 100644 index cf5f8e8..0000000 --- a/src/cli/commands/auth.ts +++ /dev/null @@ -1,155 +0,0 @@ -import chalk from 'chalk'; -import { Command } from 'commander'; -import { - loadTokens, - clearTokens, - saveTokens, - resolveToken, - startCallbackAuth, - getAuthFilePath, -} from '../../platform/auth.js'; -import type { AuthTokens } from '../../platform/types.js'; -import { createSpinner } from '../ui.js'; - -export const authCommand = new Command('auth') - .description('Authenticate with the Guard0 platform'); - -// ─── g0 auth login ─────────────────────────────────────────────────────────── - -const loginCommand = new Command('login') - .description('Authenticate with Guard0 platform via browser') - .action(async () => { - if (process.env.G0_API_TOKEN) { - console.log(chalk.yellow(' G0_API_TOKEN is set. Unset it to use interactive login.')); - return; - } - - // Check if already authenticated - const existing = loadTokens(); - if (existing && resolveToken()) { - console.log(chalk.green(` Already authenticated${existing.email ? ` as ${existing.email}` : ''}.`)); - console.log(chalk.dim(' Run `g0 auth logout` first to re-authenticate.')); - return; - } - - console.log(chalk.bold('\n Guard0 Authentication\n')); - - try { - const { authUrl, result } = await startCallbackAuth(); - - console.log(chalk.bold(' Open this URL in your browser:\n')); - console.log(chalk.cyan(` ${authUrl}\n`)); - - // Try to open browser automatically - try { - const { exec } = await import('node:child_process'); - const cmd = process.platform === 'darwin' ? 'open' - : process.platform === 'win32' ? 'start' - : 'xdg-open'; - exec(`${cmd} "${authUrl}"`); - } catch { - // Non-fatal: user can open manually - } - - const spinner = createSpinner('Waiting for authorization...'); - spinner.start(); - - const callbackResult = await result; - - const tokens: AuthTokens = { - accessToken: callbackResult.token, - expiresAt: Date.now() + 10 * 365 * 24 * 60 * 60 * 1000, // API keys don't expire - email: callbackResult.email, - userId: callbackResult.userId, - orgId: callbackResult.orgId, - }; - - await saveTokens(tokens); - spinner.stop(); - - console.log(chalk.green(`\n Authenticated successfully${tokens.email ? ` as ${chalk.bold(tokens.email)}` : ''}!`)); - console.log(chalk.dim(` Token stored in ${getAuthFilePath()}\n`)); - } catch (err) { - console.error(chalk.red(` Login failed: ${err instanceof Error ? err.message : err}`)); - process.exit(1); - } - }); - -// ─── g0 auth logout ────────────────────────────────────────────────────────── - -const logoutCommand = new Command('logout') - .description('Remove stored authentication tokens') - .action(() => { - clearTokens(); - console.log(chalk.green(' Logged out. Token cleared.')); - - if (process.env.G0_API_TOKEN) { - console.log(chalk.yellow(' Note: G0_API_TOKEN env var is still set.')); - } - }); - -// ─── g0 auth status ────────────────────────────────────────────────────────── - -const statusCommand = new Command('status') - .description('Show current authentication status') - .action(() => { - console.log(chalk.bold('\n Auth Status\n')); - - // Check env var - if (process.env.G0_API_TOKEN) { - console.log(chalk.green(' Authenticated via G0_API_TOKEN')); - console.log(''); - return; - } - - // Check stored tokens - const tokens = loadTokens(); - if (!tokens) { - console.log(chalk.yellow(' Not authenticated.')); - console.log(chalk.dim(' Run `g0 auth login` to authenticate.\n')); - return; - } - - // API keys don't expire - const isApiKey = tokens.accessToken.startsWith('g0_'); - - if (!isApiKey) { - const expired = tokens.expiresAt < Date.now(); - if (expired) { - console.log(chalk.yellow(' Token expired.')); - console.log(chalk.dim(' Run `g0 auth login` to re-authenticate.\n')); - return; - } - } - - console.log(chalk.green(' Authenticated')); - if (tokens.email) console.log(` Email: ${tokens.email}`); - if (tokens.userId) console.log(` User ID: ${chalk.dim(tokens.userId)}`); - if (tokens.orgId) console.log(` Org ID: ${chalk.dim(tokens.orgId)}`); - if (isApiKey) { - console.log(` Type: ${chalk.dim('API Key')}`); - } else { - const expiresIn = Math.round((tokens.expiresAt - Date.now()) / 1000 / 60); - console.log(` Expires: ${chalk.dim(`in ${expiresIn} minutes`)}`); - } - console.log(''); - }); - -// ─── g0 auth token ─────────────────────────────────────────────────────────── - -const tokenCommand = new Command('token') - .description('Print the current access token (for piping to other tools)') - .action(() => { - const token = resolveToken(); - if (!token) { - console.error('Not authenticated'); - process.exit(1); - } - // Print raw token to stdout for piping - process.stdout.write(token); - }); - -authCommand.addCommand(loginCommand); -authCommand.addCommand(logoutCommand); -authCommand.addCommand(statusCommand); -authCommand.addCommand(tokenCommand); diff --git a/src/cli/commands/detect.ts b/src/cli/commands/detect.ts index 77cbdd3..163dd0d 100644 --- a/src/cli/commands/detect.ts +++ b/src/cli/commands/detect.ts @@ -5,9 +5,8 @@ export const detectCommand = new Command('detect') .description('Detect MDM enrollment, running AI agents, and host security posture') .option('--json', 'Output as JSON') .option('-q, --quiet', 'Suppress terminal output') - .option('--upload', 'Upload results to Guard0 platform') .option('--section ', 'Run only a specific section (mdm|agents|host)') - .action(async (options: { json?: boolean; quiet?: boolean; upload?: boolean; section?: string }) => { + .action(async (options: { json?: boolean; quiet?: boolean; section?: string }) => { const spinner = options.quiet ? null : createSpinner('Detecting environment...'); spinner?.start(); @@ -35,11 +34,6 @@ export const detectCommand = new Command('detect') if (agents) output.agents = agents; if (host) output.host = host; console.log(JSON.stringify(output, null, 2)); - - // Upload if requested - if (options.upload) { - await uploadDetectResults(output, options.quiet); - } return; } @@ -173,15 +167,6 @@ export const detectCommand = new Command('detect') } console.log(''); - - // ── Upload ─────────────────────────────────────────────────────────── - if (options.upload) { - const output: Record = {}; - if (mdm) output.mdm = mdm; - if (agents) output.agents = agents; - if (host) output.host = host; - await uploadDetectResults(output, options.quiet); - } } catch (err) { spinner?.stop(); const message = err instanceof Error ? err.message : String(err); @@ -193,27 +178,3 @@ export const detectCommand = new Command('detect') } }); -async function uploadDetectResults(data: Record, quiet?: boolean): Promise { - try { - const { shouldUpload, uploadResults, collectMachineMeta, detectCIMeta } = await import('../../platform/upload.js'); - const uploadDecision = await shouldUpload(true); - if (!uploadDecision.upload) { - if (!quiet) { - console.error(' Upload skipped: not authenticated. Run `g0 auth login` first.'); - } - return; - } - const response = await uploadResults({ - type: 'host-hardening' as const, - machine: collectMachineMeta(), - result: data as unknown as import('../../endpoint/host-hardening.js').HostHardeningResult, - }); - if (response && !quiet) { - console.log(`\n Uploaded to: ${response.url}`); - } - } catch (err) { - if (!quiet) { - console.error(` Upload failed: ${err instanceof Error ? err.message : err}`); - } - } -} diff --git a/src/cli/commands/endpoint.ts b/src/cli/commands/endpoint.ts index f16fcbe..cf47645 100644 --- a/src/cli/commands/endpoint.ts +++ b/src/cli/commands/endpoint.ts @@ -4,8 +4,6 @@ import { Command } from 'commander'; import { loadDaemonConfig } from '../../daemon/config.js'; import { readPid } from '../../daemon/process.js'; import { getMachineId } from '../../platform/machine-id.js'; -import { isAuthenticated } from '../../platform/auth.js'; -import { collectMachineMeta, shouldUpload, uploadResults } from '../../platform/upload.js'; import { listMCPServers } from '../../mcp/analyzer.js'; import { scanEndpoint } from '../../endpoint/scanner.js'; import { reportEndpointTerminal } from '../../reporters/endpoint-terminal.js'; @@ -16,7 +14,6 @@ import type { EndpointStatusResult } from '../../types/endpoint.js'; async function runEndpointScan(options: { json?: boolean; - upload?: boolean; banner?: boolean; network?: boolean; artifacts?: boolean; @@ -41,16 +38,6 @@ async function runEndpointScan(options: { } else { reportEndpointTerminal(result); } - - // Upload - const { upload } = await shouldUpload(options.upload); - if (upload) { - const machine = collectMachineMeta(); - await uploadResults({ type: 'endpoint', machine, result }); - if (!options.json) { - console.log(chalk.dim(' Results uploaded to Guard0 platform.\n')); - } - } } // ─── Shared options ───────────────────────────────────────────────────────── @@ -58,8 +45,6 @@ async function runEndpointScan(options: { function addScanOptions(cmd: Command): Command { return cmd .option('--json', 'Output as JSON') - .option('--upload', 'Upload results to Guard0 platform') - .option('--no-upload', 'Disable upload') .option('--no-banner', 'Suppress the g0 banner') .option('--no-network', 'Skip network port scanning') .option('--no-artifacts', 'Skip credential and data store scanning') @@ -76,7 +61,6 @@ export const endpointCommand = new Command('endpoint') addScanOptions(endpointCommand) .action(async (options: { json?: boolean; - upload?: boolean; banner?: boolean; network?: boolean; artifacts?: boolean; @@ -95,7 +79,6 @@ const scanSubcommand = new Command('scan') addScanOptions(scanSubcommand) .action(async (options: { json?: boolean; - upload?: boolean; banner?: boolean; network?: boolean; artifacts?: boolean; @@ -116,7 +99,7 @@ const statusSubcommand = new Command('status') const machineId = getMachineId(); const config = loadDaemonConfig(); const pid = readPid(config.pidFile); - const authed = isAuthenticated(); + const authed = false; let mcpServerCount = 0; try { diff --git a/src/cli/commands/flows.ts b/src/cli/commands/flows.ts index 9f34b91..09f73e2 100644 --- a/src/cli/commands/flows.ts +++ b/src/cli/commands/flows.ts @@ -15,13 +15,11 @@ export const flowsCommand = new Command('flows') .option('--json', 'Output as JSON') .option('-o, --output ', 'Write output to file') .option('--config ', 'Path to config file (default: .g0.yaml)') - .option('--upload', 'Upload results to Guard0 platform') .option('--no-banner', 'Suppress the g0 banner') .action(async (targetPath: string, options: { json?: boolean; output?: string; config?: string; - upload?: boolean; banner?: boolean; }) => { let resolvedPath: string; @@ -83,29 +81,6 @@ export const flowsCommand = new Command('flows') console.log(`JSON flow analysis also written to: ${options.output}`); } } - // Upload to platform - const { shouldUpload } = await import('../../platform/upload.js'); - const uploadDecision = await shouldUpload(options.upload); - if (uploadDecision.upload) { - try { - if (uploadDecision.isAuto) { - console.log('\n Auto-uploading (authenticated)...'); - } - const { uploadResults, collectProjectMeta, collectMachineMeta, detectCIMeta } = await import('../../platform/upload.js'); - const response = await uploadResults({ - type: 'flows', - project: collectProjectMeta(resolvedPath), - machine: collectMachineMeta(), - ci: detectCIMeta(), - result, - }); - if (response) { - console.log(`\n Uploaded to: ${response.url}`); - } - } catch (err) { - console.error(` Upload failed: ${err instanceof Error ? err.message : err}`); - } - } } catch (error) { spinner.stop(); console.error('Flow analysis failed:', error instanceof Error ? error.message : error); diff --git a/src/cli/commands/inventory.ts b/src/cli/commands/inventory.ts index f5bbf15..1def693 100644 --- a/src/cli/commands/inventory.ts +++ b/src/cli/commands/inventory.ts @@ -24,7 +24,6 @@ export const inventoryCommand = new Command('inventory') .option('--diff ', 'Diff against a baseline inventory JSON') .option('-o, --output ', 'Write output to file') .option('--config ', 'Path to config file (default: .g0.yaml)') - .option('--upload', 'Upload results to Guard0 platform') .option('--no-banner', 'Suppress the g0 banner') .action(async (targetPath: string, options: { json?: boolean; @@ -33,7 +32,6 @@ export const inventoryCommand = new Command('inventory') diff?: string; output?: string; config?: string; - upload?: boolean; banner?: boolean; }) => { let resolvedPath: string; @@ -143,29 +141,6 @@ export const inventoryCommand = new Command('inventory') } } - // Upload to platform - const { shouldUpload } = await import('../../platform/upload.js'); - const uploadDecision = await shouldUpload(options.upload); - if (uploadDecision.upload) { - try { - if (uploadDecision.isAuto) { - console.log('\n Auto-uploading (authenticated)...'); - } - const { uploadResults, collectProjectMeta, collectMachineMeta, detectCIMeta } = await import('../../platform/upload.js'); - const response = await uploadResults({ - type: 'inventory', - project: collectProjectMeta(resolvedPath), - machine: collectMachineMeta(), - ci: detectCIMeta(), - result: inventory, - }); - if (response) { - console.log(`\n Uploaded to: ${response.url}`); - } - } catch (err) { - console.error(` Upload failed: ${err instanceof Error ? err.message : err}`); - } - } } catch (error) { spinner.stop(); console.error('Inventory failed:', error instanceof Error ? error.message : error); diff --git a/src/cli/commands/mcp.ts b/src/cli/commands/mcp.ts index 2468901..59be61f 100644 --- a/src/cli/commands/mcp.ts +++ b/src/cli/commands/mcp.ts @@ -28,7 +28,6 @@ export const mcpCommand = new Command('mcp') .option('--pin [file]', 'Generate tool description pins (.g0-pins.json)') .option('--check [file]', 'Verify tools against pinned descriptions') .option('--watch', 'Watch MCP config files for changes and re-scan') - .option('--upload', 'Upload results to Guard0 platform') .option('--no-banner', 'Suppress the g0 banner') .action(async (targetPath: string | undefined, options: { json?: boolean; @@ -36,7 +35,6 @@ export const mcpCommand = new Command('mcp') pin?: string | boolean; check?: string | boolean; watch?: boolean; - upload?: boolean; banner?: boolean; }) => { // Watch mode (local only) @@ -216,29 +214,6 @@ export const mcpCommand = new Command('mcp') } } - // Upload to platform - const { shouldUpload } = await import('../../platform/upload.js'); - const uploadDecision = await shouldUpload(options.upload); - if (uploadDecision.upload) { - try { - if (uploadDecision.isAuto) { - console.log('\n Auto-uploading (authenticated)...'); - } - const { uploadResults, collectProjectMeta, collectMachineMeta, detectCIMeta } = await import('../../platform/upload.js'); - const response = await uploadResults({ - type: 'mcp', - project: resolvedPath ? collectProjectMeta(resolvedPath) : undefined, - machine: collectMachineMeta(), - ci: detectCIMeta(), - result, - }); - if (response) { - console.log(`\n Uploaded to: ${response.url}`); - } - } catch (err) { - console.error(` Upload failed: ${err instanceof Error ? err.message : err}`); - } - } } catch (error) { spinner.stop(); console.error('MCP scan failed:', error instanceof Error ? error.message : error); @@ -254,12 +229,10 @@ const scanSubcommand = new Command('scan') .argument('', 'Path to MCP server source or config file') .option('--json', 'Output as JSON') .option('-o, --output ', 'Write output to file') - .option('--upload', 'Upload results to Guard0 platform') .option('--no-banner', 'Suppress the g0 banner') .action(async (targetPath: string, options: { json?: boolean; output?: string; - upload?: boolean; banner?: boolean; }) => { const resolvedPath = path.resolve(targetPath); @@ -295,29 +268,6 @@ const scanSubcommand = new Command('scan') } } - // Upload to platform - const { shouldUpload } = await import('../../platform/upload.js'); - const uploadDecision = await shouldUpload(options.upload); - if (uploadDecision.upload) { - try { - if (uploadDecision.isAuto) { - console.log('\n Auto-uploading (authenticated)...'); - } - const { uploadResults, collectProjectMeta, collectMachineMeta, detectCIMeta } = await import('../../platform/upload.js'); - const response = await uploadResults({ - type: 'mcp', - project: collectProjectMeta(resolvedPath), - machine: collectMachineMeta(), - ci: detectCIMeta(), - result, - }); - if (response) { - console.log(`\n Uploaded to: ${response.url}`); - } - } catch (err) { - console.error(` Upload failed: ${err instanceof Error ? err.message : err}`); - } - } } catch (error) { spinner.stop(); console.error('MCP scan failed:', error instanceof Error ? error.message : error); diff --git a/src/cli/commands/scan.ts b/src/cli/commands/scan.ts index eb261ce..c5e5e1e 100644 --- a/src/cli/commands/scan.ts +++ b/src/cli/commands/scan.ts @@ -30,7 +30,6 @@ export const scanCommand = new Command('scan') .option('--ai', 'Enable AI-powered analysis (requires ANTHROPIC_API_KEY, OPENAI_API_KEY, or GOOGLE_API_KEY)') .option('--model ', 'AI model to use (e.g., claude-sonnet-4-5-20250929, gpt-5-mini, gemini-2.5-flash)') .option('--report ', `Generate compliance report (${SUPPORTED_STANDARDS.join('|')})`) - .option('--upload', 'Upload results to Guard0 platform') .option('--include-tests', 'Include test files in agent graph (normally excluded)') .option('--show-all', 'Show all findings including suppressed utility-code ones') .option('--ruleset ', 'Rule pack tier: recommended (~200 high-signal), extended (~800), or all (default)') @@ -58,7 +57,6 @@ export const scanCommand = new Command('scan') ai?: boolean; model?: string; report?: string; - upload?: boolean; includeTests?: boolean; showAll?: boolean; ruleset?: string; @@ -235,16 +233,7 @@ export const scanCommand = new Command('scan') console.log(`HTML report written to: ${htmlPath}`); } } else { - // Show upload nudge when not uploading and not already authenticated - const showNudge = options.upload === undefined; - let nudge = false; - if (showNudge) { - try { - const { isAuthenticated } = await import('../../platform/auth.js'); - nudge = !isAuthenticated(); - } catch { nudge = true; } - } - reportTerminal(result, { showBanner: options.banner !== false, showUploadNudge: nudge, hiddenLowConfidence }); + reportTerminal(result, { showBanner: options.banner !== false, hiddenLowConfidence }); } // Also write JSON if --output specified alongside terminal @@ -265,62 +254,6 @@ export const scanCommand = new Command('scan') } } - // Upload to platform - const { shouldUpload } = await import('../../platform/upload.js'); - const uploadDecision = await shouldUpload(options.upload); - if (uploadDecision.upload) { - try { - if (uploadDecision.isAuto && !options.quiet) { - console.log('\n Auto-uploading (authenticated)...'); - } - const { uploadResults, collectProjectMeta, collectMachineMeta, detectCIMeta } = await import('../../platform/upload.js'); - // Cap upload payload to avoid exceeding DB limits - const MAX_UPLOAD_FINDINGS = 5000; - // Build lightweight graph for architecture page (strip large fields like AST, content, parameters) - const lightGraph = result.graph ? { - agents: (result.graph.agents ?? []).map(a => ({ - id: a.id, name: a.name, framework: a.framework, file: a.file, line: a.line, - tools: a.tools, modelId: a.modelId, delegationTargets: a.delegationTargets, - delegationEnabled: a.delegationEnabled, - })), - tools: (result.graph.tools ?? []).map(t => ({ - id: t.id, name: t.name, framework: t.framework, file: t.file, line: t.line, - hasSideEffects: t.hasSideEffects, capabilities: t.capabilities, - })), - models: (result.graph.models ?? []).map(m => ({ - id: m.id, name: m.name, provider: m.provider, framework: m.framework, file: m.file, line: m.line, - })), - vectorDBs: (result.graph.vectorDBs ?? []).map(v => ({ - id: v.id, name: v.name, framework: v.framework, file: v.file, line: v.line, - })), - interAgentLinks: result.graph.interAgentLinks ?? [], - frameworkVersions: result.graph.frameworkVersions ?? [], - edges: (result.graph.edges ?? []).map(e => ({ - id: e.id, source: e.source, target: e.target, type: e.type, - tainted: e.tainted, validated: e.validated, - })), - } : undefined; - const uploadResult = { - ...result, - findings: result.findings.slice(0, MAX_UPLOAD_FINDINGS), - graph: lightGraph as unknown as typeof result.graph, // Lightweight subset for upload - }; - const response = await uploadResults({ - type: 'scan', - project: collectProjectMeta(resolvedPath), - machine: collectMachineMeta(), - ci: detectCIMeta(), - result: uploadResult, - }); - if (response && !options.quiet) { - console.log(`\n Uploaded to: ${response.url}`); - } - } catch (err) { - if (!options.quiet) { - console.error(` Upload failed: ${err instanceof Error ? err.message : err}`); - } - } - } // CI gate evaluation if (options.ci) { try { diff --git a/src/cli/commands/test.ts b/src/cli/commands/test.ts index d185b94..d173546 100644 --- a/src/cli/commands/test.ts +++ b/src/cli/commands/test.ts @@ -48,7 +48,6 @@ export const testCommand = new Command('test') .option('--rate-delay ', 'Delay in ms between payload launches') .option('--verbose', 'Show request/response details during execution') .option('--sarif [file]', 'Output test results as SARIF 2.1.0') - .option('--upload', 'Upload results to Guard0 platform') .option('--no-banner', 'Suppress the g0 banner') .action(async (options: { target?: string; @@ -85,7 +84,6 @@ export const testCommand = new Command('test') provider?: string; verbose?: boolean; sarif?: string | boolean; - upload?: boolean; banner?: boolean; }) => { // --adaptive auto-enables --ai @@ -344,33 +342,6 @@ export const testCommand = new Command('test') } } - // Upload to platform - const { shouldUpload } = await import('../../platform/upload.js'); - const uploadDecision = await shouldUpload(options.upload); - if (uploadDecision.upload) { - try { - if (uploadDecision.isAuto && !options.json) { - console.log('\n Auto-uploading (authenticated)...'); - } - const { uploadResults, collectProjectMeta, collectMachineMeta, detectCIMeta } = await import('../../platform/upload.js'); - const projectPath = typeof options.auto === 'string' ? options.auto : '.'; - const response = await uploadResults({ - type: 'test', - project: collectProjectMeta(path.resolve(projectPath)), - machine: collectMachineMeta(), - ci: detectCIMeta(), - result, - }); - if (response && !options.json) { - console.log(`\n Uploaded to: ${response.url}`); - } - } catch (err) { - if (!options.json) { - console.error(` Upload failed: ${err instanceof Error ? err.message : err}`); - } - } - } - // Exit code: 1 if any critical vulnerability or all errors if (result.summary.overallStatus === 'fail' || result.summary.overallStatus === 'error') { process.exit(1); diff --git a/src/cli/index.ts b/src/cli/index.ts index e96806d..afed06a 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -7,7 +7,6 @@ import { inventoryCommand } from './commands/inventory.js'; import { flowsCommand } from './commands/flows.js'; import { mcpCommand } from './commands/mcp.js'; import { testCommand } from './commands/test.js'; -import { authCommand } from './commands/auth.js'; import { daemonCommand } from './commands/daemon.js'; import { endpointCommand } from './commands/endpoint.js'; import { detectCommand } from './commands/detect.js'; @@ -34,7 +33,6 @@ export function createCli(): Command { program.addCommand(flowsCommand); program.addCommand(mcpCommand); program.addCommand(testCommand); - program.addCommand(authCommand); program.addCommand(daemonCommand); program.addCommand(endpointCommand); program.addCommand(detectCommand); diff --git a/src/daemon/runner.ts b/src/daemon/runner.ts index a60a00f..98242ab 100644 --- a/src/daemon/runner.ts +++ b/src/daemon/runner.ts @@ -1,22 +1,16 @@ import { loadDaemonConfig, type DaemonConfig } from './config.js'; import { DaemonLogger } from './logger.js'; import { removePid } from './process.js'; -import { isAuthenticated } from '../platform/auth.js'; -import { PlatformClient } from '../platform/client.js'; import { getMachineId } from '../platform/machine-id.js'; -import { collectMachineMeta } from '../platform/upload.js'; import { EventReceiver } from './event-receiver.js'; import { BaselineManager } from './behavioral-baseline.js'; import { correlateEvents } from './correlation-engine.js'; import { getCostSnapshot } from './cost-monitor.js'; import { NotificationManager } from './notification-manager.js'; -import * as os from 'node:os'; -import type { HeartbeatPayload } from '../platform/types.js'; let running = true; let config: DaemonConfig; let logger: DaemonLogger; -let endpointId: string | undefined; let eventReceiver: EventReceiver | null = null; let killSwitchMonitor: import('./kill-switch.js').KillSwitchMonitor | null = null; let baselineManager: BaselineManager | null = null; @@ -175,11 +169,6 @@ async function main(): Promise { } } - // Register endpoint if authenticated - if (config.upload && isAuthenticated()) { - await registerEndpoint(); - } - // Run initial tick immediately await tick(); @@ -303,13 +292,7 @@ async function tick(): Promise { } } - // 11. Heartbeat — derive status from actual audit results - if (config.upload && isAuthenticated() && endpointId) { - const heartbeatStatus = deriveHeartbeatStatus(tickIssues); - await sendHeartbeat(heartbeatStatus, tickIssues.length > 0 ? tickIssues : undefined); - } - - // 12. Safety-net flush for notification manager (catches events the interval timer missed) + // 11. Safety-net flush for notification manager (catches events the interval timer missed) if (notificationManager && notificationManager.getPendingCount() > 0) { try { await notificationManager.flush(); @@ -324,36 +307,14 @@ async function tick(): Promise { const msg = err instanceof Error ? err.message : String(err); logger.error(`Tick failed: ${msg}`); - if (config.upload && isAuthenticated() && endpointId) { - await sendHeartbeat('error', [msg]); - } } } -/** Derive heartbeat status from OpenClaw audit + tick issues */ -function deriveHeartbeatStatus(issues: string[]): 'healthy' | 'degraded' | 'error' { - if (issues.length > 0) return 'degraded'; - if (lastOpenClawStatus === 'critical') return 'error'; - if (lastOpenClawStatus === 'warn') return 'degraded'; - return 'healthy'; -} - async function runMCPScan(): Promise { try { const { scanAllMCPConfigs } = await import('../mcp/analyzer.js'); const result = scanAllMCPConfigs(); logger.info(`MCP scan: ${result.summary.totalServers} servers, ${result.summary.totalFindings} findings`); - - if (config.upload && isAuthenticated()) { - const client = new PlatformClient(); - const meta = collectMachineMeta(); - await client.upload({ - type: 'mcp', - machine: meta, - result, - }); - logger.info('MCP scan results uploaded'); - } } catch (err) { logger.error(`MCP scan failed: ${err instanceof Error ? err.message : err}`); } @@ -398,18 +359,6 @@ async function runInventoryDiff(): Promise { const inventory = buildInventory(graph, discovery); logger.info(`Inventory for ${watchPath}: ${inventory.summary.totalModels} models, ${inventory.summary.totalTools} tools`); - - if (config.upload && isAuthenticated()) { - const client = new PlatformClient(); - const meta = collectMachineMeta(); - await client.upload({ - type: 'inventory', - project: { name: watchPath.split('/').pop() || watchPath, path: watchPath }, - machine: meta, - result: inventory, - }); - logger.info(`Inventory for ${watchPath} uploaded`); - } } catch (err) { logger.error(`Inventory diff for ${watchPath} failed: ${err instanceof Error ? err.message : err}`); } @@ -560,21 +509,6 @@ async function runOpenClawAudit(): Promise { } } - // ── Upload (proper typed payload) ───────────────────────────────── - if (config.upload && isAuthenticated()) { - try { - const client = new PlatformClient(); - const meta = collectMachineMeta(); - await client.upload({ - type: 'openclaw-audit', - machine: meta, - result, - }); - logger.info('OpenClaw audit results uploaded'); - } catch (err) { - logger.warn(`OpenClaw audit upload failed: ${err instanceof Error ? err.message : err}`); - } - } } catch (err) { logger.error(`OpenClaw audit failed: ${err instanceof Error ? err.message : err}`); } @@ -644,16 +578,6 @@ async function runHostHardening(): Promise { } } - if (config.upload && isAuthenticated()) { - try { - const client = new PlatformClient(); - const meta = collectMachineMeta(); - await client.upload({ type: 'host-hardening', machine: meta, result }); - logger.info('Host hardening results uploaded'); - } catch (err) { - logger.warn(`Host hardening upload failed: ${err instanceof Error ? err.message : err}`); - } - } } catch (err) { logger.error(`Host hardening failed: ${err instanceof Error ? err.message : err}`); } @@ -689,63 +613,11 @@ async function runEndpointScan(): Promise { // Save for next drift comparison saveLastScan(result); - // Upload full endpoint result - if (config.upload && isAuthenticated()) { - const client = new PlatformClient(); - const meta = collectMachineMeta(); - await client.upload({ - type: 'endpoint', - machine: meta, - result, - }); - logger.info('Endpoint scan results uploaded'); - } } catch (err) { logger.error(`Endpoint scan failed: ${err instanceof Error ? err.message : err}`); } } -async function registerEndpoint(): Promise { - try { - const client = new PlatformClient(); - const response = await client.registerEndpoint({ - machineId: getMachineId(), - hostname: os.hostname(), - platform: os.platform(), - arch: os.arch(), - g0Version: '1.0.0', - watchPaths: config.watchPaths, - }); - endpointId = response.endpointId; - logger.info(`Registered as endpoint ${endpointId}`); - } catch (err) { - logger.warn(`Endpoint registration failed: ${err instanceof Error ? err.message : err}`); - } -} - -async function sendHeartbeat( - status: 'healthy' | 'degraded' | 'error', - issues?: string[], -): Promise { - try { - const client = new PlatformClient(); - const payload: HeartbeatPayload = { - endpointId: endpointId ?? '', - machineId: getMachineId(), - timestamp: new Date().toISOString(), - status, - issues, - // Include OpenClaw audit state in heartbeat - openclawStatus: lastOpenClawStatus, - openclawFailedChecks: lastOpenClawFailedChecks, - openclawDriftEvents: lastOpenClawDriftEvents, - }; - await client.heartbeat(payload); - } catch (err) { - logger.warn(`Heartbeat failed: ${err instanceof Error ? err.message : err}`); - } -} - // ── Fast Egress Loop ────────────────────────────────────────────────────── let egressLoopTimer: ReturnType | undefined; diff --git a/src/platform/auth.ts b/src/platform/auth.ts deleted file mode 100644 index 9ec4e19..0000000 --- a/src/platform/auth.ts +++ /dev/null @@ -1,245 +0,0 @@ -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import * as os from 'node:os'; -import * as crypto from 'node:crypto'; -import * as http from 'node:http'; -import type { AuthTokens } from './types.js'; -import { withLock } from '../utils/file-lock.js'; -import { readEncryptedJson, writeEncryptedJson } from '../utils/encryption.js'; - -const G0_DIR = path.join(os.homedir(), '.g0'); -const AUTH_PATH = path.join(G0_DIR, 'auth.json'); - -// ─── Token Storage ─────────────────────────────────────────────────────────── - -export function loadTokens(): AuthTokens | null { - try { - return readEncryptedJson(AUTH_PATH); - } catch { - return null; - } -} - -export async function saveTokens(tokens: AuthTokens): Promise { - await withLock(AUTH_PATH, () => { - writeEncryptedJson(AUTH_PATH, tokens); - }); -} - -export function clearTokens(): void { - try { - fs.unlinkSync(AUTH_PATH); - } catch { - // Already gone - } -} - -export function getAuthFilePath(): string { - return AUTH_PATH; -} - -// ─── Token Resolution ──────────────────────────────────────────────────────── - -/** - * Resolves an auth token, checking in order: - * 1. G0_API_TOKEN env var (for CI/CD) - * 2. Stored tokens from ~/.g0/auth.json - */ -export function resolveToken(): string | null { - // CI/CD: env var takes priority - const envToken = process.env.G0_API_TOKEN; - if (envToken) return envToken; - - // Interactive: stored tokens - const tokens = loadTokens(); - if (!tokens) return null; - - // API keys (g0_ prefix) don't expire - if (tokens.accessToken.startsWith('g0_')) { - return tokens.accessToken; - } - - // Check expiry (with 60s buffer) for non-API-key tokens - if (tokens.expiresAt && Date.now() > tokens.expiresAt - 60_000) { - return null; // Expired - } - - return tokens.accessToken; -} - -/** - * Check if user is authenticated. - */ -export function isAuthenticated(): boolean { - return resolveToken() !== null; -} - -// ─── Localhost Callback Auth Flow ──────────────────────────────────────────── - -const DEFAULT_AUTH_URL = 'https://cloud.guard0.ai'; - -function getAuthBaseUrl(): string { - return process.env.G0_AUTH_URL ?? DEFAULT_AUTH_URL; -} - -interface CallbackResult { - token: string; - userId?: string; - orgId?: string; - email?: string; -} - -const SUCCESS_HTML = ` -Guard0 CLI - -

Authenticated!

You can close this tab and return to the terminal.

`; - -const ERROR_HTML = ` -Guard0 CLI - -

Authentication Failed

State mismatch. Please try again with g0 auth login.

`; - -/** - * Start a localhost HTTP server and wait for the auth callback from the platform. - * Returns the auth URL to open in the browser and a promise that resolves with the callback result. - */ -export async function startCallbackAuth(): Promise<{ - authUrl: string; - port: number; - result: Promise; - cleanup: () => void; -}> { - const baseUrl = getAuthBaseUrl(); - const state = crypto.randomBytes(16).toString('hex'); // 32 hex chars - - let resolveResult: (value: CallbackResult) => void; - let rejectResult: (reason: Error) => void; - const result = new Promise((resolve, reject) => { - resolveResult = resolve; - rejectResult = reject; - }); - - const server = http.createServer((req, res) => { - const url = new URL(req.url ?? '/', `http://127.0.0.1`); - - if (url.pathname !== '/callback') { - res.writeHead(404, { 'Content-Type': 'text/plain' }); - res.end('Not found'); - return; - } - - const callbackState = url.searchParams.get('state'); - const token = url.searchParams.get('token'); - - if (callbackState !== state) { - res.writeHead(400, { 'Content-Type': 'text/html' }); - res.end(ERROR_HTML); - rejectResult(new Error('State mismatch — possible CSRF attack. Please try again.')); - return; - } - - if (!token) { - res.writeHead(400, { 'Content-Type': 'text/html' }); - res.end(ERROR_HTML); - rejectResult(new Error('No token received from platform.')); - return; - } - - res.writeHead(200, { 'Content-Type': 'text/html' }); - res.end(SUCCESS_HTML); - - resolveResult({ - token, - userId: url.searchParams.get('user_id') ?? undefined, - orgId: url.searchParams.get('org_id') ?? undefined, - email: url.searchParams.get('email') ?? undefined, - }); - }); - - // Bind to localhost only, wait for server to be ready - const port = await new Promise((resolve, reject) => { - server.on('error', reject); - server.listen(0, '127.0.0.1', () => { - const addr = server.address(); - resolve(typeof addr === 'object' && addr ? addr.port : 0); - }); - }); - - // 2-minute timeout - const timeout = setTimeout(() => { - server.close(); - rejectResult(new Error('Authentication timed out (2 minutes). Please try again.')); - }, 120_000); - - const cleanup = () => { - clearTimeout(timeout); - server.close(); - }; - - // Auto-cleanup when result resolves or rejects - result.then(() => cleanup, () => cleanup); - - const authUrl = `${baseUrl}/cli-auth?callback_port=${port}&state=${state}`; - - return { authUrl, port, result, cleanup }; -} - -/** - * Ensure the user is authenticated, triggering inline localhost callback flow if needed. - * Used by --upload to provide frictionless first-time auth. - * Returns true if authenticated, false if user declined or flow failed. - */ -export async function ensureAuthenticated(): Promise { - // Already authenticated - if (resolveToken()) return true; - - // CI environment — don't prompt interactively - if (process.env.CI) return false; - - // Check if stdin is a TTY (interactive terminal) - if (!process.stdin.isTTY) return false; - - console.log('\n Not authenticated. Opening browser to sign in...'); - - try { - const { authUrl, result } = await startCallbackAuth(); - - console.log(`\n If the browser doesn't open, visit:`); - console.log(` ${authUrl}\n`); - - // Try to open browser automatically - try { - const { exec } = await import('node:child_process'); - const cmd = process.platform === 'darwin' ? 'open' - : process.platform === 'win32' ? 'start' - : 'xdg-open'; - exec(`${cmd} "${authUrl}"`); - } catch { - // Non-fatal: user can open manually - } - - console.log(' Waiting for authorization...'); - - const callbackResult = await result; - - const tokens: AuthTokens = { - accessToken: callbackResult.token, - expiresAt: Date.now() + 10 * 365 * 24 * 60 * 60 * 1000, // API keys don't expire; use 10 years - email: callbackResult.email, - userId: callbackResult.userId, - orgId: callbackResult.orgId, - }; - - await saveTokens(tokens); - - console.log(` Authenticated${tokens.email ? ` as ${tokens.email}` : ''}!\n`); - return true; - } catch (err) { - console.error(` Auth failed: ${err instanceof Error ? err.message : err}`); - return false; - } -} diff --git a/src/platform/client.ts b/src/platform/client.ts deleted file mode 100644 index bf1827c..0000000 --- a/src/platform/client.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { resolveToken } from './auth.js'; -import { - DEFAULT_PLATFORM_CONFIG, - type PlatformConfig, - type UploadPayload, - type UploadResponse, - type EndpointRegisterPayload, - type EndpointRegisterResponse, - type HeartbeatPayload, - type HeartbeatResponse, -} from './types.js'; - -export class PlatformClient { - private baseUrl: string; - private apiVersion: string; - - constructor(config?: Partial) { - const resolved = { ...DEFAULT_PLATFORM_CONFIG, ...config }; - this.baseUrl = process.env.G0_PLATFORM_URL ?? resolved.baseUrl; - this.apiVersion = resolved.apiVersion; - } - - private get apiBase(): string { - return `${this.baseUrl}/api/${this.apiVersion}`; - } - - private async request( - method: string, - path: string, - body?: unknown, - ): Promise { - const token = resolveToken(); - if (!token) { - throw new Error( - 'Not authenticated. Run `g0 auth login` or set G0_API_TOKEN.', - ); - } - - const headers: Record = { - 'Content-Type': 'application/json', - 'User-Agent': 'g0-cli/1.0.0', - }; - - // Determine auth header: API keys use X-API-Key, JWTs use Authorization - if (token.startsWith('g0_')) { - headers['X-API-Key'] = token; - } else { - headers['Authorization'] = `Bearer ${token}`; - } - - const url = `${this.apiBase}${path}`; - const response = await fetch(url, { - method, - headers, - body: body ? JSON.stringify(body) : undefined, - signal: AbortSignal.timeout(30_000), - }); - - if (!response.ok) { - const text = await response.text().catch(() => ''); - throw new PlatformError(response.status, text, url); - } - - return response.json() as Promise; - } - - async upload(payload: UploadPayload): Promise { - return this.request('POST', '/upload', payload); - } - - async registerEndpoint( - payload: EndpointRegisterPayload, - ): Promise { - return this.request( - 'POST', - '/endpoints/register', - payload, - ); - } - - async heartbeat(payload: HeartbeatPayload): Promise { - return this.request( - 'POST', - '/endpoints/heartbeat', - payload, - ); - } - - async checkAuth(): Promise<{ email?: string; orgId?: string }> { - return this.request<{ email?: string; orgId?: string }>('GET', '/auth/me'); - } -} - -export class PlatformError extends Error { - constructor( - public readonly status: number, - public readonly body: string, - public readonly url: string, - ) { - super(`Platform API error ${status}: ${body || 'No response body'} (${url})`); - this.name = 'PlatformError'; - } -} diff --git a/src/platform/upload.ts b/src/platform/upload.ts deleted file mode 100644 index 290b16d..0000000 --- a/src/platform/upload.ts +++ /dev/null @@ -1,185 +0,0 @@ -import * as fs from 'node:fs'; -import * as path from 'node:path'; -import * as os from 'node:os'; -import * as childProcess from 'node:child_process'; -import { PlatformClient } from './client.js'; -import { getMachineId } from './machine-id.js'; -import { isAuthenticated, ensureAuthenticated } from './auth.js'; -import type { - UploadPayload, - UploadResponse, - ProjectMeta, - MachineMeta, - GitMeta, - CIMeta, -} from './types.js'; - -/** - * Determine whether to upload based on explicit flag or auth state. - * --upload → trigger inline auth if needed, --no-upload → false, no flag → auto-detect. - * Returns { upload: boolean, isAuto: boolean } so callers can show appropriate messages. - */ -export async function shouldUpload(explicitFlag?: boolean): Promise<{ upload: boolean; isAuto: boolean }> { - // --no-upload - if (explicitFlag === false) return { upload: false, isAuto: false }; - - // --upload: ensure authenticated (trigger inline auth if needed) - if (explicitFlag === true) { - if (isAuthenticated()) return { upload: true, isAuto: false }; - const authed = await ensureAuthenticated(); - return { upload: authed, isAuto: false }; - } - - // No flag: auto-upload if already authenticated (no inline prompt) - if (isAuthenticated()) return { upload: true, isAuto: true }; - return { upload: false, isAuto: false }; -} - -/** - * Upload results to Guard0 platform. - * Non-fatal: returns null on failure instead of throwing. - */ -export async function uploadResults( - payload: UploadPayload, -): Promise { - try { - const client = new PlatformClient(); - return await client.upload(payload); - } catch (err) { - // Non-fatal: log warning but don't fail the scan - const msg = err instanceof Error ? err.message : String(err); - console.error(` Upload failed: ${msg}`); - return null; - } -} - -// ─── Metadata Collection ───────────────────────────────────────────────────── - -export function collectProjectMeta(projectPath: string): ProjectMeta { - const name = detectProjectName(projectPath); - const git = collectGitMeta(projectPath); - - return { - name, - path: projectPath, - git: git ?? undefined, - }; -} - -export function collectMachineMeta(): MachineMeta { - return { - machineId: getMachineId(), - hostname: os.hostname(), - platform: os.platform(), - arch: os.arch(), - nodeVersion: process.version, - g0Version: '1.0.0', - }; -} - -export function detectCIMeta(): CIMeta | undefined { - // GitHub Actions - if (process.env.GITHUB_ACTIONS) { - return { - provider: 'github-actions', - buildId: process.env.GITHUB_RUN_ID, - buildUrl: process.env.GITHUB_SERVER_URL && process.env.GITHUB_REPOSITORY && process.env.GITHUB_RUN_ID - ? `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}` - : undefined, - pipelineId: process.env.GITHUB_WORKFLOW, - }; - } - - // GitLab CI - if (process.env.GITLAB_CI) { - return { - provider: 'gitlab-ci', - buildId: process.env.CI_JOB_ID, - buildUrl: process.env.CI_JOB_URL, - pipelineId: process.env.CI_PIPELINE_ID, - }; - } - - // Jenkins - if (process.env.JENKINS_URL) { - return { - provider: 'jenkins', - buildId: process.env.BUILD_NUMBER, - buildUrl: process.env.BUILD_URL, - pipelineId: process.env.JOB_NAME, - }; - } - - // CircleCI - if (process.env.CIRCLECI) { - return { - provider: 'circleci', - buildId: process.env.CIRCLE_BUILD_NUM, - buildUrl: process.env.CIRCLE_BUILD_URL, - pipelineId: process.env.CIRCLE_PROJECT_REPONAME, - }; - } - - // Generic CI detection - if (process.env.CI) { - return { - provider: 'unknown', - buildId: process.env.BUILD_ID ?? process.env.BUILD_NUMBER, - }; - } - - return undefined; -} - -// ─── Helpers ───────────────────────────────────────────────────────────────── - -function detectProjectName(projectPath: string): string { - // Try package.json - try { - const pkgPath = path.join(projectPath, 'package.json'); - const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')); - if (pkg.name) return pkg.name; - } catch { /* not a node project */ } - - // Try pyproject.toml (look for name = "...") - try { - const pyprojectPath = path.join(projectPath, 'pyproject.toml'); - const content = fs.readFileSync(pyprojectPath, 'utf-8'); - const match = content.match(/^name\s*=\s*"([^"]+)"/m); - if (match) return match[1]; - } catch { /* not a python project */ } - - // Fall back to directory name - return path.basename(path.resolve(projectPath)); -} - -function collectGitMeta(projectPath: string): GitMeta | null { - try { - const opts = { cwd: projectPath, encoding: 'utf-8' as const, timeout: 5000 }; - - const remote = execGit(['config', '--get', 'remote.origin.url'], opts); - const branch = execGit(['rev-parse', '--abbrev-ref', 'HEAD'], opts); - const commit = execGit(['rev-parse', '--short', 'HEAD'], opts); - const dirty = execGit(['status', '--porcelain'], opts) !== ''; - - return { - remote: remote || undefined, - branch: branch || undefined, - commit: commit || undefined, - dirty, - }; - } catch { - return null; - } -} - -function execGit(args: string[], opts: { cwd: string; encoding: 'utf-8'; timeout: number }): string { - try { - return childProcess.execFileSync('git', args, { - ...opts, - stdio: ['pipe', 'pipe', 'pipe'], - }).trim(); - } catch { - return ''; - } -} diff --git a/tests/unit/platform.test.ts b/tests/unit/platform.test.ts deleted file mode 100644 index 49b0aaf..0000000 --- a/tests/unit/platform.test.ts +++ /dev/null @@ -1,216 +0,0 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import * as fs from 'node:fs'; -import * as os from 'node:os'; -import * as path from 'node:path'; -import * as crypto from 'node:crypto'; - -// ─── Machine ID ────────────────────────────────────────────────────────────── - -describe('machine-id', () => { - const testDir = path.join(os.tmpdir(), `g0-test-${Date.now()}`); - const machineIdPath = path.join(testDir, 'machine-id'); - - beforeEach(() => { - fs.mkdirSync(testDir, { recursive: true }); - }); - - afterEach(() => { - fs.rmSync(testDir, { recursive: true, force: true }); - }); - - it('generates a valid UUID', async () => { - // Import fresh to test generation - const { getMachineId } = await import('../../src/platform/machine-id.js'); - const id = getMachineId(); - // UUID v4 format - expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/); - }); - - it('returns same ID on repeated calls', async () => { - const { getMachineId } = await import('../../src/platform/machine-id.js'); - const id1 = getMachineId(); - const id2 = getMachineId(); - expect(id1).toBe(id2); - }); -}); - -// ─── Auth ──────────────────────────────────────────────────────────────────── - -describe('auth', () => { - const testDir = path.join(os.tmpdir(), `g0-auth-test-${Date.now()}`); - const authPath = path.join(testDir, 'auth.json'); - - beforeEach(() => { - fs.mkdirSync(testDir, { recursive: true }); - delete process.env.G0_API_TOKEN; - }); - - afterEach(() => { - fs.rmSync(testDir, { recursive: true, force: true }); - delete process.env.G0_API_TOKEN; - }); - - it('resolveToken returns null when not authenticated', async () => { - const { resolveToken } = await import('../../src/platform/auth.js'); - // When no env var and no file, resolveToken depends on home dir - // but we test the env var path - delete process.env.G0_API_TOKEN; - const token = resolveToken(); - // May or may not be null depending on whether ~/.g0/auth.json exists - expect(token === null || typeof token === 'string').toBe(true); - }); - - it('resolveToken returns G0_API_TOKEN when set', async () => { - process.env.G0_API_TOKEN = 'g0_test_token_12345'; - const { resolveToken } = await import('../../src/platform/auth.js'); - expect(resolveToken()).toBe('g0_test_token_12345'); - }); - - it('isAuthenticated returns true when G0_API_TOKEN is set', async () => { - process.env.G0_API_TOKEN = 'g0_test_token_12345'; - const { isAuthenticated } = await import('../../src/platform/auth.js'); - expect(isAuthenticated()).toBe(true); - }); - - it('saveTokens and loadTokens round-trip', async () => { - const { saveTokens, loadTokens } = await import('../../src/platform/auth.js'); - const tokens = { - accessToken: 'test-access-token', - refreshToken: 'test-refresh-token', - expiresAt: Date.now() + 3600_000, - email: 'test@example.com', - userId: 'user_123', - }; - - await saveTokens(tokens); - const loaded = loadTokens(); - expect(loaded).toBeTruthy(); - expect(loaded!.accessToken).toBe('test-access-token'); - expect(loaded!.email).toBe('test@example.com'); - }); - - it('clearTokens removes auth file', async () => { - const { saveTokens, clearTokens, loadTokens } = await import('../../src/platform/auth.js'); - await saveTokens({ - accessToken: 'test', - expiresAt: Date.now() + 3600_000, - }); - clearTokens(); - // loadTokens may return null or previous depending on path - // We mainly verify clearTokens doesn't throw - expect(true).toBe(true); - }); -}); - -// ─── Platform Client ───────────────────────────────────────────────────────── - -describe('PlatformClient', () => { - it('throws when not authenticated', async () => { - delete process.env.G0_API_TOKEN; - const { PlatformClient } = await import('../../src/platform/client.js'); - const client = new PlatformClient({ baseUrl: 'http://localhost:9999' }); - - // Only fails if no token at all - // This test validates the error path - try { - await client.checkAuth(); - // If it doesn't throw, there must be a token in ~/.g0/auth.json - } catch (err: any) { - expect(err.message).toContain('Not authenticated'); - } - }); - - it('PlatformError has correct properties', async () => { - const { PlatformError } = await import('../../src/platform/client.js'); - const err = new PlatformError(401, 'Unauthorized', 'https://cloud.guard0.ai/api/v1/upload'); - expect(err.status).toBe(401); - expect(err.body).toBe('Unauthorized'); - expect(err.url).toContain('upload'); - expect(err.name).toBe('PlatformError'); - }); - - it('uses G0_PLATFORM_URL env var', async () => { - process.env.G0_PLATFORM_URL = 'http://localhost:3000'; - process.env.G0_API_TOKEN = 'g0_test_key'; - const { PlatformClient } = await import('../../src/platform/client.js'); - const client = new PlatformClient(); - // We can't easily test the internal baseUrl, but we verify construction works - expect(client).toBeDefined(); - delete process.env.G0_PLATFORM_URL; - delete process.env.G0_API_TOKEN; - }); -}); - -// ─── Upload Metadata ───────────────────────────────────────────────────────── - -describe('upload metadata', () => { - it('collectMachineMeta returns valid metadata', async () => { - const { collectMachineMeta } = await import('../../src/platform/upload.js'); - const meta = collectMachineMeta(); - expect(meta.hostname).toBeTruthy(); - expect(meta.platform).toBeTruthy(); - expect(meta.arch).toBeTruthy(); - expect(meta.nodeVersion).toMatch(/^v\d+/); - expect(meta.machineId).toMatch(/^[0-9a-f-]+$/); - }); - - it('collectProjectMeta detects name from package.json', async () => { - const { collectProjectMeta } = await import('../../src/platform/upload.js'); - // Use this project's root directory - const meta = collectProjectMeta(process.cwd()); - expect(meta.name).toBe('@guard0/g0'); - expect(meta.path).toBe(process.cwd()); - }); - - it('collectProjectMeta returns git metadata', async () => { - const { collectProjectMeta } = await import('../../src/platform/upload.js'); - const meta = collectProjectMeta(process.cwd()); - expect(meta.git).toBeTruthy(); - expect(meta.git!.branch).toBeTruthy(); - expect(meta.git!.commit).toBeTruthy(); - }); - - it('detectCIMeta returns undefined in non-CI environment', async () => { - const origCI = process.env.CI; - const origGH = process.env.GITHUB_ACTIONS; - delete process.env.CI; - delete process.env.GITHUB_ACTIONS; - delete process.env.GITLAB_CI; - delete process.env.JENKINS_URL; - delete process.env.CIRCLECI; - - const { detectCIMeta } = await import('../../src/platform/upload.js'); - const ci = detectCIMeta(); - expect(ci).toBeUndefined(); - - // Restore - if (origCI) process.env.CI = origCI; - if (origGH) process.env.GITHUB_ACTIONS = origGH; - }); - - it('detectCIMeta detects GitHub Actions', async () => { - const origGH = process.env.GITHUB_ACTIONS; - process.env.GITHUB_ACTIONS = 'true'; - process.env.GITHUB_RUN_ID = '12345'; - - const { detectCIMeta } = await import('../../src/platform/upload.js'); - const ci = detectCIMeta(); - expect(ci).toBeTruthy(); - expect(ci!.provider).toBe('github-actions'); - expect(ci!.buildId).toBe('12345'); - - delete process.env.GITHUB_ACTIONS; - delete process.env.GITHUB_RUN_ID; - if (origGH) process.env.GITHUB_ACTIONS = origGH; - }); -}); - -// ─── Platform Types ────────────────────────────────────────────────────────── - -describe('platform types', () => { - it('DEFAULT_PLATFORM_CONFIG has correct values', async () => { - const { DEFAULT_PLATFORM_CONFIG } = await import('../../src/platform/types.js'); - expect(DEFAULT_PLATFORM_CONFIG.baseUrl).toBe('https://cloud.guard0.ai'); - expect(DEFAULT_PLATFORM_CONFIG.apiVersion).toBe('v1'); - }); -});