feat(phase-6): WS3+WS4+WS6 — Analytics, API Tiers, AGNTCY Compliance
WS3 — Advanced Analytics Dashboard: - DB migration: analytics_events table (tenant_id, date, metric_type, count) - AnalyticsService: recordEvent (fire-and-forget), getTokenTrend, getAgentActivity, getAgentUsageSummary - Analytics hooks in OAuth2Service (token_issued) and AgentService (agent_registered/deactivated) - AnalyticsController + routes/analytics.ts (gated by ANALYTICS_ENABLED flag) - Portal: TokenTrendChart (recharts LineChart), AgentHeatmap (recharts heatmap), /analytics page WS4 — API Gateway Tiers: - DB migration: tenant_tiers table; src/config/tiers.ts (free/pro/enterprise limits) - TierService: getStatus, initiateUpgrade (Stripe), applyUpgrade; TierLimitError in errors.ts - tierEnforcement middleware (Redis-backed daily call/token counters; TIER_ENFORCEMENT flag) - Agent count enforcement in AgentService.create() - Stripe webhook updated to call TierService.applyUpgrade() on checkout.session.completed - TierController + routes/tiers.ts; Portal: /settings/tier page with upgrade flow WS6 — AGNTCY Compliance Certification: - ComplianceService: generateReport() (Redis-cached 5 min), exportAgentCards() - Compliance sections: agent-identity (DID + credential expiry checks), audit-trail (Merkle chain) - ComplianceController updated with getComplianceReport, exportAgentCards handlers - routes/compliance.ts: new AGNTCY routes (gated by COMPLIANCE_ENABLED flag); SOC2 routes unaffected QA: - 28 new unit tests: AnalyticsService (8), TierService (9), ComplianceService (11) — all pass - 673 total unit tests passing; 0 TypeScript errors across API and portal - AGNTCY conformance test suite at tests/agntcy-conformance/ (4 protocol tests) - Portal builds cleanly: 9 routes including /analytics and /settings/tier - Feature flags verified: ANALYTICS_ENABLED, TIER_ENFORCEMENT, COMPLIANCE_ENABLED Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
93
src/controllers/TierController.ts
Normal file
93
src/controllers/TierController.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Tier Controller for SentryAgent.ai AgentIdP.
|
||||
* HTTP handlers for tier status and upgrade endpoints.
|
||||
* No business logic — delegates entirely to TierService.
|
||||
*/
|
||||
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { TierService } from '../services/TierService.js';
|
||||
import { AuthenticationError, ValidationError } from '../utils/errors.js';
|
||||
import { isTierName } from '../config/tiers.js';
|
||||
|
||||
/**
|
||||
* Controller for tenant tier management endpoints.
|
||||
* Receives TierService via constructor injection.
|
||||
*/
|
||||
export class TierController {
|
||||
/**
|
||||
* @param tierService - The tier management service.
|
||||
*/
|
||||
constructor(private readonly tierService: TierService) {}
|
||||
|
||||
/**
|
||||
* Handles GET /api/tiers/status — returns the current tier, limits, and usage.
|
||||
*
|
||||
* Response: 200 ITierStatus
|
||||
* Errors: 401 when unauthenticated.
|
||||
*
|
||||
* @param req - Express request. Must have req.user populated.
|
||||
* @param res - Express response.
|
||||
* @param next - Express next function.
|
||||
*/
|
||||
getStatus = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
throw new AuthenticationError();
|
||||
}
|
||||
|
||||
const orgId = req.user.organization_id;
|
||||
if (!orgId) {
|
||||
throw new AuthenticationError('organization_id is required in token.');
|
||||
}
|
||||
|
||||
const status = await this.tierService.getStatus(orgId);
|
||||
res.status(200).json(status);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles POST /api/tiers/upgrade — initiates a Stripe checkout session for a tier upgrade.
|
||||
*
|
||||
* Request body: { target_tier: 'pro' | 'enterprise' }
|
||||
* Response: 200 { checkoutUrl: string }
|
||||
* Errors: 400 when target_tier is missing/invalid or is not an upgrade.
|
||||
* 401 when unauthenticated.
|
||||
*
|
||||
* @param req - Express request. Must have req.user populated.
|
||||
* @param res - Express response.
|
||||
* @param next - Express next function.
|
||||
*/
|
||||
initiateUpgrade = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
||||
try {
|
||||
if (!req.user) {
|
||||
throw new AuthenticationError();
|
||||
}
|
||||
|
||||
const orgId = req.user.organization_id;
|
||||
if (!orgId) {
|
||||
throw new AuthenticationError('organization_id is required in token.');
|
||||
}
|
||||
|
||||
const body = req.body as { target_tier?: unknown };
|
||||
const rawTargetTier = body.target_tier;
|
||||
|
||||
if (!rawTargetTier || typeof rawTargetTier !== 'string') {
|
||||
throw new ValidationError('target_tier is required.', { received: rawTargetTier });
|
||||
}
|
||||
|
||||
if (!isTierName(rawTargetTier)) {
|
||||
throw new ValidationError(
|
||||
`target_tier must be one of: free, pro, enterprise.`,
|
||||
{ received: rawTargetTier },
|
||||
);
|
||||
}
|
||||
|
||||
const result = await this.tierService.initiateUpgrade(orgId, rawTargetTier);
|
||||
res.status(200).json(result);
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user