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:
138
src/app.ts
Normal file
138
src/app.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user