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:
SentryAgent.ai Developer
2026-04-04 02:20:09 +00:00
parent 0fad328329
commit eea885db04
34 changed files with 4262 additions and 25 deletions

70
portal/hooks/useAuth.ts Normal file
View File

@@ -0,0 +1,70 @@
'use client';
/**
* useAuth — Client-side authentication hook for the SentryAgent portal.
*
* Reads the tenant JWT stored in localStorage under the key
* `sentryagent_token`. If no token is present the hook signals that the user
* is unauthenticated so the calling page can redirect to `/login`.
*
* This is intentionally lightweight: the portal calls the AgentIdP API
* directly; the JWT is issued by the AgentIdP `/api/tenants/login` endpoint
* and stored on successful sign-in.
*
* @module hooks/useAuth
*/
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
/** The localStorage key under which the tenant JWT is persisted. */
export const AUTH_TOKEN_KEY = 'sentryagent_token';
/** Shape returned by the useAuth hook. */
export interface AuthState {
/** The stored JWT, or null if unauthenticated. */
token: string | null;
/** True while the hook is reading from localStorage on mount. */
loading: boolean;
/**
* Sign the user out by removing the stored token and redirecting to /login.
*/
signOut: () => void;
}
/**
* Returns the current authentication state and provides a sign-out helper.
* Redirects to `/login` when no token is found (after the initial mount check).
*
* @param redirectOnUnauth - When true (default), redirects to /login if
* no token is present. Pass false on public pages.
* @returns AuthState
*/
export function useAuth(redirectOnUnauth = true): AuthState {
const router = useRouter();
const [token, setToken] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const stored =
typeof window !== 'undefined'
? localStorage.getItem(AUTH_TOKEN_KEY)
: null;
setToken(stored);
setLoading(false);
if (!stored && redirectOnUnauth) {
router.replace('/login');
}
}, [redirectOnUnauth, router]);
const signOut = (): void => {
if (typeof window !== 'undefined') {
localStorage.removeItem(AUTH_TOKEN_KEY);
}
setToken(null);
router.replace('/login');
};
return { token, loading, signOut };
}