Files
sentryagent-idp/cli/src/commands/issue-token.ts
SentryAgent.ai Developer d1e6af25aa feat(phase-4): WS2 + WS3 — Developer Portal (Next.js 14) and CLI tool (sentryagent)
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>
2026-04-02 04:29:50 +00:00

71 lines
2.1 KiB
TypeScript

import { Command } from 'commander';
import chalk from 'chalk';
import { requireConfig } from '../config';
interface TokenResponse {
access_token: string;
expires_in: number;
token_type: string;
scope?: string;
}
export function registerIssueToken(program: Command): void {
program
.command('issue-token')
.description('Issue an OAuth2 access token for an agent')
.requiredOption('--agent-id <id>', 'Agent ID to issue a token for')
.action(async (options: { agentId: string }) => {
const config = requireConfig();
try {
const body = new URLSearchParams({
grant_type: 'client_credentials',
client_id: config.clientId,
client_secret: config.clientSecret,
agent_id: options.agentId,
});
const res = await fetch(`${config.apiUrl}/oauth2/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Token issuance failed (${res.status}): ${text}`);
}
const data = (await res.json()) as TokenResponse;
const expiresAt = new Date(
Date.now() + data.expires_in * 1000,
).toISOString();
console.log(chalk.green('✓') + ' Token issued successfully');
console.log();
console.log(chalk.bold('Access Token:'));
console.log(chalk.cyan(data.access_token));
console.log();
console.log(
chalk.bold('Token Type: ') + data.token_type,
);
console.log(
chalk.bold('Expires In: ') + `${data.expires_in}s`,
);
console.log(
chalk.bold('Expires At: ') + chalk.dim(expiresAt),
);
if (data.scope !== undefined) {
console.log(chalk.bold('Scope: ') + data.scope);
}
} catch (err) {
console.error(
chalk.red('Error:'),
err instanceof Error ? err.message : String(err),
);
process.exit(1);
}
});
}