feat(phase-4): WS6 — Billing & Usage Metering (Stripe, free tier enforcement)

- DB migration 023: tenant_subscriptions and usage_events tables
- UsageMeteringMiddleware: in-memory counters, 60s flush to DB via UPSERT
- FreeTierEnforcementMiddleware: 10 agents / 1,000 calls/day limits, Redis cache
- UsageService: getDailyUsage and getActiveAgentCount
- BillingService: Stripe checkout sessions, webhook verification, subscription status
- POST /billing/checkout, POST /billing/webhook, GET /billing/usage endpoints
- BILLING_ENABLED=false disables enforcement without breaking metering
- Dashboard: Usage tab with Free Tier/Pro badges and metric cards
- 19 unit tests passing across billing services and middleware

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
SentryAgent.ai Developer
2026-04-02 10:51:36 +00:00
parent fefbf1e3ea
commit 26a56f84e1
18 changed files with 1647 additions and 17 deletions

View File

@@ -8,6 +8,7 @@ import express, { Application } from 'express';
import helmet from 'helmet';
import cors from 'cors';
import morgan from 'morgan';
import Stripe from 'stripe';
import { getPool } from './db/pool.js';
import { getRedisClient } from './cache/redis.js';
@@ -21,6 +22,8 @@ import { OrgRepository } from './repositories/OrgRepository.js';
import { AuditService } from './services/AuditService.js';
import { AgentService } from './services/AgentService.js';
import { MarketplaceService } from './services/MarketplaceService.js';
import { BillingService } from './services/BillingService.js';
import { UsageService } from './services/UsageService.js';
import { CredentialService } from './services/CredentialService.js';
import { OAuth2Service } from './services/OAuth2Service.js';
import { OrgService } from './services/OrgService.js';
@@ -35,6 +38,7 @@ import { createKafkaProducer } from './adapters/KafkaAdapter.js';
import { AgentController } from './controllers/AgentController.js';
import { MarketplaceController } from './controllers/MarketplaceController.js';
import { BillingController } from './controllers/BillingController.js';
import { OIDCTrustPolicyController } from './controllers/OIDCTrustPolicyController.js';
import { OIDCTokenExchangeController } from './controllers/OIDCTokenExchangeController.js';
import { TokenController } from './controllers/TokenController.js';
@@ -49,6 +53,7 @@ import { ComplianceController } from './controllers/ComplianceController.js';
import { createAgentsRouter } from './routes/agents.js';
import { createMarketplaceRouter } from './routes/marketplace.js';
import { createBillingRouter } from './routes/billing.js';
import { createOIDCTrustPoliciesRouter } from './routes/oidcTrustPolicies.js';
import { createOIDCTokenExchangeRouter } from './routes/oidcTokenExchange.js';
import { OIDCTrustPolicyService } from './services/OIDCTrustPolicyService.js';
@@ -69,6 +74,8 @@ import { createOpaMiddleware } from './middleware/opa.js';
import { metricsMiddleware } from './middleware/metrics.js';
import { createOrgContextMiddleware } from './middleware/orgContext.js';
import { authMiddleware } from './middleware/auth.js';
import { createUsageMeteringMiddleware, startUsageMeteringFlush } from './middleware/usageMeteringMiddleware.js';
import { createFreeTierEnforcementMiddleware } from './middleware/freeTierEnforcementMiddleware.js';
import { tlsEnforcementMiddleware } from './middleware/TLSEnforcementMiddleware.js';
import { createVaultClientFromEnv } from './vault/VaultClient.js';
import { getEncryptionService } from './services/EncryptionService.js';
@@ -232,6 +239,17 @@ export async function createApp(): Promise<Application> {
const webhookController = new WebhookController(webhookService);
const marketplaceController = new MarketplaceController(marketplaceService);
// ────────────────────────────────────────────────────────────────
// Billing & Usage Metering (WS6)
// ────────────────────────────────────────────────────────────────
const stripe = new Stripe(process.env['STRIPE_SECRET_KEY'] ?? '', { apiVersion: '2026-03-25.dahlia' });
const billingService = new BillingService(pool, stripe);
const usageService = new UsageService(pool);
const billingController = new BillingController(billingService, usageService);
// Start periodic flush of in-memory usage counters to DB (every 60s)
startUsageMeteringFlush(pool);
// OIDC trust policy management + GitHub Actions token exchange
const oidcTrustPolicyService = new OIDCTrustPolicyService(pool);
const oidcTrustPolicyController = new OIDCTrustPolicyController(oidcTrustPolicyService);
@@ -254,6 +272,18 @@ export async function createApp(): Promise<Application> {
// ────────────────────────────────────────────────────────────────
app.use(createOrgContextMiddleware(pool));
// ────────────────────────────────────────────────────────────────
// Usage metering — records per-tenant API call counts in-memory
// Applied after auth middleware so req.user is populated.
// ────────────────────────────────────────────────────────────────
app.use(createUsageMeteringMiddleware(pool));
// ────────────────────────────────────────────────────────────────
// Free tier enforcement — rejects requests exceeding free plan limits
// Applied after usage metering and before routes.
// ────────────────────────────────────────────────────────────────
app.use(createFreeTierEnforcementMiddleware(pool, redis as RedisClientType));
// ────────────────────────────────────────────────────────────────
// Routes
// ────────────────────────────────────────────────────────────────
@@ -287,6 +317,9 @@ export async function createApp(): Promise<Application> {
app.use(`${API_BASE}`, createComplianceRouter(complianceController));
app.use(`${API_BASE}/marketplace`, createMarketplaceRouter(marketplaceController));
// Billing & Usage Metering — checkout, webhook, usage summary
app.use(`${API_BASE}/billing`, createBillingRouter(billingController, authMiddleware));
// OIDC trust-policy management (authenticated) and token exchange (unauthenticated)
// Both routers mount under ${API_BASE}/oidc — trust-policy routes use /trust-policies prefix,
// token exchange uses /token, so there are no path conflicts.