import { writeFileSync } from "node:fs"; import { Command } from "commander"; import chalk from "chalk"; import { ensureAuth } from "../lib/config.js"; import { apiRequest, apiRequestText } from "../lib/api-client.js"; interface OrgAuditEvent { id: string; actorUserId: string | null; actorUsername: string | null; action: string; resourceType: string; resourceId: string | null; metadata: Record | null; ip: string | null; createdAt: string; } export const orgAuditCommand = new Command("audit") .description( "Show the org governance audit trail (admin or audit:read token)", ) .argument("", "organization slug") .option("--action ", 'filter by action prefix (e.g. "org.policy.")') .option("--limit ", "events per page (max 100)", "50") .option("--json", "output raw JSON") .action( async ( org: string, opts: { action?: string; limit: string; json?: boolean }, ) => { ensureAuth(); try { const params = new URLSearchParams(); if (opts.action) params.set("action", opts.action); params.set("limit", opts.limit); const { events, nextCursor } = await apiRequest<{ events: OrgAuditEvent[]; nextCursor: string | null; }>(`/api/v1/orgs/${encodeURIComponent(org)}/audit?${params}`); if (opts.json) { console.log(JSON.stringify({ events, nextCursor }, null, 2)); return; } if (events.length === 0) { console.log(chalk.yellow("No audit events.")); return; } console.log(chalk.bold(`\nAudit trail for ${org}:\n`)); for (const e of events) { const actor = e.actorUsername ?? (e.metadata?.actorTokenName ? `token:${String(e.metadata.actorTokenName)}` : "unknown"); console.log( ` ${chalk.gray(e.createdAt)} ${chalk.bold(e.action)} ${chalk.cyan(actor)}`, ); if (e.resourceId) { console.log( ` ${chalk.gray(`${e.resourceType}: ${e.resourceId}`)}`, ); } } if (nextCursor) { console.log( chalk.gray( `\nMore events — full export: skills-hub org audit-export ${org}`, ), ); } } catch (err) { console.error( chalk.red( err instanceof Error ? err.message : "Failed to fetch audit trail", ), ); process.exit(1); } }, ); export const orgAuditExportCommand = new Command("audit-export") .description( "Export the full org audit trail as CSV (admin or audit:read token)", ) .argument("", "organization slug") .option("--out ", "write to a file instead of stdout") .action(async (org: string, opts: { out?: string }) => { ensureAuth(); try { const csv = await apiRequestText( `/api/v1/orgs/${encodeURIComponent(org)}/audit/export.csv`, ); if (opts.out) { writeFileSync(opts.out, csv); console.log(chalk.green(`Wrote audit CSV to ${opts.out}`)); } else { process.stdout.write(csv); } } catch (err) { console.error( chalk.red( err instanceof Error ? err.message : "Failed to export audit trail", ), ); process.exit(1); } });