WS2: Developer Portal (portal/) - Standalone Next.js 14 + Tailwind CSS app — independent deployment - Home page: hero, feature grid, CTA to /get-started - /pricing: free tier limits table (10 agents, 1k calls/day) + paid tier CTA - /sdks: all 4 SDKs (Node.js, Python, Go, Java) with install + code examples - /api-explorer: Swagger UI from NEXT_PUBLIC_API_URL/openapi.json, persistAuthorization - /get-started: 4-step wizard (setup → register agent → credentials → SDK snippet) - Shared Nav component with active-link highlighting - Build: 8/8 static pages, zero TypeScript errors WS3: CLI Tool (cli/ — npm package: sentryagent) - configure, register-agent, list-agents, issue-token, rotate-credentials, tail-audit-log - Auto OAuth2 token fetch + 30s-buffer cache via client_credentials flow - chalk-formatted table output, confirmation prompts, bounded audit log dedup - bash + zsh shell completion scripts - README with installation, all commands, and completion setup - Build: tsc clean, node dist/index.js --help verified Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
123 lines
3.6 KiB
TypeScript
123 lines
3.6 KiB
TypeScript
import { Command } from 'commander';
|
|
import chalk from 'chalk';
|
|
import { requireConfig } from '../config';
|
|
import { apiRequest } from '../api';
|
|
|
|
interface AuditEvent {
|
|
id: string;
|
|
timestamp: string;
|
|
action: string;
|
|
agentId?: string;
|
|
tenantId?: string;
|
|
outcome: string;
|
|
details?: Record<string, unknown>;
|
|
}
|
|
|
|
interface AuditLogsResponse {
|
|
events: AuditEvent[];
|
|
nextCursor?: string;
|
|
}
|
|
|
|
function formatEvent(event: AuditEvent): string {
|
|
const ts = chalk.dim(new Date(event.timestamp).toLocaleString());
|
|
const outcome =
|
|
event.outcome === 'success'
|
|
? chalk.green(event.outcome)
|
|
: chalk.red(event.outcome);
|
|
const action = chalk.cyan(event.action);
|
|
const agentPart =
|
|
event.agentId !== undefined
|
|
? ' ' + chalk.dim('agent=' + event.agentId)
|
|
: '';
|
|
|
|
return `${ts} ${action} outcome=${outcome}${agentPart} id=${chalk.dim(event.id)}`;
|
|
}
|
|
|
|
export function registerTailAuditLog(program: Command): void {
|
|
program
|
|
.command('tail-audit-log')
|
|
.description(
|
|
'Poll and stream audit log events every 5 seconds (Ctrl+C to stop)',
|
|
)
|
|
.option('--agent-id <id>', 'Filter events for a specific agent ID')
|
|
.action(async (options: { agentId?: string }) => {
|
|
const config = requireConfig();
|
|
|
|
console.log(
|
|
chalk.bold('Tailing audit log') +
|
|
(options.agentId !== undefined
|
|
? chalk.dim(` (agent: ${options.agentId})`)
|
|
: '') +
|
|
chalk.dim(' — press Ctrl+C to stop'),
|
|
);
|
|
console.log(chalk.dim('─'.repeat(60)));
|
|
|
|
const seenIds = new Set<string>();
|
|
let cursor: string | undefined;
|
|
let running = true;
|
|
|
|
process.on('SIGINT', () => {
|
|
running = false;
|
|
console.log();
|
|
console.log(chalk.dim('Stopped.'));
|
|
process.exit(0);
|
|
});
|
|
|
|
while (running) {
|
|
try {
|
|
const params: Record<string, string> = {};
|
|
if (options.agentId !== undefined) {
|
|
params['agentId'] = options.agentId;
|
|
}
|
|
if (cursor !== undefined) {
|
|
params['cursor'] = cursor;
|
|
}
|
|
// Request events from the last poll window
|
|
params['limit'] = '50';
|
|
|
|
const data = await apiRequest<AuditLogsResponse | AuditEvent[]>(
|
|
config,
|
|
'/audit/logs',
|
|
{ params },
|
|
);
|
|
|
|
const events: AuditEvent[] = Array.isArray(data)
|
|
? data
|
|
: (data as AuditLogsResponse).events ?? [];
|
|
|
|
if (!Array.isArray(data) && (data as AuditLogsResponse).nextCursor !== undefined) {
|
|
cursor = (data as AuditLogsResponse).nextCursor;
|
|
}
|
|
|
|
for (const event of events) {
|
|
if (!seenIds.has(event.id)) {
|
|
seenIds.add(event.id);
|
|
console.log(formatEvent(event));
|
|
}
|
|
}
|
|
|
|
// Keep the seenIds set bounded to avoid unbounded memory growth
|
|
if (seenIds.size > 10_000) {
|
|
const arr = Array.from(seenIds);
|
|
const keep = arr.slice(arr.length - 5_000);
|
|
seenIds.clear();
|
|
for (const id of keep) seenIds.add(id);
|
|
}
|
|
} catch (err) {
|
|
console.error(
|
|
chalk.yellow('⚠') +
|
|
' Poll error: ' +
|
|
(err instanceof Error ? err.message : String(err)),
|
|
);
|
|
}
|
|
|
|
// Wait 5 seconds between polls
|
|
await new Promise<void>((resolve) => {
|
|
const timer = setTimeout(resolve, 5000);
|
|
// Allow the timer to be garbage-collected if process exits
|
|
if (typeof timer.unref === 'function') timer.unref();
|
|
});
|
|
}
|
|
});
|
|
}
|