feat: Phase 1 MVP — complete AgentIdP implementation

Implements all P0 features per OpenSpec change phase-1-mvp-implementation:
- Agent Registry Service (CRUD) — full lifecycle management
- OAuth 2.0 Token Service (Client Credentials flow)
- Credential Management (generate, rotate, revoke)
- Immutable Audit Log Service

Tech: Node.js 18+, TypeScript 5.3+ strict, Express 4.18+, PostgreSQL 14+, Redis 7+
Standards: OpenAPI 3.0 specs, DRY/SOLID, zero `any` types
Quality: 18 unit test suites, 244 tests passing, 97%+ coverage
OpenAPI: 4 complete specs (14 endpoints total)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
SentryAgent.ai Developer
2026-03-28 09:14:41 +00:00
parent 245f8df427
commit d3530285b9
78 changed files with 20590 additions and 1 deletions

138
src/app.ts Normal file
View File

@@ -0,0 +1,138 @@
/**
* Express application factory for SentryAgent.ai AgentIdP.
* Creates and configures the Express app with all middleware and routes.
* Exported as a factory function — does NOT call listen (testable).
*/
import express, { Application } from 'express';
import helmet from 'helmet';
import cors from 'cors';
import morgan from 'morgan';
import { getPool } from './db/pool.js';
import { getRedisClient } from './cache/redis.js';
import { AgentRepository } from './repositories/AgentRepository.js';
import { CredentialRepository } from './repositories/CredentialRepository.js';
import { TokenRepository } from './repositories/TokenRepository.js';
import { AuditRepository } from './repositories/AuditRepository.js';
import { AuditService } from './services/AuditService.js';
import { AgentService } from './services/AgentService.js';
import { CredentialService } from './services/CredentialService.js';
import { OAuth2Service } from './services/OAuth2Service.js';
import { AgentController } from './controllers/AgentController.js';
import { TokenController } from './controllers/TokenController.js';
import { CredentialController } from './controllers/CredentialController.js';
import { AuditController } from './controllers/AuditController.js';
import { createAgentsRouter } from './routes/agents.js';
import { createTokenRouter } from './routes/token.js';
import { createCredentialsRouter } from './routes/credentials.js';
import { createAuditRouter } from './routes/audit.js';
import { errorHandler } from './middleware/errorHandler.js';
import { RedisClientType } from 'redis';
/**
* Creates and returns a configured Express application.
* All infrastructure dependencies (DB pool, Redis) are initialised here.
*
* @returns Promise resolving to the configured Express Application.
* @throws Error if required environment variables are missing.
*/
export async function createApp(): Promise<Application> {
const app = express();
// ────────────────────────────────────────────────────────────────
// Security headers
// ────────────────────────────────────────────────────────────────
app.use(helmet());
// ────────────────────────────────────────────────────────────────
// CORS
// ────────────────────────────────────────────────────────────────
const corsOrigin = process.env['CORS_ORIGIN'] ?? '*';
app.use(cors({ origin: corsOrigin }));
// ────────────────────────────────────────────────────────────────
// Request logging
// ────────────────────────────────────────────────────────────────
if (process.env['NODE_ENV'] !== 'test') {
app.use(morgan('combined'));
}
// ────────────────────────────────────────────────────────────────
// Body parsers
// JSON body parser for most routes
// urlencoded parser for token endpoint (application/x-www-form-urlencoded)
// ────────────────────────────────────────────────────────────────
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
// ────────────────────────────────────────────────────────────────
// Infrastructure singletons
// ────────────────────────────────────────────────────────────────
const pool = getPool();
const redis = await getRedisClient();
// ────────────────────────────────────────────────────────────────
// Repository layer
// ────────────────────────────────────────────────────────────────
const agentRepo = new AgentRepository(pool);
const credentialRepo = new CredentialRepository(pool);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const tokenRepo = new TokenRepository(pool, redis as RedisClientType);
const auditRepo = new AuditRepository(pool);
// ────────────────────────────────────────────────────────────────
// Service layer
// ────────────────────────────────────────────────────────────────
const auditService = new AuditService(auditRepo);
const agentService = new AgentService(agentRepo, credentialRepo, auditService);
const credentialService = new CredentialService(credentialRepo, agentRepo, auditService);
const privateKey = process.env['JWT_PRIVATE_KEY'];
const publicKey = process.env['JWT_PUBLIC_KEY'];
if (!privateKey || !publicKey) {
throw new Error('JWT_PRIVATE_KEY and JWT_PUBLIC_KEY environment variables are required');
}
const oauth2Service = new OAuth2Service(
tokenRepo,
credentialRepo,
agentRepo,
auditService,
privateKey,
publicKey,
);
// ────────────────────────────────────────────────────────────────
// Controller layer
// ────────────────────────────────────────────────────────────────
const agentController = new AgentController(agentService);
const tokenController = new TokenController(oauth2Service);
const credentialController = new CredentialController(credentialService);
const auditController = new AuditController(auditService);
// ────────────────────────────────────────────────────────────────
// Routes
// ────────────────────────────────────────────────────────────────
const API_BASE = '/api/v1';
app.use(`${API_BASE}/agents`, createAgentsRouter(agentController));
app.use(
`${API_BASE}/agents/:agentId/credentials`,
createCredentialsRouter(credentialController),
);
app.use(`${API_BASE}/token`, createTokenRouter(tokenController));
app.use(`${API_BASE}/audit`, createAuditRouter(auditController));
// ────────────────────────────────────────────────────────────────
// Global error handler (must be last)
// ────────────────────────────────────────────────────────────────
app.use(errorHandler);
return app;
}