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>
71 lines
2.1 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
}
|