diff --git a/CLAUDE.md b/CLAUDE.md index 0fcb4b8113..833b20377a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -175,6 +175,7 @@ Read the relevant file BEFORE implementing changes in that area: | Agent Control (AI Gateway native tool-call hook, file-write gating, approval, result capture, run correlation, multi-agent wiring) | `docs/technical/domains/agent-control.md` | | Agent Control integrator/developer docs (connect an agent, Claude Code + Cursor, generic contract, API ref) | `shared/user-guide-content/content/developers/` | | AI Trust Index | `docs/technical/domains/ai-trust-index.md` | +| Regulations Tracker (global AI-regulations feed: daily sync, per-country tracking, Horizon/Deadlines/Frameworks, notifications) | `docs/technical/domains/regulations-tracker.md` | | AI Detection | `docs/technical/domains/ai-detection.md` | | Risk management | `docs/technical/domains/risk-management.md` | | Vendors | `docs/technical/domains/vendors.md` | diff --git a/Clients/src/application/config/routes.tsx b/Clients/src/application/config/routes.tsx index b3b79a4fe4..0799777c73 100644 --- a/Clients/src/application/config/routes.tsx +++ b/Clients/src/application/config/routes.tsx @@ -101,6 +101,29 @@ const AITrustIndexSettings = lazyRoute( const AITrustIndexDetail = lazyRoute( () => import("../../presentation/pages/AITrustIndex/AppDetail"), ); +// ── Regulations Tracker routes ──────────────────────────────────────── +const RegulationsTracker = lazyRoute(() => import("../../presentation/pages/RegulationsTracker")); +const RegulationsTrackerBrowse = lazyRoute( + () => import("../../presentation/pages/RegulationsTracker/Browse"), +); +const RegulationsTrackerTracked = lazyRoute( + () => import("../../presentation/pages/RegulationsTracker/Tracked"), +); +const RegulationsTrackerSettings = lazyRoute( + () => import("../../presentation/pages/RegulationsTracker/Settings"), +); +const RegulationsTrackerCountryDetail = lazyRoute( + () => import("../../presentation/pages/RegulationsTracker/CountryDetail"), +); +const RegulationsTrackerHorizon = lazyRoute( + () => import("../../presentation/pages/RegulationsTracker/Horizon"), +); +const RegulationsTrackerDeadlines = lazyRoute( + () => import("../../presentation/pages/RegulationsTracker/Deadlines"), +); +const RegulationsTrackerFrameworks = lazyRoute( + () => import("../../presentation/pages/RegulationsTracker/Frameworks"), +); const InsightsPage = lazyRoute(() => import("../../presentation/pages/ShadowAI/InsightsPage")); const UserActivityPage = lazyRoute( @@ -788,6 +811,70 @@ export const createRoutes = ( } /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> } /> void; + refreshTrackedCount: () => void; +} + +const RegulationsTrackerSidebarContext = createContext( + null, +); + +export const RegulationsTrackerSidebarProvider: FC<{ children: ReactNode }> = ({ children }) => { + const [trackedCount, setTrackedCount] = useState(0); + + const refreshTrackedCount = useCallback(async () => { + try { + const response = await getTracked(); + const rows = Array.isArray(response?.data) ? response.data : []; + setTrackedCount(rows.length); + } catch { + // Sidebar badge is non-critical; ignore failures. + } + }, []); + + useEffect(() => { + refreshTrackedCount(); + }, [refreshTrackedCount]); + + return ( + + {children} + + ); +}; + +export const useRegulationsTrackerSidebarContext = () => { + const context = useContext(RegulationsTrackerSidebarContext); + if (!context) { + throw new Error( + "useRegulationsTrackerSidebarContext must be used within RegulationsTrackerSidebarProvider", + ); + } + return context; +}; + +// Safe version that returns null if not in provider (used by ContextSidebar). +export const useRegulationsTrackerSidebarContextSafe = () => { + return useContext(RegulationsTrackerSidebarContext); +}; diff --git a/Clients/src/application/hooks/useActiveModule.ts b/Clients/src/application/hooks/useActiveModule.ts index d46ce5f319..388a049ca5 100644 --- a/Clients/src/application/hooks/useActiveModule.ts +++ b/Clients/src/application/hooks/useActiveModule.ts @@ -36,6 +36,9 @@ export function useActiveModule() { if (pathname.startsWith("/ai-trust-index")) { return "ai-trust-index"; } + if (pathname.startsWith("/regulations-tracker")) { + return "regulations-tracker"; + } if (pathname.startsWith("/super-admin")) { return "super-admin"; } @@ -76,6 +79,9 @@ export function useActiveModule() { case "ai-trust-index": navigate("/ai-trust-index/browse"); break; + case "regulations-tracker": + navigate("/regulations-tracker/browse"); + break; case "super-admin": navigate("/super-admin"); break; @@ -100,6 +106,7 @@ export function useActiveModule() { "shadow-ai", "ai-gateway", "ai-trust-index", + "regulations-tracker", "super-admin", ].includes(stored) ) { diff --git a/Clients/src/application/hooks/useRegulationsTracker.ts b/Clients/src/application/hooks/useRegulationsTracker.ts new file mode 100644 index 0000000000..f78fcd4d8a --- /dev/null +++ b/Clients/src/application/hooks/useRegulationsTracker.ts @@ -0,0 +1,164 @@ +// Clients/src/application/hooks/useRegulationsTracker.ts +import { useQuery, useMutation, useQueryClient, keepPreviousData } from "@tanstack/react-query"; +import { + getCountries, + getCountryDetail, + getTracked, + trackCountry, + trackBulk, + untrackCountry, + getSettings, + updateSettings, + getHorizon, + getDeadlines, + getFrameworks, + triggerSync, + getImpactAnalysis, + refreshImpactAnalysis, +} from "../repository/regulationsTracker.repository"; + +const KEY = "regulations-tracker"; + +// keepPreviousData on the read queries: track/untrack invalidates these keys, +// which triggers a background refetch. Without it, `data` briefly becomes +// undefined and the page's content unmounts and re-mounts — a visible flicker. +// Keeping the previous data holds the UI steady while the fresh data loads. +export function useCountries(filters: { region?: string; q?: string } = {}) { + return useQuery({ + queryKey: [KEY, "countries", filters], + queryFn: () => getCountries(filters), + placeholderData: keepPreviousData, + }); +} + +export function useCountryDetail(slug: string) { + return useQuery({ + queryKey: [KEY, "country", slug], + queryFn: () => getCountryDetail(slug), + enabled: !!slug, + placeholderData: keepPreviousData, + }); +} + +export function useTracked() { + return useQuery({ + queryKey: [KEY, "tracked"], + queryFn: () => getTracked(), + placeholderData: keepPreviousData, + }); +} + +export function useTrackCountry() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (slug: string) => trackCountry(slug), + onSuccess: () => { + qc.invalidateQueries({ queryKey: [KEY, "countries"] }); + qc.invalidateQueries({ queryKey: [KEY, "tracked"] }); + // The detail page reads is_tracked from [KEY, "country", slug]; invalidate the + // "country" prefix so its Track/Untrack button reflects the new state. + qc.invalidateQueries({ queryKey: [KEY, "country"] }); + }, + }); +} + +export function useUntrackCountry() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (slug: string) => untrackCountry(slug), + onSuccess: () => { + qc.invalidateQueries({ queryKey: [KEY, "countries"] }); + qc.invalidateQueries({ queryKey: [KEY, "tracked"] }); + qc.invalidateQueries({ queryKey: [KEY, "country"] }); + }, + }); +} + +export function useTrackBulk() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (slugs: string[]) => trackBulk(slugs), + onSuccess: () => { + qc.invalidateQueries({ queryKey: [KEY, "countries"] }); + qc.invalidateQueries({ queryKey: [KEY, "tracked"] }); + qc.invalidateQueries({ queryKey: [KEY, "country"] }); + }, + }); +} + +export function useSettings() { + // Settings change rarely; a 60s staleTime avoids the global 2s default + // refetching on every interaction. + return useQuery({ + queryKey: [KEY, "settings"], + queryFn: () => getSettings(), + staleTime: 60 * 1000, + }); +} + +export function useUpdateSettings() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (body: { + recipient_user_ids: number[]; + recipient_emails: string[]; + impact_enabled?: boolean; + }) => updateSettings(body), + onSuccess: () => qc.invalidateQueries({ queryKey: [KEY, "settings"] }), + }); +} + +export function useHorizon() { + return useQuery({ + queryKey: [KEY, "horizon"], + queryFn: getHorizon, + placeholderData: keepPreviousData, + }); +} + +export function useDeadlines() { + return useQuery({ + queryKey: [KEY, "deadlines"], + queryFn: getDeadlines, + placeholderData: keepPreviousData, + }); +} + +export function useFrameworks() { + return useQuery({ + queryKey: [KEY, "frameworks"], + queryFn: getFrameworks, + placeholderData: keepPreviousData, + }); +} + +export function useTriggerSync() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: triggerSync, + onSuccess: () => { + // Refresh everything the sync may have changed: catalog, tracked list, + // the global feeds, and settings (which carries last-run status). + qc.invalidateQueries({ queryKey: [KEY] }); + }, + }); +} + +export function useImpactAnalysis(slug: string) { + return useQuery({ + queryKey: [KEY, "impact", slug], + queryFn: () => getImpactAnalysis(slug), + enabled: !!slug, + placeholderData: keepPreviousData, + }); +} + +export function useRefreshImpactAnalysis() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (slug: string) => refreshImpactAnalysis(slug), + onSuccess: (_data, slug) => { + qc.invalidateQueries({ queryKey: [KEY, "impact", slug] }); + }, + }); +} diff --git a/Clients/src/application/redux/ui/uiSlice.ts b/Clients/src/application/redux/ui/uiSlice.ts index f2ca3b13a2..3b5f6bb533 100644 --- a/Clients/src/application/redux/ui/uiSlice.ts +++ b/Clients/src/application/redux/ui/uiSlice.ts @@ -7,6 +7,7 @@ export type AppModule = | "shadow-ai" | "ai-gateway" | "ai-trust-index" + | "regulations-tracker" | "super-admin"; const initialState = { diff --git a/Clients/src/application/repository/regulationsTracker.repository.ts b/Clients/src/application/repository/regulationsTracker.repository.ts new file mode 100644 index 0000000000..9454a7161a --- /dev/null +++ b/Clients/src/application/repository/regulationsTracker.repository.ts @@ -0,0 +1,74 @@ +// Clients/src/application/repository/regulationsTracker.repository.ts +import { apiServices } from "../../infrastructure/api/networkServices"; + +const BASE = "/regulations-tracker"; + +export async function getCountries(params: { region?: string; q?: string } = {}): Promise { + const qs = new URLSearchParams(); + Object.entries(params).forEach(([k, v]) => { + if (v !== undefined && v !== "") qs.set(k, String(v)); + }); + const query = qs.toString(); + const response = await apiServices.get(`${BASE}/countries${query ? `?${query}` : ""}`); + return response.data; +} + +export async function getCountryDetail(slug: string): Promise { + const response = await apiServices.get(`${BASE}/countries/${encodeURIComponent(slug)}`); + return response.data; +} + +export async function getTracked(): Promise { + const response = await apiServices.get(`${BASE}/tracked`); + return response.data; +} + +export async function trackCountry(slug: string): Promise { + return (await apiServices.post(`${BASE}/tracked`, { slug })).data; +} + +export async function trackBulk(slugs: string[]): Promise { + return (await apiServices.post(`${BASE}/tracked/bulk`, { slugs })).data; +} + +export async function untrackCountry(slug: string): Promise { + return (await apiServices.delete(`${BASE}/tracked/${encodeURIComponent(slug)}`)).data; +} + +export async function getSettings(): Promise { + return (await apiServices.get(`${BASE}/settings`)).data; +} + +export async function updateSettings(body: { + recipient_user_ids: number[]; + recipient_emails: string[]; +}): Promise { + return (await apiServices.put(`${BASE}/settings`, body)).data; +} + +export async function getHorizon(): Promise { + return (await apiServices.get(`${BASE}/horizon`)).data; +} + +export async function getDeadlines(): Promise { + return (await apiServices.get(`${BASE}/deadlines`)).data; +} + +export async function getFrameworks(): Promise { + return (await apiServices.get(`${BASE}/frameworks`)).data; +} + +export async function getImpactAnalysis(slug: string): Promise { + const response = await apiServices.get(`${BASE}/countries/${encodeURIComponent(slug)}/impact`); + return response.data; +} + +export async function refreshImpactAnalysis(slug: string): Promise { + return ( + await apiServices.post(`${BASE}/countries/${encodeURIComponent(slug)}/impact/refresh`, {}) + ).data; +} + +export async function triggerSync(): Promise { + return (await apiServices.post(`${BASE}/sync`, {})).data; +} diff --git a/Clients/src/i18n/translations.ts b/Clients/src/i18n/translations.ts index d6aa7bbb8f..6a8617d9db 100644 --- a/Clients/src/i18n/translations.ts +++ b/Clients/src/i18n/translations.ts @@ -8703,6 +8703,102 @@ export const translations: Record> = { "Get notified when assessments change": "Benachrichtigung bei Änderungen der Bewertungen", "Tracked apps are included in the weekly change digest, so configured recipients hear about score, grade, or policy changes.": "Verfolgte Apps sind in der wöchentlichen Änderungsübersicht enthalten, sodass konfigurierte Empfänger über Änderungen an Punktzahl, Note oder Richtlinie informiert werden.", + "Track AI regulations and compliance requirements across jurisdictions": + "Verfolgen Sie KI-Vorschriften und Compliance-Anforderungen über Rechtsräume hinweg", + // Regulations Tracker + "Horizon": "Horizont", + "A dated changelog of AI-regulation changes across all tracked jurisdictions, newest first.": + "Ein datiertes Änderungsprotokoll der KI-Regulierungen für alle verfolgten Jurisdiktionen, neueste zuerst.", + "Deadlines": "Fristen", + "Upcoming effective-date milestones for AI regulations, soonest first, plus regulations with no scheduled date yet.": + "Bevorstehende Inkrafttreten-Meilensteine für KI-Regulierungen, früheste zuerst, plus Regulierungen ohne geplantes Datum.", + "International frameworks": "Internationale Rahmenwerke", + "Country details": "Länderdetails", + "AI regulations for a specific country": "KI-Vorschriften für ein bestimmtes Land", + "No international frameworks are available yet. Cross-border frameworks like the OECD AI Principles will appear here once the feed publishes them.": + "Es sind noch keine internationalen Rahmenwerke verfügbar. Grenzüberschreitende Rahmenwerke wie die OECD-KI-Grundsätze werden hier angezeigt, sobald sie im Feed veröffentlicht werden.", + "No upcoming regulation deadlines are recorded yet. Effective-date milestones for AI regulations will appear here as they are published.": + "Es sind noch keine bevorstehenden Regulierungsfristen erfasst. Stichtags-Meilensteine für KI-Vorschriften werden hier angezeigt, sobald sie veröffentlicht werden.", + "Cross-border AI governance frameworks and principles that complement national regulations.": + "Grenzüberschreitende KI-Governance-Rahmenwerke und -Grundsätze, die nationale Regulierungen ergänzen.", + "Key obligations": "Wesentliche Pflichten", + "Key principles": "Wesentliche Grundsätze", + "Max penalty:": "Höchststrafe:", + "Practical takeaway": "Praktisches Fazit", + "Next 12 months": "Nächste 12 Monate", + "Effective dates for the coming year. Months closer to today are highlighted.": + "Gültigkeitsdaten für das kommende Jahr. Monate näher an heute sind hervorgehoben.", + "No effective dates in the next 12 months.": + "Keine Gültigkeitsdaten in den nächsten 12 Monaten.", + "Scheduled": "Geplant", + "Not yet scheduled": "Noch nicht geplant", + "No regulation changes have been recorded yet.": + "Es wurden noch keine Regulierungsänderungen erfasst.", + "No upcoming regulation deadlines are recorded yet.": + "Es sind noch keine bevorstehenden Regulierungsfristen erfasst.", + "No international frameworks are recorded yet.": + "Es sind noch keine internationalen Rahmenwerke erfasst.", + "Find them in Browse.": "Diese finden Sie unter Durchsuchen.", + "Showing the last known changelog; live data is temporarily unavailable.": + "Das letzte bekannte Änderungsprotokoll wird angezeigt; Live-Daten sind vorübergehend nicht verfügbar.", + "Showing the last known deadlines; live data is temporarily unavailable.": + "Die letzten bekannten Fristen werden angezeigt; Live-Daten sind vorübergehend nicht verfügbar.", + "Showing the last known frameworks; live data is temporarily unavailable.": + "Die letzten bekannten Rahmenwerke werden angezeigt; Live-Daten sind vorübergehend nicht verfügbar.", + "Regulations tracker": "Regulierungsverfolger", + "Browse the catalogue of countries and jurisdictions. Track the ones relevant to your organization to receive updates when their AI regulations change.": + "Durchsuchen Sie den Katalog der Länder und Jurisdiktionen. Verfolgen Sie die für Ihre Organisation relevanten, um Updates zu erhalten, wenn sich deren KI-Regulierungen ändern.", + "Search countries or jurisdictions": "Länder oder Jurisdiktionen suchen", + "No countries match your filters.": "Keine Länder entsprechen Ihren Filtern.", + "Select all on page": "Alle auf der Seite auswählen", + "View source": "Quelle anzeigen", + "Tracked countries": "Verfolgte Länder", + "Countries and jurisdictions your organization is tracking. You": + "Länder und Jurisdiktionen, die Ihre Organisation verfolgt. Sie", + "Countries and jurisdictions your organization is tracking. You'll be notified when their AI regulations change.": + "Länder und Jurisdiktionen, die Ihre Organisation verfolgt. Sie werden benachrichtigt, wenn sich deren KI-Regulierungen ändern.", + "Find countries in Browse": "Länder in der Übersicht finden", + "Open the Browse tab to explore the full catalogue, then track the countries relevant to your organization.": + "Öffnen Sie die Registerkarte Durchsuchen, um den vollständigen Katalog zu erkunden, und verfolgen Sie die für Ihre Organisation relevanten Länder.", + "Get notified when regulations change": "Benachrichtigung bei Regulierungsänderungen", + "Tracked countries are monitored for regulatory updates. Configured recipients are notified when changes are detected.": + "Verfolgte Länder werden auf regulatorische Aktualisierungen überwacht. Konfigurierte Empfänger werden bei erkannten Änderungen benachrichtigt.", + "Regulations Tracker notification settings.": + "Benachrichtigungseinstellungen für den Regulierungsverfolger.", + "Only administrators can change Regulations Tracker notification settings.": + "Nur Administratoren können die Benachrichtigungseinstellungen des Regulierungsverfolgers ändern.", + "Choose who receives a notification when a tracked country": + "Wählen Sie, wer eine Benachrichtigung erhält, wenn ein verfolgtes Land", + "Choose who receives a notification when a tracked country's regulations change. If no recipients are set, no digest is sent.": + "Wählen Sie, wer eine Benachrichtigung erhält, wenn sich die Regulierungen eines verfolgten Landes ändern. Wenn keine Empfänger festgelegt sind, wird kein Digest gesendet.", + "Back to browse": "Zurück zur Übersicht", + "We couldn't find this country in the regulations catalogue.": + "Dieses Land wurde im Regulierungskatalog nicht gefunden.", + "Data may be outdated": "Daten möglicherweise veraltet", + "Disclaimer": "Haftungsausschluss", + "Regulations": "Regulierungen", + "Last known summary": "Letzte bekannte Zusammenfassung", + "Last recorded changes": "Zuletzt aufgezeichnete Änderungen", + "No regulation data is available for this country yet. Check back later as the feed is updated regularly.": + "Für dieses Land sind noch keine Regulierungsdaten verfügbar. Schauen Sie später wieder vorbei, da der Feed regelmäßig aktualisiert wird.", + "How this change affects your organisation": + "Wie sich diese Änderung auf Ihre Organisation auswirkt", + "This analysis predates the latest change.": + "Diese Analyse stammt aus der Zeit vor der letzten Änderung.", + "Re-analyse": "Neu analysieren", + "Analyse how regulation changes affect my organisation": + "Analysieren Sie, wie Regulierungsänderungen Ihre Organisation beeinflussen", + "Configure an LLM key to enable impact analysis.": + "Konfigurieren Sie einen LLM-Schlüssel, um die Auswirkungsanalyse zu aktivieren.", + "Configure key": "Schlüssel konfigurieren", + "Add an LLM key": "LLM-Schlüssel hinzufügen", + "Manage keys": "Schlüssel verwalten", + "Impact analysis: active": "Auswirkungsanalyse: aktiv", + "Impact analysis last ran: ": "Auswirkungsanalyse zuletzt ausgeführt: ", + "Impact analysis has not run yet.": "Die Auswirkungsanalyse wurde noch nicht ausgeführt.", + "Check for updates progress": "Fortschritt der Aktualisierungsprüfung", + "We couldn't check for updates right now. Please try again.": + "Die Aktualisierungsprüfung konnte gerade nicht durchgeführt werden. Bitte versuchen Sie es erneut.", }, fr: { @@ -17338,6 +17434,101 @@ export const translations: Record> = { "Get notified when assessments change": "Être averti lorsque les évaluations changent", "Tracked apps are included in the weekly change digest, so configured recipients hear about score, grade, or policy changes.": "Les applications suivies sont incluses dans le récapitulatif hebdomadaire des modifications, afin que les destinataires configurés soient informés des changements de score, de note ou de politique.", + "Track AI regulations and compliance requirements across jurisdictions": + "Suivez les réglementations sur l'IA et les exigences de conformité dans toutes les juridictions", + // Regulations Tracker + "Horizon": "Horizon", + "A dated changelog of AI-regulation changes across all tracked jurisdictions, newest first.": + "Un journal daté des modifications des réglementations IA pour toutes les juridictions suivies, du plus récent au plus ancien.", + "Deadlines": "Échéances", + "Upcoming effective-date milestones for AI regulations, soonest first, plus regulations with no scheduled date yet.": + "Prochaines échéances d'entrée en vigueur des réglementations IA, les plus proches en premier, plus les réglementations sans date prévue.", + "International frameworks": "Cadres internationaux", + "Country details": "Détails du pays", + "AI regulations for a specific country": "Réglementations sur l'IA pour un pays spécifique", + "No international frameworks are available yet. Cross-border frameworks like the OECD AI Principles will appear here once the feed publishes them.": + "Aucun cadre international n'est encore disponible. Les cadres transfrontaliers tels que les Principes de l'OCDE sur l'IA apparaîtront ici une fois publiés dans le flux.", + "No upcoming regulation deadlines are recorded yet. Effective-date milestones for AI regulations will appear here as they are published.": + "Aucune échéance réglementaire à venir n'est encore enregistrée. Les jalons de dates d'entrée en vigueur des réglementations sur l'IA apparaîtront ici au fur et à mesure de leur publication.", + "Cross-border AI governance frameworks and principles that complement national regulations.": + "Cadres et principes transfrontaliers de gouvernance de l'IA qui complètent les réglementations nationales.", + "Key obligations": "Obligations clés", + "Key principles": "Principes clés", + "Max penalty:": "Pénalité maximale :", + "Practical takeaway": "Conclusion pratique", + "Next 12 months": "12 prochains mois", + "Effective dates for the coming year. Months closer to today are highlighted.": + "Dates d'entrée en vigueur pour l'année à venir. Les mois les plus proches d'aujourd'hui sont mis en évidence.", + "No effective dates in the next 12 months.": + "Aucune date d'entrée en vigueur dans les 12 prochains mois.", + "Scheduled": "Planifié", + "Not yet scheduled": "Pas encore planifié", + "No regulation changes have been recorded yet.": + "Aucune modification réglementaire n'a encore été enregistrée.", + "No upcoming regulation deadlines are recorded yet.": + "Aucune échéance réglementaire à venir n'est encore enregistrée.", + "No international frameworks are recorded yet.": + "Aucun cadre international n'est encore enregistré.", + "Find them in Browse.": "Retrouvez-les dans Parcourir.", + "Showing the last known changelog; live data is temporarily unavailable.": + "Affichage du dernier journal connu ; les données en direct sont temporairement indisponibles.", + "Showing the last known deadlines; live data is temporarily unavailable.": + "Affichage des dernières échéances connues ; les données en direct sont temporairement indisponibles.", + "Showing the last known frameworks; live data is temporarily unavailable.": + "Affichage des derniers cadres connus ; les données en direct sont temporairement indisponibles.", + "Regulations tracker": "Suivi des réglementations", + "Browse the catalogue of countries and jurisdictions. Track the ones relevant to your organization to receive updates when their AI regulations change.": + "Parcourez le catalogue des pays et juridictions. Suivez ceux pertinents pour votre organisation afin de recevoir des mises à jour lorsque leurs réglementations IA changent.", + "Search countries or jurisdictions": "Rechercher des pays ou des juridictions", + "No countries match your filters.": "Aucun pays ne correspond à vos filtres.", + "Select all on page": "Tout sélectionner sur la page", + "View source": "Voir la source", + "Tracked countries": "Pays suivis", + "Countries and jurisdictions your organization is tracking. You": + "Pays et juridictions que votre organisation suit. Vous", + "Countries and jurisdictions your organization is tracking. You'll be notified when their AI regulations change.": + "Pays et juridictions que votre organisation suit. Vous serez notifié lorsque leurs réglementations IA changeront.", + "Find countries in Browse": "Trouver des pays dans Parcourir", + "Open the Browse tab to explore the full catalogue, then track the countries relevant to your organization.": + "Ouvrez l'onglet Parcourir pour explorer le catalogue complet, puis suivez les pays pertinents pour votre organisation.", + "Get notified when regulations change": "Être averti lorsque les réglementations changent", + "Tracked countries are monitored for regulatory updates. Configured recipients are notified when changes are detected.": + "Les pays suivis sont surveillés pour détecter les mises à jour réglementaires. Les destinataires configurés sont notifiés lorsque des changements sont détectés.", + "Regulations Tracker notification settings.": + "Paramètres de notification du suivi des réglementations.", + "Only administrators can change Regulations Tracker notification settings.": + "Seuls les administrateurs peuvent modifier les paramètres de notification du suivi des réglementations.", + "Choose who receives a notification when a tracked country": + "Choisissez qui reçoit une notification lorsqu'un pays suivi", + "Choose who receives a notification when a tracked country's regulations change. If no recipients are set, no digest is sent.": + "Choisissez qui reçoit une notification lorsque les réglementations d'un pays suivi changent. Si aucun destinataire n'est défini, aucun récapitulatif n'est envoyé.", + "Back to browse": "Retour à la navigation", + "We couldn't find this country in the regulations catalogue.": + "Ce pays est introuvable dans le catalogue des réglementations.", + "Data may be outdated": "Les données peuvent être obsolètes", + "Disclaimer": "Avertissement", + "Regulations": "Réglementations", + "Last known summary": "Dernier résumé connu", + "Last recorded changes": "Dernières modifications enregistrées", + "No regulation data is available for this country yet. Check back later as the feed is updated regularly.": + "Aucune donnée réglementaire n'est encore disponible pour ce pays. Revenez plus tard, car le flux est mis à jour régulièrement.", + "How this change affects your organisation": "Comment ce changement affecte votre organisation", + "This analysis predates the latest change.": + "Cette analyse est antérieure au dernier changement.", + "Re-analyse": "Réanalyser", + "Analyse how regulation changes affect my organisation": + "Analysez comment les changements réglementaires affectent votre organisation", + "Configure an LLM key to enable impact analysis.": + "Configurez une clé LLM pour activer l'analyse d'impact.", + "Configure key": "Configurer la clé", + "Add an LLM key": "Ajouter une clé LLM", + "Manage keys": "Gérer les clés", + "Impact analysis: active": "Analyse d'impact : active", + "Impact analysis last ran: ": "Dernière exécution de l'analyse d'impact : ", + "Impact analysis has not run yet.": "L'analyse d'impact n'a pas encore été exécutée.", + "Check for updates progress": "Progression de la vérification des mises à jour", + "We couldn't check for updates right now. Please try again.": + "Nous n'avons pas pu vérifier les mises à jour pour le moment. Veuillez réessayer.", }, es: { // AI Trust Index @@ -25890,5 +26081,99 @@ export const translations: Record> = { "Get notified when assessments change": "Reciba avisos cuando cambien las evaluaciones", "Tracked apps are included in the weekly change digest, so configured recipients hear about score, grade, or policy changes.": "Las aplicaciones seguidas se incluyen en el resumen semanal de cambios, de modo que los destinatarios configurados se enteran de los cambios de puntuación, calificación o política.", + "Track AI regulations and compliance requirements across jurisdictions": + "Realice seguimiento de las regulaciones de IA y los requisitos de cumplimiento en todas las jurisdicciones", + // Regulations Tracker + "Horizon": "Horizonte", + "A dated changelog of AI-regulation changes across all tracked jurisdictions, newest first.": + "Un registro fechado de cambios en las regulaciones de IA de todas las jurisdicciones supervisadas, el más reciente primero.", + "Deadlines": "Plazos", + "Upcoming effective-date milestones for AI regulations, soonest first, plus regulations with no scheduled date yet.": + "Próximos hitos de entrada en vigor de las regulaciones de IA, los más cercanos primero, más regulaciones sin fecha programada aún.", + "International frameworks": "Marcos internacionales", + "Country details": "Detalles del país", + "AI regulations for a specific country": "Regulaciones de IA para un país específico", + "No international frameworks are available yet. Cross-border frameworks like the OECD AI Principles will appear here once the feed publishes them.": + "Aún no hay marcos internacionales disponibles. Los marcos transfronterizos como los Principios de IA de la OCDE aparecerán aquí una vez que se publiquen en el feed.", + "No upcoming regulation deadlines are recorded yet. Effective-date milestones for AI regulations will appear here as they are published.": + "Aún no se han registrado próximos plazos regulatorios. Los hitos de fechas de entrada en vigor de las regulaciones de IA aparecerán aquí a medida que se publiquen.", + "Cross-border AI governance frameworks and principles that complement national regulations.": + "Marcos y principios transfronterizos de gobernanza de IA que complementan las regulaciones nacionales.", + "Key obligations": "Obligaciones clave", + "Key principles": "Principios clave", + "Max penalty:": "Sanción máxima:", + "Practical takeaway": "Conclusión práctica", + "Next 12 months": "Próximos 12 meses", + "Effective dates for the coming year. Months closer to today are highlighted.": + "Fechas de entrada en vigor para el próximo año. Los meses más cercanos a hoy están resaltados.", + "No effective dates in the next 12 months.": + "No hay fechas de entrada en vigor en los próximos 12 meses.", + "Scheduled": "Programado", + "Not yet scheduled": "Aún no programado", + "No regulation changes have been recorded yet.": + "Todavía no se han registrado cambios regulatorios.", + "No upcoming regulation deadlines are recorded yet.": + "Todavía no se han registrado plazos regulatorios próximos.", + "No international frameworks are recorded yet.": + "Todavía no se han registrado marcos internacionales.", + "Find them in Browse.": "Encuéntralos en Explorar.", + "Showing the last known changelog; live data is temporarily unavailable.": + "Mostrando el último registro de cambios conocido; los datos en tiempo real no están disponibles temporalmente.", + "Showing the last known deadlines; live data is temporarily unavailable.": + "Mostrando los últimos plazos conocidos; los datos en tiempo real no están disponibles temporalmente.", + "Showing the last known frameworks; live data is temporarily unavailable.": + "Mostrando los últimos marcos conocidos; los datos en tiempo real no están disponibles temporalmente.", + "Regulations tracker": "Rastreador de regulaciones", + "Browse the catalogue of countries and jurisdictions. Track the ones relevant to your organization to receive updates when their AI regulations change.": + "Explore el catálogo de países y jurisdicciones. Siga los relevantes para su organización para recibir actualizaciones cuando cambien sus regulaciones de IA.", + "Search countries or jurisdictions": "Buscar países o jurisdicciones", + "No countries match your filters.": "Ningún país coincide con sus filtros.", + "Select all on page": "Seleccionar todo en la página", + "View source": "Ver fuente", + "Tracked countries": "Países seguidos", + "Countries and jurisdictions your organization is tracking. You": + "Países y jurisdicciones que su organización está siguiendo. Se", + "Countries and jurisdictions your organization is tracking. You'll be notified when their AI regulations change.": + "Países y jurisdicciones que su organización está siguiendo. Se le notificará cuando cambien sus regulaciones de IA.", + "Find countries in Browse": "Buscar países en Explorar", + "Open the Browse tab to explore the full catalogue, then track the countries relevant to your organization.": + "Abra la pestaña Explorar para ver el catálogo completo y, a continuación, siga los países relevantes para su organización.", + "Get notified when regulations change": "Reciba avisos cuando cambien las regulaciones", + "Tracked countries are monitored for regulatory updates. Configured recipients are notified when changes are detected.": + "Los países seguidos se monitorizan para detectar actualizaciones regulatorias. Los destinatarios configurados son notificados cuando se detectan cambios.", + "Regulations Tracker notification settings.": + "Configuración de notificaciones del rastreador de regulaciones.", + "Only administrators can change Regulations Tracker notification settings.": + "Solo los administradores pueden cambiar la configuración de notificaciones del rastreador de regulaciones.", + "Choose who receives a notification when a tracked country": + "Elija quién recibe una notificación cuando un país seguido", + "Choose who receives a notification when a tracked country's regulations change. If no recipients are set, no digest is sent.": + "Elija quién recibe una notificación cuando cambien las regulaciones de un país seguido. Si no se establecen destinatarios, no se envía ningún resumen.", + "Back to browse": "Volver a explorar", + "We couldn't find this country in the regulations catalogue.": + "No se encontró este país en el catálogo de regulaciones.", + "Data may be outdated": "Los datos pueden estar desactualizados", + "Disclaimer": "Aviso legal", + "Regulations": "Regulaciones", + "Last known summary": "Último resumen conocido", + "Last recorded changes": "Últimos cambios registrados", + "No regulation data is available for this country yet. Check back later as the feed is updated regularly.": + "Aún no hay datos de regulación disponibles para este país. Vuelva más tarde, ya que el feed se actualiza regularmente.", + "How this change affects your organisation": "Cómo afecta este cambio a su organización", + "This analysis predates the latest change.": "Este análisis es anterior al último cambio.", + "Re-analyse": "Volver a analizar", + "Analyse how regulation changes affect my organisation": + "Analice cómo los cambios regulatorios afectan a su organización", + "Configure an LLM key to enable impact analysis.": + "Configure una clave LLM para habilitar el análisis de impacto.", + "Configure key": "Configurar clave", + "Add an LLM key": "Añadir una clave LLM", + "Manage keys": "Gestionar claves", + "Impact analysis: active": "Análisis de impacto: activo", + "Impact analysis last ran: ": "Último análisis de impacto ejecutado: ", + "Impact analysis has not run yet.": "El análisis de impacto aún no se ha ejecutado.", + "Check for updates progress": "Progreso de la verificación de actualizaciones", + "We couldn't check for updates right now. Please try again.": + "No pudimos verificar las actualizaciones en este momento. Por favor, inténtelo de nuevo.", }, }; diff --git a/Clients/src/presentation/components/AppSwitcher/index.tsx b/Clients/src/presentation/components/AppSwitcher/index.tsx index 1b4ba42d2b..f33c3aadfb 100644 --- a/Clients/src/presentation/components/AppSwitcher/index.tsx +++ b/Clients/src/presentation/components/AppSwitcher/index.tsx @@ -1,6 +1,6 @@ import { FC, memo } from "react"; import { Stack, Tooltip, Box, Typography, useTheme } from "@mui/material"; -import { Shield, FlaskConical, ScanSearch, Eye, Router, Crown, Gauge } from "lucide-react"; +import { Shield, FlaskConical, ScanSearch, Eye, Router, Crown, Gauge, Scale } from "lucide-react"; import { AppModule } from "../../../application/redux/ui/uiSlice"; import "./index.css"; @@ -43,6 +43,12 @@ const modules: ModuleItem[] = [ label: "AI Trust Index", description: "Browse AI app risk scores and track the apps you use", }, + { + id: "regulations-tracker", + icon: , + label: "Regulations tracker", + description: "Track AI regulations and compliance requirements across jurisdictions", + }, { id: "shadow-ai", icon: , diff --git a/Clients/src/presentation/components/ContextSidebar/index.tsx b/Clients/src/presentation/components/ContextSidebar/index.tsx index 8c016bf854..8bb97fa8d9 100644 --- a/Clients/src/presentation/components/ContextSidebar/index.tsx +++ b/Clients/src/presentation/components/ContextSidebar/index.tsx @@ -5,6 +5,7 @@ import { useAIDetectionSidebarContextSafe } from "../../../application/contexts/ import { useShadowAISidebarContextSafe } from "../../../application/contexts/ShadowAISidebar.context"; import { useAIGatewaySidebarContextSafe } from "../../../application/contexts/AIGatewaySidebar.context"; import { useAITrustIndexSidebarContextSafe } from "../../../application/contexts/AITrustIndexSidebar.context"; +import { useRegulationsTrackerSidebarContextSafe } from "../../../application/contexts/RegulationsTrackerSidebar.context"; import Sidebar from "../Sidebar"; import SuperAdminSidebar from "../SuperAdminSidebar"; import EvalsSidebar from "../../pages/EvalsDashboard/EvalsSidebar"; @@ -12,6 +13,7 @@ import AIDetectionSidebar from "../../pages/AIDetection/AIDetectionSidebar"; import ShadowAISidebar from "../../pages/ShadowAI/ShadowAISidebar"; import AIGatewaySidebar from "../../pages/AIGateway/AIGatewaySidebar"; import AITrustIndexSidebar from "../../pages/AITrustIndex/AITrustIndexSidebar"; +import RegulationsTrackerSidebar from "../../pages/RegulationsTracker/RegulationsTrackerSidebar"; interface ContextSidebarProps { activeModule: AppModule; @@ -45,6 +47,7 @@ export function ContextSidebar({ const shadowAiSidebarContext = useShadowAISidebarContextSafe(); const aiGatewaySidebarContext = useAIGatewaySidebarContextSafe(); const aiTrustIndexSidebarContext = useAITrustIndexSidebarContextSafe(); + const regulationsTrackerSidebarContext = useRegulationsTrackerSidebarContextSafe(); const location = useLocation(); const navigate = useNavigate(); @@ -223,6 +226,32 @@ export function ContextSidebar({ /> ); } + case "regulations-tracker": { + const regulationsTrackerTab = location.pathname.includes("/regulations-tracker/tracked") + ? "tracked" + : location.pathname.includes("/regulations-tracker/settings") + ? "settings" + : location.pathname.includes("/regulations-tracker/horizon") + ? "horizon" + : location.pathname.includes("/regulations-tracker/deadlines") + ? "deadlines" + : location.pathname.includes("/regulations-tracker/frameworks") + ? "frameworks" + : "browse"; + + const handleRegulationsTrackerTabChange = (newTab: string) => { + navigate(`/regulations-tracker/${newTab}`); + }; + + return ( + + ); + } case "super-admin": return ; default: diff --git a/Clients/src/presentation/components/Link/VWLink/index.tsx b/Clients/src/presentation/components/Link/VWLink/index.tsx index 8c7476ef63..cb2da58828 100644 --- a/Clients/src/presentation/components/Link/VWLink/index.tsx +++ b/Clients/src/presentation/components/Link/VWLink/index.tsx @@ -50,6 +50,7 @@ export const VWLink = memo(function VWLink({ openInNewTab = false, showUnderline = true, showIcon = true, + alwaysShowIcon = false, sx, className, ariaLabel, @@ -124,7 +125,7 @@ export const VWLink = memo(function VWLink({ > {children} - {url && isHovered && showIcon && ( + {url && showIcon && (isHovered || alwaysShowIcon) && ( = { "/ai-trust-index/tracked": "Tracked", "/ai-trust-index/settings": "Settings", + // Regulations Tracker + "/regulations-tracker": "Regulations Tracker", + "/regulations-tracker/browse": "Browse", + "/regulations-tracker/tracked": "Tracked", + "/regulations-tracker/horizon": "Activity", + "/regulations-tracker/deadlines": "Deadlines", + "/regulations-tracker/frameworks": "Frameworks", + "/regulations-tracker/settings": "Settings", + // AI Gateway "/ai-gateway": "AI gateway", "/ai-gateway/dashboard": "Dashboard", @@ -307,6 +320,19 @@ export const routeIconMapping: Record React.ReactNode> = { "/ai-trust-index/tracked": () => React.createElement(Star, { size: 14, strokeWidth: 1.5 }), "/ai-trust-index/settings": () => React.createElement(Settings, { size: 14, strokeWidth: 1.5 }), + // Regulations Tracker + "/regulations-tracker": () => React.createElement(Scale, { size: 14, strokeWidth: 1.5 }), + "/regulations-tracker/browse": () => React.createElement(Globe, { size: 14, strokeWidth: 1.5 }), + "/regulations-tracker/tracked": () => React.createElement(Star, { size: 14, strokeWidth: 1.5 }), + "/regulations-tracker/horizon": () => + React.createElement(History, { size: 14, strokeWidth: 1.5 }), + "/regulations-tracker/deadlines": () => + React.createElement(CalendarClock, { size: 14, strokeWidth: 1.5 }), + "/regulations-tracker/frameworks": () => + React.createElement(Landmark, { size: 14, strokeWidth: 1.5 }), + "/regulations-tracker/settings": () => + React.createElement(Settings, { size: 14, strokeWidth: 1.5 }), + // Intake forms "/intake-forms": () => React.createElement(ClipboardList, { size: 14, strokeWidth: 1.5 }), "/intake-forms/submissions": () => React.createElement(Inbox, { size: 14, strokeWidth: 1.5 }), @@ -440,6 +466,15 @@ export const dynamicRoutePatterns = [ description: "Full assessment for a specific AI Trust Index app", icon: () => React.createElement(Gauge, { size: 14, strokeWidth: 1.5 }), }, + { + // Only consulted when there's no exact routeMapping hit, so the explicit + // /regulations-tracker/browse|tracked|horizon|deadlines|frameworks|settings + // entries take precedence; this catches the per-country detail page. + pattern: /\/regulations-tracker\/[^/]+$/, + label: "Country details", + description: "AI regulations for a specific country", + icon: () => React.createElement(Globe, { size: 14, strokeWidth: 1.5 }), + }, { pattern: /\/super-admin\/organizations\/\d+\/users/, label: "Organization users", diff --git a/Clients/src/presentation/containers/Dashboard/index.tsx b/Clients/src/presentation/containers/Dashboard/index.tsx index a794c403a1..db6add9858 100644 --- a/Clients/src/presentation/containers/Dashboard/index.tsx +++ b/Clients/src/presentation/containers/Dashboard/index.tsx @@ -9,6 +9,7 @@ import { AIDetectionSidebarProvider } from "../../../application/contexts/AIDete import { ShadowAISidebarProvider } from "../../../application/contexts/ShadowAISidebar.context"; import { AIGatewaySidebarProvider } from "../../../application/contexts/AIGatewaySidebar.context"; import { AITrustIndexSidebarProvider } from "../../../application/contexts/AITrustIndexSidebar.context"; +import { RegulationsTrackerSidebarProvider } from "../../../application/contexts/RegulationsTrackerSidebar.context"; import DemoAppBanner from "../../components/DemoBanner/DemoAppBanner"; import { getAllProjects } from "../../../application/repository/project.repository"; import { @@ -300,104 +301,106 @@ const Dashboard: FC = ({ reloadTrigger }) => { - - - setOpenDemoDataModal(true)} - onOpenDeleteDemoData={() => setOpenDeleteDemoDataModal(true)} - onDismissDemoDataButton={() => { - localStorage.setItem("hideDemoDataButton", "true"); - setShowDemoDataButton(false); - }} - showDemoDataButton={showDemoDataButton} - hasDemoData={hasDemoData} - isAdmin={isAdmin} - /> + - - <> - - {alertState && ( - setAlertState(undefined)} - /> - )} - {showToastNotification && } - - - - + + setOpenDemoDataModal(true)} + onOpenDeleteDemoData={() => setOpenDeleteDemoDataModal(true)} + onDismissDemoDataButton={() => { + localStorage.setItem("hideDemoDataButton", "true"); + setShowDemoDataButton(false); + }} + showDemoDataButton={showDemoDataButton} + hasDemoData={hasDemoData} + isAdmin={isAdmin} + /> + + + <> + + {alertState && ( + setAlertState(undefined)} + /> + )} + {showToastNotification && } + + + + + + + {/* Demo Data Modals */} + setOpenDemoDataModal(false)} + title="Create demo data" + description="Generate sample data to explore VerifyWise features" + submitButtonText="Create demo data" + onSubmit={handleCreateDemoData} + isSubmitting={showToastNotification} + maxWidth="480px" + > + + This will generate sample projects, risks, vendors, and policies to help you + explore VerifyWise. You can remove this demo data at any time. + + + + setOpenDeleteDemoDataModal(false)} + title="Delete demo data" + description="Remove all demo data from your workspace" + submitButtonText="Delete demo data" + onSubmit={handleDeleteDemoData} + isSubmitting={showToastNotification} + submitButtonColor={status.error.text} + maxWidth="480px" + > + + This will remove all sample projects, risks, vendors, and policies that were + generated as demo data. Your real data will remain untouched. + + - - {/* Demo Data Modals */} - setOpenDemoDataModal(false)} - title="Create demo data" - description="Generate sample data to explore VerifyWise features" - submitButtonText="Create demo data" - onSubmit={handleCreateDemoData} - isSubmitting={showToastNotification} - maxWidth="480px" - > - - This will generate sample projects, risks, vendors, and policies to help you - explore VerifyWise. You can remove this demo data at any time. - - - - setOpenDeleteDemoDataModal(false)} - title="Delete demo data" - description="Remove all demo data from your workspace" - submitButtonText="Delete demo data" - onSubmit={handleDeleteDemoData} - isSubmitting={showToastNotification} - submitButtonColor={status.error.text} - maxWidth="480px" - > - - This will remove all sample projects, risks, vendors, and policies that were - generated as demo data. Your real data will remain untouched. - - - + diff --git a/Clients/src/presentation/pages/RegulationsTracker/Browse/index.tsx b/Clients/src/presentation/pages/RegulationsTracker/Browse/index.tsx new file mode 100644 index 0000000000..6c21062bed --- /dev/null +++ b/Clients/src/presentation/pages/RegulationsTracker/Browse/index.tsx @@ -0,0 +1,344 @@ +/** + * @fileoverview Regulations Tracker — Browse tab. + * + * Lists the full country/jurisdiction catalogue with region filter and search. + * Supports per-row tracking and bulk-track selection. + * + * @module pages/RegulationsTracker/Browse + */ + +import { useState, useEffect, useMemo, useCallback } from "react"; +import { useNavigate } from "react-router-dom"; +import { Box, Stack, TablePagination, CircularProgress } from "@mui/material"; +import { SearchX, AlertTriangle, CheckSquare, Square } from "lucide-react"; +import { SearchBox } from "../../../components/Search"; +import { CustomSelect } from "../../../components/CustomSelect"; +import { CustomizableButton } from "../../../components/button/customizable-button"; +import { EmptyState } from "../../../components/EmptyState"; +import { PageHeaderExtended } from "../../../components/Layout/PageHeaderExtended"; +import TablePaginationActions from "../../../components/TablePagination"; +import { palette } from "../../../themes/palette"; +import { + useCountries, + useTrackCountry, + useUntrackCountry, + useTrackBulk, +} from "../../../../application/hooks/useRegulationsTracker"; +import { useRegulationsTrackerSidebarContextSafe } from "../../../../application/contexts/RegulationsTrackerSidebar.context"; +import { useAuth } from "../../../../application/hooks/useAuth"; +import { useTrackerAlert } from "../useTrackerAlert"; +import { CountryRow, CountryRowCard } from "../CountryRowCard"; + +const PAGE_SIZE = 24; + +export default function Browse() { + const navigate = useNavigate(); + const sidebar = useRegulationsTrackerSidebarContextSafe(); + const { userRoleName, isSuperAdmin } = useAuth(); + // Tracking is available to admins and editors. A super-admin viewing an organization + // has read-only access (the backend blocks all writes), so they see a read-only + // catalogue, as do other roles. + const canTrack = !isSuperAdmin && (userRoleName === "Admin" || userRoleName === "Editor"); + const { showError, AlertSlot } = useTrackerAlert(); + + const [searchInput, setSearchInput] = useState(""); + const [search, setSearch] = useState(""); + const [region, setRegion] = useState(""); + const [page, setPage] = useState(0); + const [selected, setSelected] = useState([]); + + // Debounce the search input (~300ms) before it hits the query. + useEffect(() => { + const id = setTimeout(() => { + setSearch(searchInput); + setPage(0); + }, 300); + return () => clearTimeout(id); + }, [searchInput]); + + const { data, isLoading, isError } = useCountries({ region, q: search }); + // Unfiltered fetch used only to build the stable region dropdown — so selecting a region + // never removes other regions from the list (fix: region options were previously derived + // from the filtered result, which collapsed to a single option after region selection). + const { data: allData } = useCountries({}); + + const trackCountry = useTrackCountry(); + const untrackCountry = useUntrackCountry(); + const trackBulk = useTrackBulk(); + + const rows: CountryRow[] = useMemo(() => { + const list = Array.isArray(data?.data) ? data.data : []; + return list; + }, [data]); + + const total = rows.length; + + // Build region options from the UNFILTERED country list so the dropdown always shows all + // regions regardless of the active region/search filter. + const regionOptions = useMemo(() => { + const allRows: CountryRow[] = Array.isArray(allData?.data) ? allData.data : []; + const allTotal = allRows.length; + const regions = Array.from(new Set(allRows.map((r) => r.region).filter(Boolean))) as string[]; + regions.sort(); + return [ + { value: "", label: allTotal ? `All regions (${allTotal})` : "All regions" }, + ...regions.map((r) => ({ + value: r, + label: r, + })), + ]; + }, [allData]); + + const pagedRows = useMemo( + () => rows.slice(page * PAGE_SIZE, page * PAGE_SIZE + PAGE_SIZE), + [rows, page], + ); + + // Clamp page when dataset shrinks. + useEffect(() => { + if (total > 0 && page > 0 && page * PAGE_SIZE >= total) { + setPage(Math.max(0, Math.ceil(total / PAGE_SIZE) - 1)); + } + }, [total, page]); + + // Only untracked rows are selectable for bulk-track. + const selectableSlugs = useMemo( + () => pagedRows.filter((r) => !r.is_tracked).map((r) => r.slug), + [pagedRows], + ); + const allOnPageSelected = + selectableSlugs.length > 0 && selectableSlugs.every((s) => selected.includes(s)); + const someOnPageSelected = selectableSlugs.some((s) => selected.includes(s)); + + // Clear selection when filters/page change. + useEffect(() => { + setSelected([]); + }, [search, region, page]); + + const toggleSelectAll = useCallback(() => { + setSelected((prev) => { + if (allOnPageSelected) { + return prev.filter((s) => !selectableSlugs.includes(s)); + } + const next = new Set(prev); + selectableSlugs.forEach((s) => next.add(s)); + return Array.from(next); + }); + }, [allOnPageSelected, selectableSlugs]); + + const toggleRow = useCallback((slug: string) => { + setSelected((prev) => (prev.includes(slug) ? prev.filter((s) => s !== slug) : [...prev, slug])); + }, []); + + const handleTrackSelected = useCallback(() => { + if (selected.length === 0) return; + trackBulk.mutate(selected, { + onSuccess: () => { + setSelected([]); + sidebar?.refreshTrackedCount(); + }, + onError: () => showError("We couldn't track the selected countries. Please try again."), + }); + }, [selected, trackBulk, sidebar, showError]); + + const handleToggleTrack = useCallback( + (row: CountryRow) => { + const onDone = () => sidebar?.refreshTrackedCount(); + if (row.is_tracked) { + untrackCountry.mutate(row.slug, { + onSuccess: onDone, + onError: () => showError(`We couldn't untrack ${row.name}. Please try again.`), + }); + } else { + trackCountry.mutate(row.slug, { + onSuccess: onDone, + onError: () => showError(`We couldn't track ${row.name}. Please try again.`), + }); + } + }, + [trackCountry, untrackCountry, sidebar, showError], + ); + + const isEmpty = !isLoading && rows.length === 0; + + return ( + + {AlertSlot} + + {/* Filters */} + + setSearchInput(value)} + fullWidth={false} + sx={{ width: 280 }} + /> + { + setRegion(String(v)); + setPage(0); + return true; + }} + options={regionOptions} + /> + + {/* Select-all checkbox area — only for roles that can track */} + {canTrack && ( + + { + if (el) el.indeterminate = !allOnPageSelected && someOnPageSelected; + }} + onChange={toggleSelectAll} + sx={{ cursor: "pointer", width: 16, height: 16, accentColor: palette.brand.primary }} + /> + + + )} + + + {isLoading && ( + + + + )} + + {isError && ( + + )} + + {isEmpty && !isError && ( + + )} + + {!isLoading && !isError && pagedRows.length > 0 && ( + <> + {/* Country rows */} + + {pagedRows.map((row) => { + const isSelected = selected.includes(row.slug); + return ( + navigate(`/regulations-tracker/${row.slug}`)} + actionLabel={canTrack ? (row.is_tracked ? "Untrack" : "Track") : undefined} + onAction={ + canTrack + ? (e) => { + e.stopPropagation(); + handleToggleTrack(row); + } + : undefined + } + actionDisabled={ + (trackCountry.isPending && trackCountry.variables === row.slug) || + (untrackCountry.isPending && untrackCountry.variables === row.slug) + } + checkbox={ + !canTrack ? undefined : row.is_tracked ? ( + + + + ) : ( + { + e.stopPropagation(); + toggleRow(row.slug); + }} + sx={{ + "width": 16, + "height": 16, + "flexShrink": 0, + "p": 0, + "border": "none", + "background": "none", + "cursor": "pointer", + "display": "flex", + "alignItems": "center", + "justifyContent": "center", + "&:focus-visible": { + outline: `2px solid ${palette.brand.primary}`, + outlineOffset: "2px", + borderRadius: "2px", + }, + }} + > + + + + ) + } + /> + ); + })} + + + + setPage(p)} + rowsPerPage={PAGE_SIZE} + rowsPerPageOptions={[PAGE_SIZE]} + ActionsComponent={TablePaginationActions as any} + labelRowsPerPage="Rows per page" + sx={{ mt: "24px" }} + /> + + + )} + + ); +} diff --git a/Clients/src/presentation/pages/RegulationsTracker/CountryDetail/index.tsx b/Clients/src/presentation/pages/RegulationsTracker/CountryDetail/index.tsx new file mode 100644 index 0000000000..93a43141a9 --- /dev/null +++ b/Clients/src/presentation/pages/RegulationsTracker/CountryDetail/index.tsx @@ -0,0 +1,811 @@ +/** + * @fileoverview Regulations Tracker — Country detail. + * + * Full view for a single country/jurisdiction: header (name, region, track + * toggle), regulations list, timeline, change history, and feed disclaimer. + * + * The feed disclaimer text is rendered VERBATIM from the API payload + * (meta.disclaimer / scopeStatement). It is never paraphrased or translated — + * only the UI chrome around it is in the app language. + * + * A "stale" indicator is shown when the payload includes stale: true. + * + * @module pages/RegulationsTracker/CountryDetail + */ + +import { useCallback } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { Box, Stack, Typography, CircularProgress, useTheme } from "@mui/material"; +import { ArrowLeft, Globe, SearchX, AlertTriangle, Clock } from "lucide-react"; +import { PageBreadcrumbs } from "../../../components/breadcrumbs/PageBreadcrumbs"; +import { CustomizableButton } from "../../../components/button/customizable-button"; +import { EmptyState } from "../../../components/EmptyState"; +import Chip from "../../../components/Chip"; +import { VWLink } from "../../../components/Link"; +import { palette } from "../../../themes/palette"; +import { + useCountryDetail, + useImpactAnalysis, + useRefreshImpactAnalysis, + useTrackCountry, + useUntrackCountry, +} from "../../../../application/hooks/useRegulationsTracker"; +import { useRegulationsTrackerSidebarContextSafe } from "../../../../application/contexts/RegulationsTrackerSidebar.context"; +import { useAuth } from "../../../../application/hooks/useAuth"; +import { useTrackerAlert } from "../useTrackerAlert"; +import { regulationStatusVariant } from "../statusVariant"; + +// Amber/warning colors for the stale-data banner. +// palette.status.warning uses a slightly different hue (#FFF8E1/#795548), +// so we keep the original designer values as named consts rather than +// substituting a mismatched token (same approach as AITrustIndex/AppDetail). +const STALE_BANNER_BG = "#FFFBEA"; +const STALE_BANNER_ICON_COLOR = "#B45309"; +const STALE_BANNER_TEXT_COLOR = "#92400E"; + +interface Regulation { + name: string; + type?: string; + status?: string; + effectiveDate?: string; + dateConfidence?: string; + scope?: string; + obligations?: string[]; + maxPenalty?: string; + industryTags?: string[]; + sourceUrl?: string; + lastVerified?: string; +} + +interface TimelineEvent { + date: string; + description?: string; +} + +interface ChangeHistoryEntry { + date: string; + summary: string; + type?: string; +} + +interface CountryDetailMeta { + disclaimer?: string; + scopeStatement?: string; + last_updated?: string; + source?: string; +} + +interface CountryDetailData { + slug: string; + name: string; + region?: string; + iso2?: string; + /** Unicode flag emoji from the feed (e.g. "🇪🇺"); falls back to a globe icon. */ + flag?: string; + oneLiner?: string; + executiveSummary?: string; + practicalTakeaway?: string; + is_tracked?: boolean; + stale?: boolean; + regulations?: Regulation[]; + timeline?: TimelineEvent[]; + change_history?: ChangeHistoryEntry[]; + meta?: CountryDetailMeta; +} + +function SectionCard({ title, children }: { title: string; children: React.ReactNode }) { + return ( + + {title} + {children} + + ); +} + +export default function CountryDetail() { + const theme = useTheme(); + const navigate = useNavigate(); + const { slug = "" } = useParams(); + const sidebar = useRegulationsTrackerSidebarContextSafe(); + + const { data, isLoading, isError } = useCountryDetail(slug); + const trackCountry = useTrackCountry(); + const untrackCountry = useUntrackCountry(); + const { showError, AlertSlot } = useTrackerAlert(); + + const { userRoleName, isSuperAdmin } = useAuth(); + // Tracking is available to admins and editors; re-running impact analysis is + // admin-only. A super-admin viewing an organization is read-only, so both + // actions are hidden for them (the backend blocks the writes regardless). + const canTrack = !isSuperAdmin && (userRoleName === "Admin" || userRoleName === "Editor"); + const canReanalyse = !isSuperAdmin && userRoleName === "Admin"; + + const { data: impactRes } = useImpactAnalysis(slug); + const refreshImpact = useRefreshImpactAnalysis(); + const impact = impactRes?.data ?? null; + + const country: CountryDetailData | null = data?.data ?? null; + + const handleToggleTrack = useCallback(() => { + if (!country) return; + const onDone = () => sidebar?.refreshTrackedCount(); + if (country.is_tracked) { + untrackCountry.mutate(country.slug, { + onSuccess: onDone, + onError: () => showError(`We couldn't untrack ${country.name}. Please try again.`), + }); + } else { + trackCountry.mutate(country.slug, { + onSuccess: onDone, + onError: () => showError(`We couldn't track ${country.name}. Please try again.`), + }); + } + }, [country, trackCountry, untrackCountry, sidebar, showError]); + + const breadcrumbItems = [ + { + label: "Regulations tracker", + path: "/regulations-tracker/browse", + icon: , + }, + { label: country?.name || "Country", path: "" }, + ]; + + if (isLoading) { + return ( + + + + ); + } + + if (isError || !country) { + return ( + + , + }, + ]} + autoGenerate={false} + testId="regulations-tracker-detail-breadcrumbs" + /> + + + navigate("/regulations-tracker/browse")} + sx={{ height: 34 }} + /> + + + ); + } + + const disclaimer = country.meta?.disclaimer || country.meta?.scopeStatement; + + return ( + + {AlertSlot} + + + + navigate("/regulations-tracker/browse")} + variant="text" + startIcon={} + sx={{ mb: "8px" }} + /> + + + {/* Header */} + + + {country.flag ? ( + + {country.flag} + + ) : ( + + )} + + + + + {country.name} + {country.stale && ( + + )} + + + {country.region && ( + + {country.region} + + )} + {country.iso2 && ( + + · {country.iso2} + + )} + + {country.meta?.last_updated && ( + + + + Last updated: {country.meta.last_updated} + + + )} + + + {canTrack && ( + + )} + + + {/* Stale data warning banner */} + {country.stale && ( + + + + This data may be outdated. The feed has not been refreshed recently for this country. + Information below reflects the last available snapshot. + + + )} + + + {/* Overview: narrative summary fields from the feed */} + {(country.oneLiner || country.executiveSummary || country.practicalTakeaway) && ( + + + {country.oneLiner && ( + + {country.oneLiner} + + )} + {country.executiveSummary && ( + + {country.executiveSummary} + + )} + {country.practicalTakeaway && ( + + + Practical takeaway + + + {country.practicalTakeaway} + + + )} + + + )} + + {/* Regulations list */} + {country.regulations && country.regulations.length > 0 && ( + + + {country.regulations.map((reg, i) => ( + + + + {reg.name} + + {reg.status && ( + + )} + + + + {reg.type && ( + + Type: {reg.type} + + )} + {reg.effectiveDate && ( + + Effective: {reg.effectiveDate} + {reg.dateConfidence && reg.dateConfidence !== "exact" + ? ` (${reg.dateConfidence})` + : ""} + + )} + {reg.lastVerified && ( + + Last verified: {reg.lastVerified} + + )} + + + {reg.scope && ( + + {reg.scope} + + )} + + {reg.obligations && reg.obligations.length > 0 && ( + + + Key obligations + + + {reg.obligations.map((ob, j) => ( + + {ob} + + ))} + + + )} + + {reg.maxPenalty && ( + + + Max penalty: + {" "} + {reg.maxPenalty} + + )} + + {reg.industryTags && reg.industryTags.length > 0 && ( + + {reg.industryTags.map((tag, j) => ( + + ))} + + )} + + {reg.sourceUrl && ( + + + View source + + + )} + + ))} + + + )} + + {/* Timeline */} + {country.timeline && country.timeline.length > 0 && ( + + {/* Relatively-positioned container so the absolute vertical rail + sits behind all event rows (z-index 0) while dots sit on top + (z-index 1). The rail spans the full height of this box; dots + have a solid card-background fill so the line appears to + start/end at the outermost dot centers rather than bleeding + past them. */} + + {/* Vertical connecting rail — 1px, aligned to dot center (10px + from left edge of the dot column: 4px left-padding of the + row + half of 12px dot = 10px). */} + + + )} + + {/* Change history */} + {country.change_history && country.change_history.length > 0 && ( + + + {country.change_history.map((entry, i) => ( + + + + {entry.date} + + {entry.type && } + + + {entry.summary} + + + ))} + + + )} + + {/* Feed disclaimer — rendered VERBATIM from the feed payload, never paraphrased */} + {disclaimer && ( + + + Disclaimer + + {/* Verbatim feed content — do NOT translate or paraphrase */} + + {disclaimer} + + + )} + + {/* Impact analysis panel: shown only when the API has run an analysis (status === "ok"). + Renders five groups (systems / controls / policies / vendors / assessments). + A stale banner with a Re-analyse action is shown when impact.stale is true. + If no analysis exists (no API key, org not set up) the panel is omitted entirely. */} + {impact?.status === "ok" && impact.result && ( + + {impact.stale && ( + + + + + This analysis predates the latest change. + + + {canReanalyse && ( + refreshImpact.mutate(slug)} + isDisabled={refreshImpact.isPending} + sx={{ height: 30, fontSize: "13px", flexShrink: 0, p: "0 8px" }} + /> + )} + + )} + {( + [ + ["systems", "AI systems"], + ["controls", "Controls to review"], + ["policies", "Policies that may be outdated"], + ["vendors", "Vendors impacted"], + ["assessments", "Assessments to update"], + ] as const + ).map(([key, label]) => { + const groups = impact.result as Record< + string, + { id: number; name: string; why: string }[] + >; + const entities = groups[key]; + if (!entities?.length) return null; + return ( + + + {entities.length} {label} + + + {entities.map((e) => ( + + + {e.name} + {" "} + — {e.why} + + ))} + + + ); + })} + + )} + + {/* Stale summary card: shown when data is stale AND no regulation/timeline detail is + available (i.e. the feed returned a manifest-only summary). Shows what IS known + (regulation count, last change history from the manifest) rather than a blank empty state. */} + {country.stale && + (!country.regulations || country.regulations.length === 0) && + (!country.timeline || country.timeline.length === 0) && + (!country.change_history || country.change_history.length === 0) && ( + + + {(country as any).regulationCount != null && ( + + Regulation count (at last snapshot):{" "} + {(country as any).regulationCount} + + )} + {(country as any).history?.lastChange?.changes?.length > 0 && ( + + + Last recorded changes + + + {(country as any).history.lastChange.changes.map( + (ch: Record, i: number) => ( + + • {String(ch.field ?? "")}: {String(ch.from ?? "")} →{" "} + {String(ch.to ?? ch.value ?? "")} + + ), + )} + + + )} + + Live regulation details are temporarily unavailable. The information above + reflects the last available snapshot from the feed. + + + + )} + + {/* Empty state when no regulations data yet and data is NOT stale (live fetch returned empty) */} + {!country.stale && + (!country.regulations || country.regulations.length === 0) && + (!country.timeline || country.timeline.length === 0) && + (!country.change_history || country.change_history.length === 0) && ( + + )} + + + ); +} diff --git a/Clients/src/presentation/pages/RegulationsTracker/CountryRowCard.tsx b/Clients/src/presentation/pages/RegulationsTracker/CountryRowCard.tsx new file mode 100644 index 0000000000..4478779e7b --- /dev/null +++ b/Clients/src/presentation/pages/RegulationsTracker/CountryRowCard.tsx @@ -0,0 +1,172 @@ +/** + * @fileoverview Shared row card for the Regulations Tracker country list. + * + * Used by both the Browse and Tracked tabs to render a consistent country row: + * globe icon, country name, region, and a configurable action button. + * Browse also renders a checkbox (via the optional `checkbox` prop). + * + * @module pages/RegulationsTracker/CountryRowCard + */ + +import React from "react"; +import { Box, Typography } from "@mui/material"; +import { Globe } from "lucide-react"; +import { CustomizableButton } from "../../components/button/customizable-button"; +import { palette } from "../../themes/palette"; + +/** Shared interface for a catalogue country row. */ +export interface CountryRow { + slug: string; + name: string; + region?: string; + iso2?: string; + /** Unicode flag emoji from the feed (e.g. "🇪🇺"); falls back to a globe icon. */ + flag?: string; + is_tracked?: boolean; + /** Number of regulations recorded for this country (from listTracked). */ + regulation_count?: number; + /** ISO timestamp of the last regulation change (from listTracked). */ + last_changed_at?: string | null; + /** ISO timestamp when the org started tracking this country (from listTracked). */ + created_at?: string | null; +} + +export interface CountryRowCardProps { + row: CountryRow; + /** Called when the row body (name/region area) is clicked. */ + onClick: () => void; + /** Label for the action button (e.g. "Track", "Untrack"). Omit to hide the action (read-only row). */ + actionLabel?: string; + /** Variant for the action button. */ + actionVariant?: "outlined" | "contained" | "text"; + /** Called when the action button is clicked. Required only when actionLabel is set. */ + onAction?: (e: React.MouseEvent) => void; + /** Whether the action button should be shown as disabled. */ + actionDisabled?: boolean; + /** + * Optional checkbox element rendered at the leading edge of the row. + * Browse uses this for the bulk-select checkbox; Tracked omits it. + */ + checkbox?: React.ReactNode; + /** + * When true, renders a secondary metadata line with regulation_count, + * last_changed_at, and created_at from the row. Used only by the Tracked + * page; Browse leaves this unset so no metadata line appears there. + */ + showMeta?: boolean; +} + +const DATE_FMT: Intl.DateTimeFormatOptions = { year: "numeric", month: "short", day: "numeric" }; + +function formatDate(iso: string): string | null { + const d = new Date(iso); + if (isNaN(d.getTime())) return null; + return d.toLocaleDateString(undefined, DATE_FMT); +} + +export function CountryRowCard({ + row, + onClick, + actionLabel, + actionVariant = "outlined", + onAction, + actionDisabled = false, + checkbox, + showMeta = false, +}: CountryRowCardProps) { + const metaParts: string[] = []; + if (showMeta) { + if (typeof row.regulation_count === "number") { + metaParts.push( + row.regulation_count === 1 ? "1 regulation" : `${row.regulation_count} regulations`, + ); + } + if (row.last_changed_at) { + const d = formatDate(row.last_changed_at); + if (d) metaParts.push(`Last changed ${d}`); + } + if (row.created_at) { + const d = formatDate(row.created_at); + if (d) metaParts.push(`Tracked since ${d}`); + } + } + return ( + + {checkbox && ( + + {checkbox} + + )} + + {row.flag ? ( + + {row.flag} + + ) : ( + + )} + + + + {row.name} + + {row.region && ( + + {row.region} + + )} + {showMeta && metaParts.length > 0 && ( + + {metaParts.join(" · ")} + + )} + + + {actionLabel && ( + { + e.stopPropagation(); + onAction?.(e); + }} + isDisabled={actionDisabled} + sx={{ flexShrink: 0 }} + /> + )} + + ); +} diff --git a/Clients/src/presentation/pages/RegulationsTracker/Deadlines/index.tsx b/Clients/src/presentation/pages/RegulationsTracker/Deadlines/index.tsx new file mode 100644 index 0000000000..231c69c8c6 --- /dev/null +++ b/Clients/src/presentation/pages/RegulationsTracker/Deadlines/index.tsx @@ -0,0 +1,525 @@ +/** + * @fileoverview Regulations Tracker — Deadlines tab. + * + * Forward-looking effective-date milestones for AI regulations, plus regulations + * whose effective date is not yet scheduled. Mirrored from the public feed. + * Read-only. + * + * @module pages/RegulationsTracker/Deadlines + */ + +import { useCallback, useEffect, useRef, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { Box, Stack, Typography, CircularProgress } from "@mui/material"; +import { AlertTriangle, CalendarClock } from "lucide-react"; +import { EmptyState } from "../../../components/EmptyState"; +import { PageHeaderExtended } from "../../../components/Layout/PageHeaderExtended"; +import Chip from "../../../components/Chip"; +import { VWLink } from "../../../components/Link"; +import VWTooltip from "../../../components/VWTooltip"; +import { palette } from "../../../themes/palette"; +import { useDeadlines } from "../../../../application/hooks/useRegulationsTracker"; +import { regulationStatusVariant } from "../statusVariant"; + +interface Deadline { + effectiveDateISO: string; + effectiveDateRaw?: string; + dateConfidence?: string; + countrySlug: string; + countryName: string; + countryFlag?: string; + regulationName: string; + status?: string; + type?: string; + sourceUrl?: string; +} + +interface Unscheduled { + countrySlug: string; + countryName: string; + countryFlag?: string; + regulationName: string; + effectiveDateRaw?: string; + status?: string; +} + +function Row({ + children, + id, + highlighted, +}: { + children: React.ReactNode; + id?: string; + highlighted?: boolean; +}) { + return ( + + {children} + + ); +} + +/** Build the 12-month window starting from the current month. + * Returns an array of "YYYY-MM" strings. + */ +function buildMonthWindow(): string[] { + const now = new Date(); + const months: string[] = []; + for (let i = 0; i < 12; i++) { + const d = new Date(now.getFullYear(), now.getMonth() + i, 1); + const yyyy = d.getFullYear(); + const mm = String(d.getMonth() + 1).padStart(2, "0"); + months.push(`${yyyy}-${mm}`); + } + return months; +} + +const SHORT_MONTH_NAMES = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", +]; + +function MonthLabel({ yearMonth, isFirst }: { yearMonth: string; isFirst: boolean }) { + const [yyyy, mm] = yearMonth.split("-"); + const monthIdx = parseInt(mm, 10) - 1; + const label = SHORT_MONTH_NAMES[monthIdx]; + const showYear = isFirst || monthIdx === 0; // first column OR January + + return ( + + + {label} + + {showYear && ( + + '{String(yyyy).slice(2)} + + )} + + ); +} + +interface RunwayProps { + deadlines: Deadline[]; + onMarkerClick: (id: string) => void; +} + +function RunwayCalendar({ deadlines, onMarkerClick }: RunwayProps) { + const months = buildMonthWindow(); + const windowSet = new Set(months); + + // Bucket deadlines by YYYY-MM using safe substring parse (avoids TZ drift) + const byMonth = new Map(); + for (const d of deadlines) { + if (!d.effectiveDateISO) continue; + const ym = d.effectiveDateISO.slice(0, 7); // "YYYY-MM" + if (!windowSet.has(ym)) continue; + if (!byMonth.has(ym)) byMonth.set(ym, []); + byMonth.get(ym)!.push(d); + } + + const hasAnyInWindow = byMonth.size > 0; + + // Build a map from deadline object → its index in the `deadlines` array so + // the runway markers use the exact same index as the Scheduled list rows + // (`deadline-${countrySlug}-${i}`). This is done once here rather than + // calling findIndex() per marker, which would return the FIRST match for + // duplicate entries and jump to the wrong row. + const markerIdxMap = new Map(deadlines.map((d, i) => [d, i])); + + const prefersReducedMotion = + typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches; + + const handleClick = useCallback( + (d: Deadline, idx: number) => { + const id = `deadline-${d.countrySlug}-${idx}`; + onMarkerClick(id); + const el = document.getElementById(id); + if (el) { + el.scrollIntoView({ + behavior: prefersReducedMotion ? "auto" : "smooth", + block: "center", + }); + } + }, + [onMarkerClick, prefersReducedMotion], + ); + + return ( + + Next 12 months + + Effective dates for the coming year. Months closer to today are highlighted. + + + {!hasAnyInWindow && ( + + No effective dates in the next 12 months. + + )} + + + + {months.map((ym, colIdx) => { + const monthsFromNow = colIdx; // 0 = current month + const isUrgent = monthsFromNow <= 2; + const colDeadlines = byMonth.get(ym) ?? []; + + return ( + + {/* Markers stack */} + + {colDeadlines.map((d, mIdx) => { + // markerIdxMap carries the real position of each deadline in the + // top-level `deadlines` array, built once before the grid render. + // Using it here avoids findIndex which returns the FIRST match and + // would jump to the wrong row when two deadlines share the same + // countrySlug + regulationName + effectiveDateISO triple. + const stableIdx = markerIdxMap.get(d) ?? mIdx; + const ariaLabel = `${d.regulationName} — ${d.effectiveDateRaw || ym} (${d.countryName})`; + + return ( + +
{d.effectiveDateRaw || d.effectiveDateISO}
+
+ {d.countryFlag ? `${d.countryFlag} ` : ""} + {d.countryName} +
+ + } + placement="top" + > + handleClick(d, stableIdx)} + aria-label={ariaLabel} + sx={{ + "background": "transparent", + "border": "none", + "cursor": "pointer", + "p": "0", + "display": "flex", + "alignItems": "center", + "justifyContent": "center", + "width": "22px", + "height": "22px", + "borderRadius": "50%", + "&:focus-visible": { + outline: `2px solid ${palette.brand.primary}`, + outlineOffset: "2px", + }, + }} + > + {d.countryFlag ? ( + + {d.countryFlag} + + ) : ( + + )} + +
+ ); + })} +
+ + {/* Month label at the bottom */} + +
+ ); + })} +
+
+
+ ); +} + +export default function Deadlines() { + const navigate = useNavigate(); + const { data, isLoading, isError } = useDeadlines(); + const deadlines: Deadline[] = Array.isArray(data?.data?.deadlines) ? data.data.deadlines : []; + const unscheduled: Unscheduled[] = Array.isArray(data?.data?.unscheduled) + ? data.data.unscheduled + : []; + const stale = data?.data?.stale === true; + const isEmpty = !isLoading && deadlines.length === 0 && unscheduled.length === 0; + + // Highlighted row state for click-to-jump + const [highlightedId, setHighlightedId] = useState(null); + const highlightTimer = useRef | null>(null); + + const handleMarkerClick = useCallback((id: string) => { + if (highlightTimer.current) clearTimeout(highlightTimer.current); + setHighlightedId(id); + highlightTimer.current = setTimeout(() => setHighlightedId(null), 1500); + }, []); + + useEffect(() => { + return () => { + if (highlightTimer.current) clearTimeout(highlightTimer.current); + }; + }, []); + + return ( + + {isLoading && ( + + + + )} + + {isError && ( + + )} + + {isEmpty && !isError && ( + + )} + + {!isLoading && !isError && !isEmpty && ( + + {stale && ( + + Showing the last known deadlines; live data is temporarily unavailable. + + )} + + {/* ── Next-12-months runway calendar ── */} + + + {deadlines.length > 0 && ( + + + Scheduled + + + {deadlines.map((d, i) => { + const rowId = `deadline-${d.countrySlug}-${i}`; + return ( + + + + {d.effectiveDateRaw || d.effectiveDateISO} + + {d.status && ( + + )} + navigate(`/regulations-tracker/${d.countrySlug}`)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + navigate(`/regulations-tracker/${d.countrySlug}`); + } + }} + title={`View ${d.countryName}`} + sx={{ + "fontSize": "12px", + "color": palette.text.tertiary, + "ml": "auto", + "display": "flex", + "alignItems": "center", + "gap": "4px", + "cursor": "pointer", + "&:hover": { + color: palette.brand.primary, + textDecoration: "underline", + }, + }} + > + {d.countryFlag && ( + + {d.countryFlag} + + )} + {d.countryName} + + + + {d.regulationName} + + {d.sourceUrl && ( + + + View source + + + )} + + ); + })} + + + )} + + {unscheduled.length > 0 && ( + + + Not yet scheduled + + + {unscheduled.map((u, i) => ( + + + + {u.effectiveDateRaw || "Date TBD"} + + {u.status && ( + + )} + navigate(`/regulations-tracker/${u.countrySlug}`)} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + navigate(`/regulations-tracker/${u.countrySlug}`); + } + }} + title={`View ${u.countryName}`} + sx={{ + "fontSize": "12px", + "color": palette.text.tertiary, + "ml": "auto", + "display": "flex", + "alignItems": "center", + "gap": "4px", + "cursor": "pointer", + "&:hover": { + color: palette.brand.primary, + textDecoration: "underline", + }, + }} + > + {u.countryFlag && ( + + {u.countryFlag} + + )} + {u.countryName} + + + + {u.regulationName} + + + ))} + + + )} + + )} + + ); +} diff --git a/Clients/src/presentation/pages/RegulationsTracker/Frameworks/index.tsx b/Clients/src/presentation/pages/RegulationsTracker/Frameworks/index.tsx new file mode 100644 index 0000000000..8411dcda1c --- /dev/null +++ b/Clients/src/presentation/pages/RegulationsTracker/Frameworks/index.tsx @@ -0,0 +1,207 @@ +/** + * @fileoverview Regulations Tracker — International frameworks tab. + * + * The cross-border AI governance frameworks (OECD, UNESCO, etc.) mirrored from + * the public feed. Read-only. + * + * @module pages/RegulationsTracker/Frameworks + */ + +import { Box, Stack, Typography, CircularProgress } from "@mui/material"; +import { AlertTriangle, Info, Landmark } from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import { EmptyState } from "../../../components/EmptyState"; +import { PageHeaderExtended } from "../../../components/Layout/PageHeaderExtended"; +import Chip from "../../../components/Chip"; +import { VWLink } from "../../../components/Link"; +import { palette } from "../../../themes/palette"; +import { useFrameworks } from "../../../../application/hooks/useRegulationsTracker"; +import { regulationStatusVariant } from "../statusVariant"; + +interface Framework { + name: string; + status?: string; + adoptedBy?: string; + whyItMatters?: string; + keyPrinciples?: string[]; + namedDocuments?: string[]; + sourceUrl?: string; +} + +export default function Frameworks() { + const { data, isLoading, isError } = useFrameworks(); + const navigate = useNavigate(); + const items: Framework[] = Array.isArray(data?.data?.items) ? data.data.items : []; + const stale = data?.data?.stale === true; + const isEmpty = !isLoading && items.length === 0; + + return ( + + {/* EU AI Act discoverability callout */} + + + + Looking for the EU AI Act, or another country's law? Those live under each country.{" "} + navigate("/regulations-tracker/browse")}> + Find them in Browse. + + + + + {isLoading && ( + + + + )} + + {isError && ( + + )} + + {isEmpty && !isError && ( + + )} + + {!isLoading && !isError && items.length > 0 && ( + + {stale && ( + + Showing the last known frameworks; live data is temporarily unavailable. + + )} + + {items.map((f, i) => ( + + + + {f.name} + + {f.status && ( + + )} + + {f.adoptedBy && ( + + Adopted by: {f.adoptedBy} + + )} + {f.whyItMatters && ( + + {f.whyItMatters} + + )} + {f.keyPrinciples && f.keyPrinciples.length > 0 && ( + + + Key principles + + + {f.keyPrinciples.map((p, j) => ( + + {p} + + ))} + + + )} + {f.namedDocuments && f.namedDocuments.length > 0 && ( + + {f.namedDocuments.map((doc, j) => ( + + {doc} + + ))} + + )} + {f.sourceUrl && ( + + + View source + + + )} + + ))} + + + )} + + ); +} diff --git a/Clients/src/presentation/pages/RegulationsTracker/Horizon/index.tsx b/Clients/src/presentation/pages/RegulationsTracker/Horizon/index.tsx new file mode 100644 index 0000000000..1f8525e382 --- /dev/null +++ b/Clients/src/presentation/pages/RegulationsTracker/Horizon/index.tsx @@ -0,0 +1,114 @@ +/** + * @fileoverview Regulations Tracker — Horizon (changelog) tab. + * + * A curated, dated changelog of AI-regulation changes across all countries, + * mirrored from the public feed. Read-only. + * + * @module pages/RegulationsTracker/Horizon + */ + +import { Box, Stack, Typography, CircularProgress } from "@mui/material"; +import { AlertTriangle, History } from "lucide-react"; +import { EmptyState } from "../../../components/EmptyState"; +import { PageHeaderExtended } from "../../../components/Layout/PageHeaderExtended"; +import Chip from "../../../components/Chip"; +import { palette } from "../../../themes/palette"; +import { useHorizon } from "../../../../application/hooks/useRegulationsTracker"; + +interface HorizonChange { + date: string; + countrySlug: string; + countryName: string; + countryFlag?: string; + type?: string; + description: string; + detail?: string | null; +} + +export default function Horizon() { + const { data, isLoading, isError } = useHorizon(); + const items: HorizonChange[] = Array.isArray(data?.data?.items) ? data.data.items : []; + const stale = data?.data?.stale === true; + const isEmpty = !isLoading && items.length === 0; + + return ( + + {isLoading && ( + + + + )} + + {isError && ( + + )} + + {isEmpty && !isError && ( + + )} + + {!isLoading && !isError && items.length > 0 && ( + + {stale && ( + + Showing the last known changelog; live data is temporarily unavailable. + + )} + {items.map((c, i) => ( + + + {c.countryFlag && ( + + {c.countryFlag} + + )} + {c.countryName} + {c.type && } + + {c.date} + + + + {c.description} + + {c.detail && ( + + {c.detail} + + )} + + ))} + + )} + + ); +} diff --git a/Clients/src/presentation/pages/RegulationsTracker/RegulationsTrackerSidebar.tsx b/Clients/src/presentation/pages/RegulationsTracker/RegulationsTrackerSidebar.tsx new file mode 100644 index 0000000000..aa02de6008 --- /dev/null +++ b/Clients/src/presentation/pages/RegulationsTracker/RegulationsTrackerSidebar.tsx @@ -0,0 +1,102 @@ +/** + * @fileoverview Regulations Tracker Sidebar Component + * + * Sidebar navigation for the Regulations Tracker module. + * Follows the SidebarShell pattern established by AITrustIndexSidebar. + * + * @module pages/RegulationsTracker/RegulationsTrackerSidebar + */ + +import { useCallback } from "react"; +import { Compass, Star, Settings, History, CalendarClock, Landmark } from "lucide-react"; +import SidebarShell, { SidebarMenuItem } from "../../components/Sidebar/SidebarShell"; +import { useUserGuideSidebarContext } from "../../components/UserGuide"; + +interface RegulationsTrackerSidebarProps { + activeTab: string; + onTabChange: (value: string) => void; + trackedCount?: number; + isAdmin?: boolean; +} + +export default function RegulationsTrackerSidebar({ + activeTab, + onTabChange, + trackedCount = 0, + isAdmin = false, +}: RegulationsTrackerSidebarProps) { + const { open: openUserGuide, openTab } = useUserGuideSidebarContext(); + const openReleaseNotes = useCallback(() => openTab("whats-new"), [openTab]); + + const flatItems: SidebarMenuItem[] = [ + { + id: "browse", + label: "Browse", + value: "browse", + icon: , + disabled: false, + }, + { + id: "tracked", + label: "Tracked", + value: "tracked", + icon: , + count: trackedCount, + disabled: false, + }, + { + id: "horizon", + label: "Activity", + value: "horizon", + icon: , + disabled: false, + }, + { + id: "deadlines", + label: "Deadlines", + value: "deadlines", + icon: , + disabled: false, + }, + { + id: "frameworks", + label: "Frameworks", + value: "frameworks", + icon: , + disabled: false, + }, + ...(isAdmin + ? [ + { + id: "settings", + label: "Settings", + value: "settings", + icon: , + disabled: false, + }, + ] + : []), + ]; + + const isItemActive = (item: SidebarMenuItem): boolean => { + return item.value === activeTab || item.id === activeTab; + }; + + const handleItemClick = (item: SidebarMenuItem) => { + if (item.value) { + onTabChange(item.value); + } + }; + + return ( + + ); +} diff --git a/Clients/src/presentation/pages/RegulationsTracker/Settings/index.tsx b/Clients/src/presentation/pages/RegulationsTracker/Settings/index.tsx new file mode 100644 index 0000000000..a38003ae72 --- /dev/null +++ b/Clients/src/presentation/pages/RegulationsTracker/Settings/index.tsx @@ -0,0 +1,638 @@ +/** + * @fileoverview Regulations Tracker — Settings tab. + * + * Admin-only configuration of who receives regulation-change notifications: + * an organization-user multi-select plus free-text email entry. Changes + * auto-save (debounced) via the settings mutation. + * + * @module pages/RegulationsTracker/Settings + */ + +import { useState, useEffect, useRef, useMemo } from "react"; +import { useNavigate } from "react-router-dom"; +import { Box, Stack, Typography, CircularProgress } from "@mui/material"; +import ChipInput from "../../../components/Inputs/ChipInput"; +import AutoCompleteField from "../../../components/Inputs/Autocomplete"; +import { Check, Lock, RefreshCw, X } from "lucide-react"; +import Toggle from "../../../components/Inputs/Toggle"; +import { EmptyState } from "../../../components/EmptyState"; +import { PageHeaderExtended } from "../../../components/Layout/PageHeaderExtended"; +import { CustomizableButton } from "../../../components/button/customizable-button"; +import { palette } from "../../../themes/palette"; +import { + useSettings, + useUpdateSettings, + useTriggerSync, +} from "../../../../application/hooks/useRegulationsTracker"; +import useUsers from "../../../../application/hooks/useUsers"; +import { useAuth } from "../../../../application/hooks/useAuth"; +import { useTrackerAlert } from "../useTrackerAlert"; + +interface UserOption { + id: number; + label: string; +} + +// ─── Sync progress state machine ──────────────────────────────────────────── + +const SYNC_STAGES = [ + "Retrieving the latest regulations feed", + "Validating the feed", + "Comparing against your tracked countries", + "Finishing up", +] as const; + +type StageIndex = 0 | 1 | 2 | 3; + +// Delay (ms) between advancing stages during the simulation. +// INVARIANT: STAGE_DELAYS.length === SYNC_STAGES.length - 1 +// (one inter-stage delay per transition; the final stage has no delay after it). +// If you add a stage to SYNC_STAGES, add a corresponding delay here or the +// progress bar will stall one step short of "Finishing up". +const STAGE_DELAYS: number[] = [800, 850, 900]; + +type SyncStatus = "idle" | "running" | "done" | "error"; + +interface SyncState { + status: SyncStatus; + currentStage: StageIndex; + resultMessage: string | null; +} + +const INITIAL_SYNC_STATE: SyncState = { + status: "idle", + currentStage: 0, + resultMessage: null, +}; + +// ─── Component ────────────────────────────────────────────────────────────── + +export default function Settings() { + const navigate = useNavigate(); + const { userRoleName, isSuperAdmin } = useAuth(); + // Editing settings requires an organization administrator. A super-admin viewing an + // organization has read-only access (the backend blocks all writes), so they see the + // read-only view rather than editable controls that would fail to save. + // Note: the `userRoleName === "SuperAdmin"` case is intentionally excluded — + // !isSuperAdmin already gates super-admins out, so only org Admins can edit. + const isAdmin = !isSuperAdmin && userRoleName === "Admin"; + + const { data: settingsData, isLoading: settingsLoading } = useSettings(); + const { users, loading: usersLoading } = useUsers(); + const updateSettings = useUpdateSettings(); + const triggerSync = useTriggerSync(); + const { showError, AlertSlot } = useTrackerAlert(); + + const [recipientUserIds, setRecipientUserIds] = useState([]); + const [recipientEmails, setRecipientEmails] = useState([]); + const [impactEnabled, setImpactEnabled] = useState(true); + const [justSaved, setJustSaved] = useState(false); + + // Sync progress state + const [syncState, setSyncState] = useState(INITIAL_SYNC_STATE); + + // Refs to coordinate simulation vs real mutation + const stageTimersRef = useRef[]>([]); + const mutationSettledRef = useRef<{ settled: boolean; result?: string; error?: boolean }>({ + settled: false, + }); + const simulationDoneRef = useRef(false); + + useEffect(() => { + if (!justSaved) return undefined; + const t = setTimeout(() => setJustSaved(false), 2000); + return () => clearTimeout(t); + }, [justSaved]); + + const hydratedRef = useRef(false); + const debounceRef = useRef | null>(null); + + // Hydrate local state from saved settings once they arrive. + // Guard setState behind value comparison to avoid echo-loop with auto-save. + useEffect(() => { + if (settingsData?.data) { + const nextUserIds: number[] = settingsData.data.recipient_user_ids ?? []; + const nextEmails: string[] = settingsData.data.recipient_emails ?? []; + setRecipientUserIds((prev) => + prev.length === nextUserIds.length && prev.every((v, i) => v === nextUserIds[i]) + ? prev + : nextUserIds, + ); + setRecipientEmails((prev) => + prev.length === nextEmails.length && prev.every((v, i) => v === nextEmails[i]) + ? prev + : nextEmails, + ); + if (settingsData.data.impact_enabled !== undefined) { + setImpactEnabled(settingsData.data.impact_enabled); + } + } + }, [settingsData]); + + // Debounced auto-save on any change (after initial hydration). + useEffect(() => { + if (!isAdmin) return; + if (!hydratedRef.current) { + hydratedRef.current = true; + return; + } + if (debounceRef.current) clearTimeout(debounceRef.current); + debounceRef.current = setTimeout(() => { + updateSettings.mutate( + { + recipient_user_ids: recipientUserIds, + recipient_emails: recipientEmails, + impact_enabled: impactEnabled, + }, + { + onSuccess: () => setJustSaved(true), + onError: () => showError("We couldn't save your recipient changes. Please try again."), + }, + ); + }, 600); + return () => { + if (debounceRef.current) clearTimeout(debounceRef.current); + }; + // updateSettings is stable from React Query; intentionally excluded. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [recipientUserIds, recipientEmails, impactEnabled, isAdmin]); + + const userOptions: UserOption[] = useMemo( + () => + (users ?? []).map((u) => ({ + id: u.id, + label: [u.name, u.surname].filter(Boolean).join(" ") || u.email || `User ${u.id}`, + })), + [users], + ); + + const selectedUsers = useMemo( + () => userOptions.filter((o) => recipientUserIds.includes(o.id)), + [userOptions, recipientUserIds], + ); + + // Clear all stage timers + const clearStageTimers = () => { + stageTimersRef.current.forEach(clearTimeout); + stageTimersRef.current = []; + }; + + // Resolve: fast-forward remaining stages to done, then show result + const resolveSimulation = (resultMsg: string) => { + clearStageTimers(); + setSyncState({ + status: "done", + currentStage: (SYNC_STAGES.length - 1) as StageIndex, + resultMessage: resultMsg, + }); + simulationDoneRef.current = true; + }; + + const handleCheckNow = () => { + if (syncState.status === "running") return; + + // Reset refs + mutationSettledRef.current = { settled: false }; + simulationDoneRef.current = false; + clearStageTimers(); + + // Start the display + setSyncState({ status: "running", currentStage: 0, resultMessage: null }); + + // ── Simulated stage advancement ───────────────────────────────── + // Stages 0→1→2→3, each on a timer. Stage 3 ("Finishing up") stays + // active (spinning) until the real mutation resolves. + let accumulatedDelay = 0; + STAGE_DELAYS.forEach((delay, i) => { + accumulatedDelay += delay; + const nextStage = (i + 1) as StageIndex; + const timer = setTimeout(() => { + setSyncState((prev) => { + if (prev.status !== "running") return prev; + // If the mutation already settled while we were simulating, + // and we just hit the last simulated stage, resolve immediately. + if (nextStage === ((SYNC_STAGES.length - 1) as StageIndex)) { + if (mutationSettledRef.current.settled) { + simulationDoneRef.current = true; + if (mutationSettledRef.current.error) { + return { + status: "error", + currentStage: nextStage, + resultMessage: null, + }; + } + return { + status: "done", + currentStage: nextStage, + resultMessage: mutationSettledRef.current.result ?? null, + }; + } + // Mutation not yet settled — stay on stage 3 spinner and wait + } + return { ...prev, currentStage: nextStage }; + }); + }, accumulatedDelay); + stageTimersRef.current.push(timer); + }); + + // ── Real mutation ─────────────────────────────────────────────── + triggerSync.mutate(undefined, { + onSuccess: (res) => { + const r = res?.data ?? {}; + const msg = r.skipped + ? `Already up to date (${r.skipped}).` + : `Done — ${r.changed ?? 0} changed, ${r.newlyRemoved ?? 0} removed${r.newlyAdded ? `, ${r.newlyAdded} new` : ""}.`; + + mutationSettledRef.current = { settled: true, result: msg }; + + // If the simulation already reached the last stage, resolve now. + // Otherwise let the stage timer detect the settled flag. + if (simulationDoneRef.current) { + resolveSimulation(msg); + } else { + setSyncState((prev) => { + if (prev.status !== "running") return prev; + if (prev.currentStage === ((SYNC_STAGES.length - 1) as StageIndex)) { + // We're already on the last spinner — resolve immediately + simulationDoneRef.current = true; + return { status: "done", currentStage: prev.currentStage, resultMessage: msg }; + } + // Still on an earlier stage — the stage timer will pick up the flag + return prev; + }); + } + }, + onError: () => { + mutationSettledRef.current = { settled: true, error: true }; + clearStageTimers(); + setSyncState((prev) => ({ + status: "error", + currentStage: prev.currentStage, + resultMessage: null, + })); + simulationDoneRef.current = true; + }, + }); + }; + + // Cleanup on unmount + useEffect(() => { + return () => clearStageTimers(); + }, []); + + const isSyncRunning = syncState.status === "running"; + const showProgressPanel = syncState.status !== "idle"; + + if (!isAdmin) { + return ( + + + + ); + } + + return ( + + {AlertSlot} + {settingsLoading ? ( + + + + ) : ( + + {/* Cadence note + check-for-updates */} + + + The regulations feed is checked automatically. Recipients are notified only when a + tracked country's regulations change materially — no manual checks needed. + {settingsData?.data?.last_run_at && ( + + {" "} + Last checked:{" "} + {new Date(settingsData.data.last_run_at).toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric", + })} + {typeof settingsData.data.last_run_status === "string" && + settingsData.data.last_run_status.startsWith("ok") + ? "." + : settingsData.data.last_run_status + ? ` (${settingsData.data.last_run_status}).` + : "."} + + )} + + + + } + isDisabled={isSyncRunning || triggerSync.isPending} + onClick={handleCheckNow} + /> + + + {/* ── Progress panel ── */} + {showProgressPanel && ( + + + {SYNC_STAGES.map((label, idx) => { + const stageIdx = idx as StageIndex; + const isDone = + syncState.status === "done" || + (syncState.status === "error" && stageIdx < syncState.currentStage) || + (syncState.status === "running" && stageIdx < syncState.currentStage); + const isActive = + syncState.status === "running" && stageIdx === syncState.currentStage; + const isUpcoming = + syncState.status === "running" && stageIdx > syncState.currentStage; + + return ( + + {/* Stage indicator */} + + {isDone && ( + + ); + })} + + + {/* Result / error line */} + {syncState.status === "done" && syncState.resultMessage && ( + + {syncState.resultMessage} + + )} + + {syncState.status === "error" && ( + + + )} + + {/* Dismiss / reset */} + {(syncState.status === "done" || syncState.status === "error") && ( + + setSyncState(INITIAL_SYNC_STATE)} + /> + + )} + + )} + + + option.label} + isOptionEqualToValue={(option, val) => option.id === val.id} + onChange={(_e, value) => setRecipientUserIds(value.map((v) => v.id))} + placeholder={usersLoading ? "Loading team members…" : "Select team members"} + /> + + + + {/* Impact analysis toggle */} + + + + Analyse how regulation changes affect my organisation + + + + Turn this on to see how each regulation change affects your organisation. When a + country you track updates its regulations, VerifyWise looks at your AI systems, + controls, policies, vendors, and assessments, then flags the ones that may be + impacted. You get a short summary in the change notification and on the country + page. + + + The analysis runs during the weekly check using your organisation's LLM key, + so each run uses LLM credits. It sends the updated regulation text and the names + and descriptions of your possibly-relevant entities to your LLM provider. + + + If you leave this off, you still get regulation-change notifications, without the + impact summary. + + + + setImpactEnabled(checked)} + /> + + + {/* LLM key status */} + {!settingsData?.data?.has_llm_key && ( + + Impact analysis needs an LLM key.{" "} + navigate("/settings/apikeys")} + sx={{ + color: palette.brand.primary, + textDecoration: "underline", + cursor: "pointer", + }} + > + Add an LLM key + {" "} + to turn this on. + + )} + {settingsData?.data?.has_llm_key && ( + + {settingsData.data.llm_key_provider + ? `Impact analysis will use your ${settingsData.data.llm_key_provider} key${settingsData.data.llm_key_model ? ` (${settingsData.data.llm_key_model})` : ""}.` + : "Impact analysis is active."}{" "} + navigate("/settings/apikeys")} + sx={{ + color: palette.brand.primary, + textDecoration: "underline", + cursor: "pointer", + }} + > + Manage keys + + + )} + + {/* Last impact run */} + + {settingsData?.data?.last_impact_run_at + ? "Impact analysis last ran: " + + new Date(settingsData.data.last_impact_run_at).toLocaleString() + : "Impact analysis has not run yet."} + + + + + Changes are saved automatically. + + {updateSettings.isPending && ( + + + + Saving… + + + )} + {!updateSettings.isPending && justSaved && ( + Saved + )} + + + )} + + ); +} diff --git a/Clients/src/presentation/pages/RegulationsTracker/Tracked/index.tsx b/Clients/src/presentation/pages/RegulationsTracker/Tracked/index.tsx new file mode 100644 index 0000000000..a580b4b610 --- /dev/null +++ b/Clients/src/presentation/pages/RegulationsTracker/Tracked/index.tsx @@ -0,0 +1,221 @@ +/** + * @fileoverview Regulations Tracker — Tracked tab. + * + * Lists countries the organization tracks and supports inline untracking. + * Client-side sorting and pagination since the tracked list loads in full. + * + * @module pages/RegulationsTracker/Tracked + */ + +import { useMemo, useState, useCallback, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { Box, Stack, TablePagination, CircularProgress } from "@mui/material"; +import { Star, Compass, Bell, AlertTriangle } from "lucide-react"; +import { CustomSelect } from "../../../components/CustomSelect"; +import { EmptyState } from "../../../components/EmptyState"; +import EmptyStateTip from "../../../components/EmptyState/EmptyStateTip"; +import { PageHeaderExtended } from "../../../components/Layout/PageHeaderExtended"; +import TablePaginationActions from "../../../components/TablePagination"; +import { palette } from "../../../themes/palette"; +import { useTracked, useUntrackCountry } from "../../../../application/hooks/useRegulationsTracker"; +import { useRegulationsTrackerSidebarContextSafe } from "../../../../application/contexts/RegulationsTrackerSidebar.context"; +import { useTrackerAlert } from "../useTrackerAlert"; +import { CountryRow, CountryRowCard } from "../CountryRowCard"; + +const ROWS_PER_PAGE_OPTIONS = [12, 24, 48]; + +function sortValue(row: CountryRow, key: string): string { + switch (key) { + case "name": + return (row.name || row.slug || "").toLowerCase(); + case "region": + return (row.region || "").toLowerCase(); + default: + return ""; + } +} + +export default function Tracked() { + const navigate = useNavigate(); + const sidebar = useRegulationsTrackerSidebarContextSafe(); + const { data, isLoading, isError } = useTracked(); + const untrackCountry = useUntrackCountry(); + const { showError, AlertSlot } = useTrackerAlert(); + + const [sortValueKey, setSortValueKey] = useState("name-asc"); + const [region, setRegion] = useState(""); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(12); + + const [sortBy, sortDir] = useMemo(() => { + const [by, dir] = sortValueKey.split("-"); + return [by, dir as "asc" | "desc"] as const; + }, [sortValueKey]); + + const sortOptions = useMemo( + () => [ + { value: "name-asc", label: "Name A–Z" }, + { value: "name-desc", label: "Name Z–A" }, + { value: "region-asc", label: "Region A–Z" }, + ], + [], + ); + + const rows: CountryRow[] = useMemo(() => { + const list = Array.isArray(data?.data) ? data.data : []; + return list; + }, [data]); + + // Region filter with per-region counts. + const regionOptions = useMemo(() => { + const counts: Record = {}; + for (const r of rows) { + if (r.region) counts[r.region] = (counts[r.region] ?? 0) + 1; + } + return [ + { value: "", label: `All regions (${rows.length})` }, + ...Object.keys(counts) + .sort() + .map((r) => ({ value: r, label: `${r} (${counts[r]})` })), + ]; + }, [rows]); + + const filteredRows = useMemo( + () => (region ? rows.filter((r) => r.region === region) : rows), + [rows, region], + ); + + const sortedRows = useMemo(() => { + const copy = [...filteredRows]; + copy.sort((a, b) => { + const av = sortValue(a, sortBy); + const bv = sortValue(b, sortBy); + const cmp = av.localeCompare(bv); + return sortDir === "asc" ? cmp : -cmp; + }); + return copy; + }, [filteredRows, sortBy, sortDir]); + + const pagedRows = useMemo( + () => sortedRows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage), + [sortedRows, page, rowsPerPage], + ); + + const handleUntrack = useCallback( + (row: CountryRow) => { + untrackCountry.mutate(row.slug, { + onSuccess: () => sidebar?.refreshTrackedCount(), + onError: () => showError(`We couldn't untrack ${row.name}. Please try again.`), + }); + }, + [untrackCountry, sidebar, showError], + ); + + // Clamp the page when the tracked list shrinks below the current page's range. + useEffect(() => { + const lastPage = Math.max(0, Math.ceil(sortedRows.length / rowsPerPage) - 1); + if (page > lastPage) setPage(lastPage); + }, [sortedRows.length, rowsPerPage, page]); + + const isEmpty = !isLoading && rows.length === 0; + + return ( + + {AlertSlot} + + {isLoading && ( + + + + )} + + {isError && ( + + )} + + {isEmpty && !isError && ( + + + + + )} + + {!isLoading && !isError && rows.length > 0 && ( + <> + + { + setRegion(String(v)); + setPage(0); + return true; + }} + options={regionOptions} + /> + { + setSortValueKey(String(v)); + setPage(0); + return true; + }} + options={sortOptions} + /> + + + + {pagedRows.map((row) => ( + navigate(`/regulations-tracker/${row.slug}`)} + actionLabel="Untrack" + onAction={() => handleUntrack(row)} + actionDisabled={untrackCountry.isPending && untrackCountry.variables === row.slug} + showMeta + /> + ))} + + + + setPage(p)} + rowsPerPage={rowsPerPage} + rowsPerPageOptions={ROWS_PER_PAGE_OPTIONS} + onRowsPerPageChange={(e) => { + setRowsPerPage(parseInt(e.target.value, 10)); + setPage(0); + }} + ActionsComponent={TablePaginationActions as any} + labelRowsPerPage="Per page" + sx={{ mt: "24px" }} + /> + + + )} + + ); +} diff --git a/Clients/src/presentation/pages/RegulationsTracker/index.tsx b/Clients/src/presentation/pages/RegulationsTracker/index.tsx new file mode 100644 index 0000000000..f3e07bc03c --- /dev/null +++ b/Clients/src/presentation/pages/RegulationsTracker/index.tsx @@ -0,0 +1,15 @@ +/** + * @fileoverview Regulations Tracker module entry. + * + * The module sidebar is mounted by the shared ContextSidebar (keyed on the + * active module), exactly like AI Trust Index — so there is no per-page shell. + * This entry simply redirects the bare /regulations-tracker path to the Browse tab. + * + * @module pages/RegulationsTracker + */ + +import { Navigate } from "react-router-dom"; + +export default function RegulationsTracker() { + return ; +} diff --git a/Clients/src/presentation/pages/RegulationsTracker/statusVariant.ts b/Clients/src/presentation/pages/RegulationsTracker/statusVariant.ts new file mode 100644 index 0000000000..dcdc0a9c7e --- /dev/null +++ b/Clients/src/presentation/pages/RegulationsTracker/statusVariant.ts @@ -0,0 +1,34 @@ +/** + * @fileoverview Shared helper — map a regulation/deadline status string to a + * Chip variant so the colour logic lives in one place. + * + * @module pages/RegulationsTracker/statusVariant + */ + +import type { ChipVariant } from "../../types/interfaces/i.chip"; + +/** + * Return the semantic Chip variant for a regulation or deadline status value. + * + * Known feed values: + * "in-force" → success (green — active & enforceable) + * "passed-not-active" → info (blue — passed, not yet live) + * "proposed" → warning (amber — pending) + * "policy-only" → default (gray — non-binding) + * "voluntary" → default (gray — non-binding) + * anything else → default + */ +export function regulationStatusVariant(status?: string): ChipVariant { + const s = (status ?? "").toLowerCase().trim(); + if (s.includes("in-force") || s.includes("in force") || s === "active" || s === "enacted") { + return "success"; + } + if (s.includes("passed")) { + return "info"; // passed-not-active + } + if (s.includes("proposed") || s.includes("draft")) { + return "warning"; + } + // policy-only, voluntary, unknown → default + return "default"; +} diff --git a/Clients/src/presentation/pages/RegulationsTracker/useTrackerAlert.tsx b/Clients/src/presentation/pages/RegulationsTracker/useTrackerAlert.tsx new file mode 100644 index 0000000000..abf478f507 --- /dev/null +++ b/Clients/src/presentation/pages/RegulationsTracker/useTrackerAlert.tsx @@ -0,0 +1,46 @@ +// Shared error-alert helper for the Regulations Tracker pages. +// +// Mirrors the useTrustIndexAlert pattern from the AI Trust Index module. +// +// Usage: +// const { showError, AlertSlot } = useTrackerAlert(); +// trackCountry.mutate(slug, { onError: () => showError("Couldn't track this country.") }); +// ... +// return (<>{AlertSlot}); +import { useCallback, useEffect, useState } from "react"; +import { Box } from "@mui/material"; +import Alert from "../../components/Alert"; +import { alertState } from "../../../domain/types/alert.types"; + +export function useTrackerAlert() { + const [alert, setAlert] = useState(null); + + // Auto-dismiss after 4s so a transient error toast does not linger. + useEffect(() => { + if (!alert) return undefined; + const timer = setTimeout(() => setAlert(null), 4000); + return () => clearTimeout(timer); + }, [alert]); + + const showError = useCallback((body: string, title = "Something went wrong") => { + setAlert({ variant: "error", title, body }); + }, []); + + const showSuccess = useCallback((body: string, title = "Done") => { + setAlert({ variant: "success", title, body }); + }, []); + + const AlertSlot = alert ? ( + + setAlert(null)} + /> + + ) : null; + + return { showError, showSuccess, AlertSlot }; +} diff --git a/Clients/src/presentation/types/interfaces/i.link.ts b/Clients/src/presentation/types/interfaces/i.link.ts index 223b16889a..4932fcaccf 100644 --- a/Clients/src/presentation/types/interfaces/i.link.ts +++ b/Clients/src/presentation/types/interfaces/i.link.ts @@ -15,6 +15,8 @@ export interface IVWLinkCoreProps { showUnderline?: boolean; /** If true, shows icon on hover for URL links (default: true) */ showIcon?: boolean; + /** If true, the external-link icon is always visible (not only on hover) for URL links (default: false) */ + alwaysShowIcon?: boolean; /** Custom class name */ className?: string; /** ARIA label for accessibility */ diff --git a/Servers/app.ts b/Servers/app.ts index 1c061d4589..2dc629dfed 100644 --- a/Servers/app.ts +++ b/Servers/app.ts @@ -109,6 +109,7 @@ import { sequelize } from "./database/db"; import redisClient from "./database/redis"; import ssoConfigRoutes from "./routes/ssoConfig.route"; import aiTrustIndexRoutes from "./routes/aiTrustIndex.route"; +import regulationsTrackerRoutes from "./routes/regulationsTracker.route"; const swaggerDoc = YAML.load("./swagger.yaml"); @@ -315,6 +316,7 @@ export function createApp(preRoutesMiddleware?: RequestHandler[]): express.Appli app.use("/v1", virtualKeyProxyRoutes()); app.use("/api/ssoConfig", ssoConfigRoutes); app.use("/api/ai-trust-index", aiTrustIndexRoutes); + app.use("/api/regulations-tracker", regulationsTrackerRoutes); return app; } diff --git a/Servers/controllers/__tests__/regulationImpact.ctrl.test.ts b/Servers/controllers/__tests__/regulationImpact.ctrl.test.ts new file mode 100644 index 0000000000..26afb23376 --- /dev/null +++ b/Servers/controllers/__tests__/regulationImpact.ctrl.test.ts @@ -0,0 +1,134 @@ +jest.mock("../../utils/regulationImpact.utils", () => ({ + getImpactRow: jest.fn(), + runImpactAnalysis: jest.fn(), +})); +jest.mock("../../utils/regulationsTracker.utils", () => ({ + getCountryRow: jest.fn(), + getSettings: jest.fn().mockResolvedValue({ impact_enabled: true }), + normalizeSlug: (s: string) => String(s).trim().toLowerCase(), +})); +jest.mock("../../utils/logger/logHelper", () => ({ + logProcessing: jest.fn(), + logSuccess: jest.fn(), + logFailure: jest.fn(), +})); +import { getImpactRow, runImpactAnalysis } from "../../utils/regulationImpact.utils"; +import { getCountryRow, getSettings } from "../../utils/regulationsTracker.utils"; +import { getImpactAnalysis, refreshImpactAnalysis } from "../regulationsTracker.ctrl"; + +function mockRes() { + const res: any = {}; + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + return res; +} +beforeEach(() => jest.clearAllMocks()); + +describe("getImpactAnalysis", () => { + const storedRow = { + regulation_hash: "h1", + status: "ok", + result: { + systems: [], + controls: [], + policies: [], + vendors: [], + assessments: [], + generatedAt: "x", + }, + refreshed_at: "t", + }; + + it("returns stale: true when catalog hash differs from stored hash", async () => { + (getSettings as jest.Mock).mockResolvedValue({ impact_enabled: true }); + (getImpactRow as jest.Mock).mockResolvedValue(storedRow); + // Different hash → stale + (getCountryRow as jest.Mock).mockResolvedValue({ hash: "h2" }); + const req: any = { userId: 1, organizationId: 7, params: { slug: "eu" } }; + const res = mockRes(); + await getImpactAnalysis(req, res); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ stale: true }), + }), + ); + }); + + it("returns stale: false when catalog hash matches stored hash", async () => { + (getSettings as jest.Mock).mockResolvedValue({ impact_enabled: true }); + (getImpactRow as jest.Mock).mockResolvedValue(storedRow); + // Same hash → not stale + (getCountryRow as jest.Mock).mockResolvedValue({ hash: "h1" }); + const req: any = { userId: 1, organizationId: 7, params: { slug: "eu" } }; + const res = mockRes(); + await getImpactAnalysis(req, res); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ stale: false }), + }), + ); + }); + + it("returns 200 with null when there is no analysis row", async () => { + (getSettings as jest.Mock).mockResolvedValue({ impact_enabled: true }); + (getImpactRow as jest.Mock).mockResolvedValue(null); + const req: any = { userId: 1, organizationId: 7, params: { slug: "eu" } }; + const res = mockRes(); + await getImpactAnalysis(req, res); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ data: null })); + }); + + // BUG 4: impact_enabled=false → return null even when a row exists + it("returns 200/null when impact_enabled is false, even though a row exists", async () => { + (getSettings as jest.Mock).mockResolvedValue({ impact_enabled: false }); + const req: any = { userId: 1, organizationId: 7, params: { slug: "eu" } }; + const res = mockRes(); + await getImpactAnalysis(req, res); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ data: null })); + // getImpactRow must NOT have been called when impact is disabled + expect(getImpactRow).not.toHaveBeenCalled(); + }); +}); + +describe("refreshImpactAnalysis", () => { + it("403s for non-admins", async () => { + const req: any = { userId: 1, organizationId: 7, role: "Editor", params: { slug: "eu" } }; + const res = mockRes(); + await refreshImpactAnalysis(req, res); + expect(res.status).toHaveBeenCalledWith(403); + expect(runImpactAnalysis).not.toHaveBeenCalled(); + }); + + // BUG 2: refreshImpactAnalysis must pass force=true so admin re-analysis is never a no-op + it("runs analysis for admins with force=true and returns 200", async () => { + (getSettings as jest.Mock).mockResolvedValue({ impact_enabled: true }); + (runImpactAnalysis as jest.Mock).mockResolvedValue({ + status: "ok", + result: null, + counts: {}, + cached: false, + }); + const req: any = { userId: 1, organizationId: 7, role: "Admin", params: { slug: "eu" } }; + const res = mockRes(); + await refreshImpactAnalysis(req, res); + // BUG 2 fix: force=true must be the third argument + expect(runImpactAnalysis).toHaveBeenCalledWith(7, "eu", true); + expect(res.status).toHaveBeenCalledWith(200); + }); + + it("returns 200 {status: disabled} and does NOT call runImpactAnalysis when impact_enabled is false", async () => { + (getSettings as jest.Mock).mockResolvedValue({ impact_enabled: false }); + const req: any = { userId: 1, organizationId: 7, role: "Admin", params: { slug: "eu" } }; + const res = mockRes(); + await refreshImpactAnalysis(req, res); + expect(runImpactAnalysis).not.toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ data: { status: "disabled" } }), + ); + }); +}); diff --git a/Servers/controllers/__tests__/regulationsTracker.ctrl.test.ts b/Servers/controllers/__tests__/regulationsTracker.ctrl.test.ts new file mode 100644 index 0000000000..73f278a149 --- /dev/null +++ b/Servers/controllers/__tests__/regulationsTracker.ctrl.test.ts @@ -0,0 +1,688 @@ +// jest.mock calls must precede all imports (hoisted by Jest). +jest.mock("../../utils/regulationsTracker.utils", () => ({ + listCountries: jest + .fn() + .mockResolvedValue([ + { slug: "eu", name: "European Union", region: "Europe", is_tracked: false }, + ]), + getCountryRow: jest.fn().mockResolvedValue({ + slug: "eu", + data: { name: "European Union", slug: "eu" }, + is_tracked: false, + hash: "h1", + }), + listTracked: jest.fn().mockResolvedValue([{ country_slug: "eu", name: "European Union" }]), + trackCountry: jest.fn().mockResolvedValue({ tracked: true }), + trackCountriesBulk: jest.fn().mockResolvedValue({ tracked: 2 }), + untrackCountry: jest.fn().mockResolvedValue({ untracked: true }), + getSettings: jest.fn().mockResolvedValue({ + recipient_user_ids: [], + recipient_emails: [], + updated_by: null, + updated_at: null, + impact_enabled: true, + last_impact_run_at: null, + }), + upsertSettings: jest.fn().mockResolvedValue({ + recipient_user_ids: [1], + recipient_emails: ["dpo@acme.com"], + updated_by: 1, + updated_at: new Date(), + impact_enabled: true, + last_impact_run_at: null, + }), + getMetaQuery: jest.fn().mockResolvedValue({ + seeded_at: new Date(), + last_good_count: 60, + last_run_week: "2026-W26", + last_run_at: new Date(), + last_run_status: "ok: 0 changed, 0 removed", + }), + getGlobalFeed: jest.fn().mockResolvedValue(null), + setGlobalFeeds: jest.fn().mockResolvedValue(undefined), + setLastImpactRunAt: jest.fn().mockResolvedValue(undefined), + // BUG 3: normalizeSlug exported — keep real behaviour in tests + normalizeSlug: (s: string) => String(s).trim().toLowerCase(), +})); + +jest.mock("../../utils/llmKey.utils", () => ({ + getLLMKeysWithKeyQuery: jest.fn().mockResolvedValue([]), +})); + +jest.mock("../../utils/regulationImpact.utils", () => ({ + getImpactRow: jest.fn().mockResolvedValue({ + regulation_hash: "h1", + status: "ok", + result: { + systems: [], + controls: [], + policies: [], + vendors: [], + assessments: [], + generatedAt: "x", + }, + refreshed_at: "2026-06-27T00:00:00.000Z", + }), + runImpactAnalysis: jest.fn().mockResolvedValue({ + status: "ok", + result: { + systems: [], + controls: [], + policies: [], + vendors: [], + assessments: [], + generatedAt: "x", + }, + counts: { system: 0, control: 0, policy: 0, vendor: 0, assessment: 0 }, + cached: false, + }), +})); + +jest.mock("../../utils/regulationsTrackerFeed", () => ({ + // The real feed nests detail under `country` with `meta` alongside. + fetchCountryDetail: jest.fn().mockResolvedValue({ + country: { slug: "eu", name: "European Union", regulations: [{ name: "EU AI Act" }] }, + meta: { disclaimer: "info only" }, + }), + fetchHorizon: jest.fn().mockResolvedValue({ changes: [] }), + fetchDeadlines: jest.fn().mockResolvedValue({ deadlines: [], unscheduled: [] }), + fetchSnapshot: jest.fn().mockResolvedValue({ frameworks: [] }), +})); + +jest.mock("../../utils/logger/logHelper", () => ({ + logProcessing: jest.fn(), + logSuccess: jest.fn(), + logFailure: jest.fn(), +})); + +import { + getCountries, + getCountryDetail, + getTracked, + trackCountryCtrl, + trackBulkCtrl, + untrackCountryCtrl, + getSettingsCtrl, + updateSettingsCtrl, + triggerSync, + getImpactAnalysis, + refreshImpactAnalysis, +} from "../regulationsTracker.ctrl"; +import { + listCountries, + listTracked, + trackCountry, + trackCountriesBulk, + untrackCountry, + getCountryRow, + upsertSettings, + getSettings, + getMetaQuery, + getGlobalFeed, +} from "../../utils/regulationsTracker.utils"; +import { getLLMKeysWithKeyQuery } from "../../utils/llmKey.utils"; +import { getImpactRow, runImpactAnalysis } from "../../utils/regulationImpact.utils"; + +// --------------------------------------------------------------------------- +// Global mock reset — prevents call counts from accumulating across tests +// --------------------------------------------------------------------------- +beforeEach(() => { + jest.clearAllMocks(); +}); + +// --------------------------------------------------------------------------- +// Helper: minimal mock Response (mirrors aiTrustIndex.ctrl.test.ts pattern) +// --------------------------------------------------------------------------- +function mockRes() { + const res: any = {}; + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + return res; +} + +// --------------------------------------------------------------------------- +// GET /api/regulations-tracker/countries +// --------------------------------------------------------------------------- +describe("getCountries", () => { + it("returns 200 with the array from the mocked util", async () => { + const req: any = { userId: 1, organizationId: 7, query: {} }; + const res = mockRes(); + await getCountries(req, res); + expect(res.status).toHaveBeenCalledWith(200); + expect(listCountries).toHaveBeenCalled(); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + message: "OK", + data: expect.arrayContaining([expect.objectContaining({ slug: "eu" })]), + }), + ); + }); + + it("passes region and q query params to the util", async () => { + const req: any = { userId: 1, organizationId: 7, query: { region: "Europe", q: "gdpr" } }; + const res = mockRes(); + await getCountries(req, res); + expect(listCountries).toHaveBeenCalledWith(7, { region: "Europe", q: "gdpr" }); + }); +}); + +// --------------------------------------------------------------------------- +// GET /api/regulations-tracker/countries/:slug +// --------------------------------------------------------------------------- +describe("getCountryDetail", () => { + it("returns 200 with flattened live data (not stale) when fetchCountryDetail succeeds", async () => { + const req: any = { userId: 1, organizationId: 7, params: { slug: "eu" } }; + const res = mockRes(); + await getCountryDetail(req, res); + expect(res.status).toHaveBeenCalledWith(200); + expect(getCountryRow).toHaveBeenCalledWith("eu", 7); + const payload = (res.json as jest.Mock).mock.calls[0][0]; + // Live country is flattened to the root (regulations/name/meta) and stale:false. + expect(payload.data).toEqual(expect.objectContaining({ name: "European Union", stale: false })); + expect(payload.data.regulations).toEqual([{ name: "EU AI Act" }]); + }); + + it("falls back to stale stored data when the live feed returns an empty country payload", async () => { + const { fetchCountryDetail } = require("../../utils/regulationsTrackerFeed"); + fetchCountryDetail.mockResolvedValueOnce({ country: {}, meta: null }); + const req: any = { userId: 1, organizationId: 7, params: { slug: "eu" } }; + const res = mockRes(); + await getCountryDetail(req, res); + expect(res.status).toHaveBeenCalledWith(200); + const payload = (res.json as jest.Mock).mock.calls[0][0]; + expect(payload.data.stale).toBe(true); + }); + + it("returns 404 when the country slug is unknown", async () => { + (getCountryRow as jest.Mock).mockResolvedValueOnce(null); + const req: any = { userId: 1, organizationId: 7, params: { slug: "unknown-slug" } }; + const res = mockRes(); + await getCountryDetail(req, res); + expect(res.status).toHaveBeenCalledWith(404); + }); + + it("falls back to stale local data when fetchCountryDetail throws", async () => { + const { fetchCountryDetail } = require("../../utils/regulationsTrackerFeed"); + fetchCountryDetail.mockRejectedValueOnce(new Error("network error")); + const req: any = { userId: 1, organizationId: 7, params: { slug: "eu" } }; + const res = mockRes(); + await getCountryDetail(req, res); + // Controller catches the inner error and still returns 200 with stale data + expect(res.status).toHaveBeenCalledWith(200); + const payload = (res.json as jest.Mock).mock.calls[0][0]; + expect(payload.data.stale).toBe(true); + }); +}); + +// --------------------------------------------------------------------------- +// GET /api/regulations-tracker/tracked +// --------------------------------------------------------------------------- +describe("getTracked", () => { + it("returns 200 with tracked list", async () => { + const req: any = { userId: 1, organizationId: 7 }; + const res = mockRes(); + await getTracked(req, res); + expect(res.status).toHaveBeenCalledWith(200); + }); + + it("tenant isolation: passes organizationId to listTracked", async () => { + const req: any = { userId: 1, organizationId: 42 }; + const res = mockRes(); + await getTracked(req, res); + expect(listTracked).toHaveBeenCalledWith(42); + }); + + it("org A cannot see org B's tracked list", async () => { + (listTracked as jest.Mock) + .mockResolvedValueOnce([{ country_slug: "eu" }]) // org A + .mockResolvedValueOnce([{ country_slug: "us" }]); // org B + + const reqA: any = { userId: 1, organizationId: 1 }; + const reqB: any = { userId: 2, organizationId: 2 }; + const resA = mockRes(); + const resB = mockRes(); + + await getTracked(reqA, resA); + await getTracked(reqB, resB); + + // Verify each call was scoped to its own org + expect((listTracked as jest.Mock).mock.calls[0][0]).toBe(1); + expect((listTracked as jest.Mock).mock.calls[1][0]).toBe(2); + }); +}); + +// --------------------------------------------------------------------------- +// POST /api/regulations-tracker/tracked [ADMIN] +// --------------------------------------------------------------------------- +describe("trackCountryCtrl", () => { + it("returns 200 for an Editor (tracking is allowed for admins and editors)", async () => { + const req: any = { role: "Editor", userId: 1, organizationId: 7, body: { slug: "eu" } }; + const res = mockRes(); + await trackCountryCtrl(req, res); + expect(res.status).toHaveBeenCalledWith(200); + expect(trackCountry).toHaveBeenCalled(); + }); + + it("returns 403 for an Auditor role", async () => { + const req: any = { role: "Auditor", userId: 1, organizationId: 7, body: { slug: "eu" } }; + const res = mockRes(); + await trackCountryCtrl(req, res); + expect(res.status).toHaveBeenCalledWith(403); + expect(trackCountry).not.toHaveBeenCalled(); + }); + + it("returns 403 for a Reviewer role", async () => { + const req: any = { role: "Reviewer", userId: 1, organizationId: 7, body: { slug: "eu" } }; + const res = mockRes(); + await trackCountryCtrl(req, res); + expect(res.status).toHaveBeenCalledWith(403); + expect(trackCountry).not.toHaveBeenCalled(); + }); + + it("returns 400 when slug is missing", async () => { + const req: any = { role: "Admin", userId: 1, organizationId: 7, body: {} }; + const res = mockRes(); + await trackCountryCtrl(req, res); + expect(res.status).toHaveBeenCalledWith(400); + expect(trackCountry).not.toHaveBeenCalled(); + }); + + it("returns 200 for an Admin with a valid slug", async () => { + const req: any = { role: "Admin", userId: 1, organizationId: 7, body: { slug: "eu" } }; + const res = mockRes(); + await trackCountryCtrl(req, res); + expect(res.status).toHaveBeenCalledWith(200); + }); + + it("returns 200 for a SuperAdmin with a valid slug", async () => { + const req: any = { role: "SuperAdmin", userId: 2, organizationId: 7, body: { slug: "us" } }; + const res = mockRes(); + await trackCountryCtrl(req, res); + expect(res.status).toHaveBeenCalledWith(200); + }); + + it("tenant isolation: passes organizationId to trackCountry", async () => { + const req: any = { role: "Admin", userId: 1, organizationId: 99, body: { slug: "eu" } }; + const res = mockRes(); + await trackCountryCtrl(req, res); + expect(trackCountry).toHaveBeenCalledWith(99, "eu", 1); + }); +}); + +// --------------------------------------------------------------------------- +// POST /api/regulations-tracker/tracked/bulk [ADMIN] +// --------------------------------------------------------------------------- +describe("trackBulkCtrl", () => { + it("returns 403 for a Reviewer role", async () => { + const req: any = { + role: "Reviewer", + userId: 1, + organizationId: 7, + body: { slugs: ["eu", "us"] }, + }; + const res = mockRes(); + await trackBulkCtrl(req, res); + expect(res.status).toHaveBeenCalledWith(403); + expect(trackCountriesBulk).not.toHaveBeenCalled(); + }); + + it("returns 200 for an Editor (tracking is allowed for admins and editors)", async () => { + const req: any = { + role: "Editor", + userId: 1, + organizationId: 7, + body: { slugs: ["eu", "us"] }, + }; + const res = mockRes(); + await trackBulkCtrl(req, res); + expect(res.status).toHaveBeenCalledWith(200); + expect(trackCountriesBulk).toHaveBeenCalled(); + }); + + it("returns 400 when slugs is not an array", async () => { + const req: any = { role: "Admin", userId: 1, organizationId: 7, body: { slugs: "eu" } }; + const res = mockRes(); + await trackBulkCtrl(req, res); + expect(res.status).toHaveBeenCalledWith(400); + }); + + it("returns 400 when slugs array contains a non-string entry", async () => { + const req: any = { role: "Admin", userId: 1, organizationId: 7, body: { slugs: ["eu", 123] } }; + const res = mockRes(); + await trackBulkCtrl(req, res); + expect(res.status).toHaveBeenCalledWith(400); + }); + + it("returns 200 for an Admin with a valid slugs array", async () => { + const req: any = { role: "Admin", userId: 1, organizationId: 7, body: { slugs: ["eu", "us"] } }; + const res = mockRes(); + await trackBulkCtrl(req, res); + expect(res.status).toHaveBeenCalledWith(200); + expect(trackCountriesBulk).toHaveBeenCalledWith(7, ["eu", "us"], 1); + }); +}); + +// --------------------------------------------------------------------------- +// DELETE /api/regulations-tracker/tracked/:slug [ADMIN] +// --------------------------------------------------------------------------- +describe("untrackCountryCtrl", () => { + it("returns 403 for an Auditor role", async () => { + const req: any = { role: "Auditor", userId: 1, organizationId: 7, params: { slug: "eu" } }; + const res = mockRes(); + await untrackCountryCtrl(req, res); + expect(res.status).toHaveBeenCalledWith(403); + expect(untrackCountry).not.toHaveBeenCalled(); + }); + + it("returns 200 for an Editor (tracking is allowed for admins and editors)", async () => { + const req: any = { role: "Editor", userId: 1, organizationId: 7, params: { slug: "eu" } }; + const res = mockRes(); + await untrackCountryCtrl(req, res); + expect(res.status).toHaveBeenCalledWith(200); + expect(untrackCountry).toHaveBeenCalled(); + }); + + it("is idempotent: returns 200 even when the country was never tracked", async () => { + // untrackCountry is a DELETE that resolves regardless (no-op if not tracked) + (untrackCountry as jest.Mock).mockResolvedValueOnce({ untracked: true }); + const req: any = { + role: "Admin", + userId: 1, + organizationId: 7, + params: { slug: "never-tracked" }, + }; + const res = mockRes(); + await untrackCountryCtrl(req, res); + expect(res.status).toHaveBeenCalledWith(200); + }); + + it("returns 200 and echoes the slug on success", async () => { + const req: any = { role: "Admin", userId: 1, organizationId: 7, params: { slug: "eu" } }; + const res = mockRes(); + await untrackCountryCtrl(req, res); + expect(res.status).toHaveBeenCalledWith(200); + expect(untrackCountry).toHaveBeenCalledWith(7, "eu"); + }); +}); + +// --------------------------------------------------------------------------- +// GET /api/regulations-tracker/settings +// --------------------------------------------------------------------------- +describe("getSettingsCtrl", () => { + it("returns 200 with settings for any authenticated user", async () => { + const req: any = { userId: 1, organizationId: 7 }; + const res = mockRes(); + await getSettingsCtrl(req, res); + expect(res.status).toHaveBeenCalledWith(200); + }); + + it("merges global run status (last_run_at + last_run_status) into the settings payload", async () => { + const req: any = { userId: 1, organizationId: 7 }; + const res = mockRes(); + await getSettingsCtrl(req, res); + const payload = (res.json as jest.Mock).mock.calls[0][0]; + expect(payload.data).toEqual( + expect.objectContaining({ last_run_status: "ok: 0 changed, 0 removed" }), + ); + expect(payload.data.last_run_at).toBeTruthy(); + }); + + it("surfaces feed_last_data_update from the stored horizon blob meta", async () => { + (getGlobalFeed as jest.Mock).mockResolvedValueOnce({ + meta: { lastDataUpdate: "2026-06-27" }, + }); + const req: any = { userId: 1, organizationId: 7 }; + const res = mockRes(); + await getSettingsCtrl(req, res); + const payload = (res.json as jest.Mock).mock.calls[0][0]; + expect(payload.data.feed_last_data_update).toBe("2026-06-27"); + }); + + it("returns feed_last_data_update = null when no horizon blob is stored", async () => { + (getGlobalFeed as jest.Mock).mockResolvedValueOnce(null); + const req: any = { userId: 1, organizationId: 7 }; + const res = mockRes(); + await getSettingsCtrl(req, res); + const payload = (res.json as jest.Mock).mock.calls[0][0]; + expect(payload.data.feed_last_data_update).toBeNull(); + }); +}); + +// --------------------------------------------------------------------------- +// PUT /api/regulations-tracker/settings [ADMIN] +// --------------------------------------------------------------------------- +describe("updateSettingsCtrl", () => { + it("returns 403 for a non-admin role and does not write", async () => { + const req: any = { + role: "Editor", + userId: 1, + organizationId: 7, + body: { recipient_user_ids: [], recipient_emails: [] }, + }; + const res = mockRes(); + await updateSettingsCtrl(req, res); + expect(res.status).toHaveBeenCalledWith(403); + expect(upsertSettings).not.toHaveBeenCalled(); + }); + + it("returns 400 when recipient_emails contains a malformed email", async () => { + const req: any = { + role: "Admin", + userId: 1, + organizationId: 7, + body: { recipient_user_ids: [], recipient_emails: ["not-an-email"] }, + }; + const res = mockRes(); + await updateSettingsCtrl(req, res); + expect(res.status).toHaveBeenCalledWith(400); + expect(upsertSettings).not.toHaveBeenCalled(); + }); + + it("returns 400 when recipient_user_ids contains a non-integer", async () => { + const req: any = { + role: "Admin", + userId: 1, + organizationId: 7, + body: { recipient_user_ids: ["abc"], recipient_emails: [] }, + }; + const res = mockRes(); + await updateSettingsCtrl(req, res); + expect(res.status).toHaveBeenCalledWith(400); + expect(upsertSettings).not.toHaveBeenCalled(); + }); + + it("returns 200 for an Admin with valid arrays", async () => { + const req: any = { + role: "Admin", + userId: 1, + organizationId: 7, + body: { recipient_user_ids: [2], recipient_emails: ["dpo@acme.com"] }, + }; + const res = mockRes(); + await updateSettingsCtrl(req, res); + expect(res.status).toHaveBeenCalledWith(200); + expect(upsertSettings).toHaveBeenCalledWith(7, [2], ["dpo@acme.com"], 1, undefined); + }); +}); + +// --------------------------------------------------------------------------- +// POST /api/regulations-tracker/sync [ADMIN] +// --------------------------------------------------------------------------- +describe("triggerSync", () => { + it("returns 403 for a non-admin (gate fires before any sync work)", async () => { + const req: any = { userId: 1, organizationId: 7, role: "Editor" }; + const res = mockRes(); + await triggerSync(req, res); + expect(res.status).toHaveBeenCalledWith(403); + }); +}); + +// --------------------------------------------------------------------------- +// GET /api/regulations-tracker/settings — impact fields + has_llm_key +// --------------------------------------------------------------------------- +describe("getSettingsCtrl with impact fields", () => { + it("includes has_llm_key=true when the org has a key", async () => { + (getSettings as jest.Mock).mockResolvedValue({ + recipient_user_ids: [], + recipient_emails: [], + updated_by: null, + updated_at: null, + impact_enabled: true, + last_impact_run_at: null, + }); + (getMetaQuery as jest.Mock).mockResolvedValue({ last_run_at: null, last_run_status: null }); + (getLLMKeysWithKeyQuery as jest.Mock).mockResolvedValue([{ key: "k" }]); + const req: any = { organizationId: 7, role: "Admin" }; + const res = mockRes(); + await getSettingsCtrl(req, res); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + data: expect.objectContaining({ has_llm_key: true, impact_enabled: true }), + }), + ); + }); + + it("has_llm_key=false when the org has no key", async () => { + (getSettings as jest.Mock).mockResolvedValue({ + recipient_user_ids: [], + recipient_emails: [], + updated_by: null, + updated_at: null, + impact_enabled: true, + last_impact_run_at: null, + }); + (getMetaQuery as jest.Mock).mockResolvedValue({ last_run_at: null, last_run_status: null }); + (getLLMKeysWithKeyQuery as jest.Mock).mockResolvedValue([]); + const req: any = { organizationId: 7, role: "Admin" }; + const res = mockRes(); + await getSettingsCtrl(req, res); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ data: expect.objectContaining({ has_llm_key: false }) }), + ); + }); +}); + +// --------------------------------------------------------------------------- +// PUT /api/regulations-tracker/settings — impact_enabled passthrough +// --------------------------------------------------------------------------- +describe("updateSettingsCtrl with impact_enabled", () => { + it("passes impact_enabled through to upsertSettings", async () => { + (upsertSettings as jest.Mock).mockResolvedValue({}); + const req: any = { + organizationId: 7, + userId: 1, + role: "Admin", + body: { recipient_user_ids: [], recipient_emails: [], impact_enabled: false }, + }; + const res = mockRes(); + await updateSettingsCtrl(req, res); + expect(upsertSettings).toHaveBeenCalledWith(7, [], [], 1, false); + }); +}); + +// --------------------------------------------------------------------------- +// GET /api/regulations-tracker/impact/:slug +// BUG 4: impact_enabled=false must return 200/null even when a row exists. +// BUG 6: handler must have logProcessing / logSuccess / logFailure. +// --------------------------------------------------------------------------- +describe("getImpactAnalysis", () => { + it("returns 200 with the stored row when impact is enabled", async () => { + (getSettings as jest.Mock).mockResolvedValue({ impact_enabled: true }); + const req: any = { userId: 1, organizationId: 7, params: { slug: "eu" } }; + const res = mockRes(); + await getImpactAnalysis(req, res); + expect(res.status).toHaveBeenCalledWith(200); + const payload = (res.json as jest.Mock).mock.calls[0][0]; + expect(payload.data).not.toBeNull(); + expect(payload.data.status).toBe("ok"); + }); + + // BUG 4: GET must honor impact_enabled toggle + it("returns 200/null when impact_enabled is false, even though a row exists", async () => { + (getSettings as jest.Mock).mockResolvedValue({ impact_enabled: false }); + const req: any = { userId: 1, organizationId: 7, params: { slug: "eu" } }; + const res = mockRes(); + await getImpactAnalysis(req, res); + expect(res.status).toHaveBeenCalledWith(200); + const payload = (res.json as jest.Mock).mock.calls[0][0]; + // Must be null — impact disabled means no panel + expect(payload.data).toBeNull(); + // getImpactRow must NOT have been called — settings check comes first + expect(getImpactRow).not.toHaveBeenCalled(); + }); + + it("returns 200/null when no row exists yet", async () => { + (getSettings as jest.Mock).mockResolvedValue({ impact_enabled: true }); + (getImpactRow as jest.Mock).mockResolvedValueOnce(null); + const req: any = { userId: 1, organizationId: 7, params: { slug: "eu" } }; + const res = mockRes(); + await getImpactAnalysis(req, res); + expect(res.status).toHaveBeenCalledWith(200); + expect((res.json as jest.Mock).mock.calls[0][0].data).toBeNull(); + }); + + it("fails open (200/null) when getSettings throws — transient settings blip must not 500 the GET", async () => { + // settings read fails → proceed as enabled → no impact row → 200/null + (getSettings as jest.Mock).mockRejectedValueOnce(new Error("db down")); + (getImpactRow as jest.Mock).mockResolvedValueOnce(null); + const req: any = { userId: 1, organizationId: 7, params: { slug: "eu" } }; + const res = mockRes(); + await getImpactAnalysis(req, res); + expect(res.status).toHaveBeenCalledWith(200); + expect((res.json as jest.Mock).mock.calls[0][0].data).toBeNull(); + }); + + it("returns 500 on unexpected error from getImpactRow", async () => { + (getSettings as jest.Mock).mockResolvedValue({ impact_enabled: true }); + (getImpactRow as jest.Mock).mockRejectedValueOnce(new Error("db down")); + const req: any = { userId: 1, organizationId: 7, params: { slug: "eu" } }; + const res = mockRes(); + await getImpactAnalysis(req, res); + expect(res.status).toHaveBeenCalledWith(500); + }); +}); + +// --------------------------------------------------------------------------- +// POST /api/regulations-tracker/impact/:slug/refresh [ADMIN] +// BUG 2: refreshImpactAnalysis must call runImpactAnalysis with force=true. +// BUG 6: handler must have logProcessing / logSuccess / logFailure. +// --------------------------------------------------------------------------- +describe("refreshImpactAnalysis", () => { + it("returns 403 for non-admin", async () => { + const req: any = { userId: 1, organizationId: 7, role: "Editor", params: { slug: "eu" } }; + const res = mockRes(); + await refreshImpactAnalysis(req, res); + expect(res.status).toHaveBeenCalledWith(403); + expect(runImpactAnalysis).not.toHaveBeenCalled(); + }); + + it("returns 200/disabled when impact_enabled is false", async () => { + (getSettings as jest.Mock).mockResolvedValue({ impact_enabled: false }); + const req: any = { userId: 1, organizationId: 7, role: "Admin", params: { slug: "eu" } }; + const res = mockRes(); + await refreshImpactAnalysis(req, res); + expect(res.status).toHaveBeenCalledWith(200); + expect((res.json as jest.Mock).mock.calls[0][0].data.status).toBe("disabled"); + expect(runImpactAnalysis).not.toHaveBeenCalled(); + }); + + // BUG 2: force=true must be passed so admin refresh is never silently no-op'd + it("calls runImpactAnalysis with force=true for an Admin", async () => { + (getSettings as jest.Mock).mockResolvedValue({ impact_enabled: true }); + const req: any = { userId: 1, organizationId: 7, role: "Admin", params: { slug: "eu" } }; + const res = mockRes(); + await refreshImpactAnalysis(req, res); + expect(res.status).toHaveBeenCalledWith(200); + // Verify force=true was passed + expect(runImpactAnalysis).toHaveBeenCalledWith(7, "eu", true); + }); + + it("returns 500 on unexpected error", async () => { + (getSettings as jest.Mock).mockResolvedValue({ impact_enabled: true }); + (runImpactAnalysis as jest.Mock).mockRejectedValueOnce(new Error("llm exploded")); + const req: any = { userId: 1, organizationId: 7, role: "Admin", params: { slug: "eu" } }; + const res = mockRes(); + await refreshImpactAnalysis(req, res); + expect(res.status).toHaveBeenCalledWith(500); + }); +}); diff --git a/Servers/controllers/regulationsTracker.ctrl.ts b/Servers/controllers/regulationsTracker.ctrl.ts new file mode 100644 index 0000000000..70e8fffe3e --- /dev/null +++ b/Servers/controllers/regulationsTracker.ctrl.ts @@ -0,0 +1,797 @@ +import { Request, Response } from "express"; +import { STATUS_CODE } from "../utils/statusCode.utils"; +import { logProcessing, logSuccess, logFailure } from "../utils/logger/logHelper"; +import logger from "../utils/logger/fileLogger"; +import { + listCountries, + getCountryRow, + listTracked, + trackCountry, + trackCountriesBulk, + untrackCountry, + getSettings, + upsertSettings, + getGlobalFeed, + getMetaQuery, + normalizeSlug, + enrichWithFlags, +} from "../utils/regulationsTracker.utils"; +import { + fetchCountryDetail, + fetchHorizon, + fetchDeadlines, + fetchSnapshot, +} from "../utils/regulationsTrackerFeed"; +import { getLLMKeysWithKeyQuery } from "../utils/llmKey.utils"; +import { getImpactRow, runImpactAnalysis } from "../utils/regulationImpact.utils"; + +const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; +const isAdmin = (role?: string) => role === "Admin" || role === "SuperAdmin"; +// Tracking (track / untrack / bulk-track) is allowed for admins and editors. +// Settings remain admin-only via isAdmin. +const canTrack = (role?: string) => role === "Admin" || role === "SuperAdmin" || role === "Editor"; + +const file = "regulationsTracker.ctrl.ts"; + +// --------------------------------------------------------------------------- +// GET /api/regulations-tracker/countries +// --------------------------------------------------------------------------- +export async function getCountries(req: Request, res: Response): Promise { + const fn = "getCountries"; + logProcessing({ + description: "list regulation countries", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + try { + const qStr = (v: unknown): string | undefined => (typeof v === "string" ? v : undefined); + const data = await listCountries(req.organizationId!, { + region: qStr(req.query.region), + q: qStr(req.query.q), + }); + await logSuccess({ + eventType: "Read", + description: "listed regulation countries", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(200).json(STATUS_CODE[200](data)); + } catch (error) { + await logFailure({ + eventType: "Read", + description: "list regulation countries failed", + functionName: fn, + fileName: file, + error: error as Error, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(500).json(STATUS_CODE[500]("Internal server error")); + } +} + +// --------------------------------------------------------------------------- +// GET /api/regulations-tracker/countries/:slug +// --------------------------------------------------------------------------- +export async function getCountryDetail(req: Request, res: Response): Promise { + const fn = "getCountryDetail"; + logProcessing({ + description: "proxy country detail", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + try { + const slug = req.params.slug as string; + const local = await getCountryRow(slug, req.organizationId!); + if (!local) return res.status(404).json(STATUS_CODE[404]("country not found")); + + // Try the live feed first; fall back to our stored snapshot if the feed is + // unreachable OR returns a 200 with an empty/unexpected body. We capture the + // fallback reason and log it, so a genuine bug in the live path surfaces in + // monitoring instead of being silently masked as "stale". + let staleReason: string | null = null; + try { + // Use the canonical stored slug (normalized) rather than the raw URL param to + // avoid a false stale fallback when the URL slug has different casing/whitespace. + const live = (await fetchCountryDetail(local.slug)) as { + country?: Record; + meta?: Record; + }; + // fetchCountryDetail only throws on non-200; a 200 with an empty/missing + // `country` would otherwise render a blank page. Guard for real content. + const liveCountry = live?.country; + if (!liveCountry || Object.keys(liveCountry).length === 0) { + staleReason = "live feed returned an empty country payload"; + } else { + await logSuccess({ + eventType: "Read", + description: "fetched live country detail", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + // The live feed nests detail under `country` with `meta` alongside; the client + // reads regulations/timeline/meta at the root, so flatten to one shape that + // matches the stale (DB) path exactly. + return res.status(200).json( + STATUS_CODE[200]({ + ...liveCountry, + meta: live.meta ?? null, + stale: false, + is_tracked: local.is_tracked, + }), + ); + } + } catch (liveErr) { + staleReason = (liveErr as Error).message; + } + + // Fallback path: log WHY we're serving stale so real failures are visible. + logger.warn(`[regulations-tracker] serving stored detail for ${local.slug}: ${staleReason}`); + await logSuccess({ + eventType: "Read", + description: "returned stored country detail", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + // local.data already holds the full detail (regulations/timeline/meta) seeded + // and refreshed by the daily sync, so this renders complete content offline. + return res + .status(200) + .json(STATUS_CODE[200]({ ...local.data, stale: true, is_tracked: local.is_tracked })); + } catch (error) { + await logFailure({ + eventType: "Read", + description: "country detail failed", + functionName: fn, + fileName: file, + error: error as Error, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(500).json(STATUS_CODE[500]("Internal server error")); + } +} + +// --------------------------------------------------------------------------- +// GET /api/regulations-tracker/tracked +// --------------------------------------------------------------------------- +export async function getTracked(req: Request, res: Response): Promise { + const fn = "getTracked"; + logProcessing({ + description: "list tracked countries", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + try { + const data = await listTracked(req.organizationId!); + await logSuccess({ + eventType: "Read", + description: "listed tracked countries", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(200).json(STATUS_CODE[200](data)); + } catch (error) { + await logFailure({ + eventType: "Read", + description: "list tracked countries failed", + functionName: fn, + fileName: file, + error: error as Error, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(500).json(STATUS_CODE[500]("Internal server error")); + } +} + +// --------------------------------------------------------------------------- +// POST /api/regulations-tracker/tracked [ADMIN] +// --------------------------------------------------------------------------- +export async function trackCountryCtrl(req: Request, res: Response): Promise { + const fn = "trackCountryCtrl"; + logProcessing({ + description: "track country", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + try { + if (!canTrack(req.role)) + return res.status(403).json(STATUS_CODE[403]("Admin or editor access required")); + const { slug } = req.body ?? {}; + if (!slug || typeof slug !== "string") + return res.status(400).json(STATUS_CODE[400]("slug is required")); + const result = await trackCountry(req.organizationId!, slug, req.userId!); + await logSuccess({ + eventType: "Create", + description: "tracked country", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(200).json(STATUS_CODE[200](result)); + } catch (error) { + await logFailure({ + eventType: "Create", + description: "track country failed", + functionName: fn, + fileName: file, + error: error as Error, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(500).json(STATUS_CODE[500]("Internal server error")); + } +} + +// --------------------------------------------------------------------------- +// POST /api/regulations-tracker/tracked/bulk [ADMIN] +// --------------------------------------------------------------------------- +export async function trackBulkCtrl(req: Request, res: Response): Promise { + const fn = "trackBulkCtrl"; + logProcessing({ + description: "bulk track countries", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + try { + if (!canTrack(req.role)) + return res.status(403).json(STATUS_CODE[403]("Admin or editor access required")); + const slugs: unknown = req.body?.slugs; + if (!Array.isArray(slugs) || slugs.length === 0) + return res.status(400).json(STATUS_CODE[400]("slugs must be a non-empty array")); + if (slugs.length > 200) + return res.status(400).json(STATUS_CODE[400]("too many slugs (max 200)")); + const badSlug = (slugs as unknown[]).find((s) => typeof s !== "string" || !s.trim()); + if (badSlug !== undefined) + return res.status(400).json(STATUS_CODE[400](`Invalid slug: ${String(badSlug)}`)); + const result = await trackCountriesBulk(req.organizationId!, slugs as string[], req.userId!); + await logSuccess({ + eventType: "Create", + description: "bulk tracked countries", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(200).json(STATUS_CODE[200](result)); + } catch (error) { + await logFailure({ + eventType: "Create", + description: "bulk track countries failed", + functionName: fn, + fileName: file, + error: error as Error, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(500).json(STATUS_CODE[500]("Internal server error")); + } +} + +// --------------------------------------------------------------------------- +// DELETE /api/regulations-tracker/tracked/:slug [ADMIN] +// --------------------------------------------------------------------------- +export async function untrackCountryCtrl(req: Request, res: Response): Promise { + const fn = "untrackCountryCtrl"; + logProcessing({ + description: "untrack country", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + try { + if (!canTrack(req.role)) + return res.status(403).json(STATUS_CODE[403]("Admin or editor access required")); + const slug = req.params.slug as string; + await untrackCountry(req.organizationId!, slug); + await logSuccess({ + eventType: "Delete", + description: "untracked country", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(200).json(STATUS_CODE[200]({ slug })); + } catch (error) { + await logFailure({ + eventType: "Delete", + description: "untrack country failed", + functionName: fn, + fileName: file, + error: error as Error, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(500).json(STATUS_CODE[500]("Internal server error")); + } +} + +// --------------------------------------------------------------------------- +// GET /api/regulations-tracker/settings +// --------------------------------------------------------------------------- +export async function getSettingsCtrl(req: Request, res: Response): Promise { + const fn = "getSettingsCtrl"; + logProcessing({ + description: "get regulation tracker settings", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + try { + const settings = await getSettings(req.organizationId!); + // Merge in global run observability (last sync time + outcome) so the + // Settings page can show when the catalogue was last checked. + const meta = await getMetaQuery(); + // Surface the FEED's own data-update timestamp (independent of when our sync + // last ran). A feed that returns 200 but stopped publishing looks identical + // to "nothing changed" from last_run_status alone; an aging + // feed_last_data_update is the signal the UI can warn on. Read from the + // stored horizon blob's meta.lastDataUpdate — no extra external call. Best- + // effort: never let it turn this GET into a 500. + let feed_last_data_update: string | null = null; + try { + const horizon = (await getGlobalFeed("horizon")) as { + meta?: { lastDataUpdate?: unknown }; + } | null; + const v = horizon?.meta?.lastDataUpdate; + if (typeof v === "string") feed_last_data_update = v; + } catch { + feed_last_data_update = null; + } + let has_llm_key = false; + let llm_key_provider: string | null = null; + let llm_key_model: string | null = null; + try { + const keys = await getLLMKeysWithKeyQuery(req.organizationId!); + has_llm_key = keys.length > 0; + if (has_llm_key) { + llm_key_provider = keys[0].name ?? null; + llm_key_model = keys[0].model ?? null; + } + } catch { + has_llm_key = false; + } + const data = { + ...settings, + last_run_at: meta.last_run_at, + last_run_status: meta.last_run_status, + feed_last_data_update, + has_llm_key, + llm_key_provider, + llm_key_model, + }; + await logSuccess({ + eventType: "Read", + description: "fetched regulation tracker settings", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(200).json(STATUS_CODE[200](data)); + } catch (error) { + await logFailure({ + eventType: "Read", + description: "get regulation tracker settings failed", + functionName: fn, + fileName: file, + error: error as Error, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(500).json(STATUS_CODE[500]("Internal server error")); + } +} + +// --------------------------------------------------------------------------- +// PUT /api/regulations-tracker/settings [ADMIN] +// --------------------------------------------------------------------------- +export async function updateSettingsCtrl(req: Request, res: Response): Promise { + const fn = "updateSettingsCtrl"; + logProcessing({ + description: "update regulation tracker settings", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + try { + if (!isAdmin(req.role)) return res.status(403).json(STATUS_CODE[403]("Admin access required")); + const recipientUserIds: unknown = req.body?.recipient_user_ids ?? []; + const recipientEmails: unknown = req.body?.recipient_emails ?? []; + if (!Array.isArray(recipientUserIds) || !Array.isArray(recipientEmails)) + return res + .status(400) + .json(STATUS_CODE[400]("recipient_user_ids and recipient_emails must be arrays")); + const badUserId = (recipientUserIds as unknown[]).find((id) => !Number.isInteger(id)); + if (badUserId !== undefined) + return res.status(400).json(STATUS_CODE[400](`Invalid user id: ${String(badUserId)}`)); + const badEmail = (recipientEmails as unknown[]).find( + (e) => typeof e !== "string" || !EMAIL_RE.test(e), + ); + if (badEmail !== undefined) + return res.status(400).json(STATUS_CODE[400](`Invalid email: ${String(badEmail)}`)); + const impactEnabledRaw = req.body?.impact_enabled; + const impactEnabled = typeof impactEnabledRaw === "boolean" ? impactEnabledRaw : undefined; + const result = await upsertSettings( + req.organizationId!, + recipientUserIds as number[], + recipientEmails as string[], + req.userId!, + impactEnabled, + ); + await logSuccess({ + eventType: "Update", + description: "updated regulation tracker settings", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(200).json(STATUS_CODE[200](result)); + } catch (error) { + await logFailure({ + eventType: "Update", + description: "update regulation tracker settings failed", + functionName: fn, + fileName: file, + error: error as Error, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(500).json(STATUS_CODE[500]("Internal server error")); + } +} + +// --------------------------------------------------------------------------- +// Global feeds: changelog (horizon), deadlines, international frameworks. +// Each tries the live feed first and falls back to the stored snapshot so the +// page renders offline. Returns { items, stale } (frameworks uses { items }); +// deadlines returns { deadlines, unscheduled, stale }. +// --------------------------------------------------------------------------- + +export async function getHorizon(req: Request, res: Response): Promise { + const fn = "getHorizon"; + logProcessing({ + description: "regulations changelog (horizon)", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + try { + try { + const live = (await fetchHorizon()) as { changes?: unknown[] }; + return res.status(200).json(STATUS_CODE[200]({ items: live.changes ?? [], stale: false })); + } catch { + const stored = (await getGlobalFeed("horizon")) as { changes?: unknown[] } | null; + return res.status(200).json(STATUS_CODE[200]({ items: stored?.changes ?? [], stale: true })); + } + } catch (error) { + await logFailure({ + eventType: "Read", + description: "horizon fetch failed", + functionName: fn, + fileName: file, + error: error as Error, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(500).json(STATUS_CODE[500]("Internal server error")); + } +} + +// --------------------------------------------------------------------------- +// Enriches a deadlines array with countryFlag from the regulation_countries +// enrichWithFlags has been moved to regulationsTracker.utils.ts (thin-controller +// convention: raw SQL belongs in utils, not controllers). It is imported above. + +export async function getDeadlines(req: Request, res: Response): Promise { + const fn = "getDeadlines"; + logProcessing({ + description: "regulations deadlines", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + try { + try { + const live = (await fetchDeadlines()) as { deadlines?: unknown[]; unscheduled?: unknown[] }; + // Run both flag-enrichment calls concurrently — they are independent queries. + const [deadlines, unscheduled] = await Promise.all([ + enrichWithFlags(live.deadlines ?? []), + enrichWithFlags(live.unscheduled ?? []), + ]); + return res.status(200).json( + STATUS_CODE[200]({ + deadlines, + unscheduled, + stale: false, + }), + ); + } catch { + const stored = (await getGlobalFeed("deadlines")) as { + deadlines?: unknown[]; + unscheduled?: unknown[]; + } | null; + // Run both flag-enrichment calls concurrently — they are independent queries. + const [deadlines, unscheduled] = await Promise.all([ + enrichWithFlags(stored?.deadlines ?? []), + enrichWithFlags(stored?.unscheduled ?? []), + ]); + return res.status(200).json( + STATUS_CODE[200]({ + deadlines, + unscheduled, + stale: true, + }), + ); + } + } catch (error) { + await logFailure({ + eventType: "Read", + description: "deadlines fetch failed", + functionName: fn, + fileName: file, + error: error as Error, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(500).json(STATUS_CODE[500]("Internal server error")); + } +} + +export async function getFrameworks(req: Request, res: Response): Promise { + const fn = "getFrameworks"; + logProcessing({ + description: "international AI frameworks", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + try { + // Read-only: try the live feed, fall back to the cached copy. The cache is + // written exclusively by the daily sync job's global-feed refresh — a GET + // must not mutate shared, cross-tenant state, so we do NOT write here. + try { + const live = (await fetchSnapshot()) as { frameworks?: unknown[] }; + return res.status(200).json(STATUS_CODE[200]({ items: live.frameworks ?? [], stale: false })); + } catch { + const stored = (await getGlobalFeed("frameworks")) as unknown[] | null; + return res.status(200).json(STATUS_CODE[200]({ items: stored ?? [], stale: true })); + } + } catch (error) { + await logFailure({ + eventType: "Read", + description: "frameworks fetch failed", + functionName: fn, + fileName: file, + error: error as Error, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(500).json(STATUS_CODE[500]("Internal server error")); + } +} + +// --------------------------------------------------------------------------- +// POST /api/regulations-tracker/sync [ADMIN] +// On-demand "check for updates now". Bypasses the daily-idempotency guard by +// clearing the day key (last_run_week column) first, then runs the sync inline. +// Rate-limited at the route layer. +// --------------------------------------------------------------------------- +export async function triggerSync(req: Request, res: Response): Promise { + const fn = "triggerSync"; + logProcessing({ + description: "manual regulations sync", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + try { + if (!isAdmin(req.role)) return res.status(403).json(STATUS_CODE[403]("Admin access required")); + // Lazy import to avoid a controller -> automations import cycle at module load. + const { syncRegulationsTracker } = + await import("../services/automations/actions/syncRegulationsTracker"); + const { clearLastRunDay } = await import("../utils/regulationsTracker.utils"); + await clearLastRunDay(); + const result = await syncRegulationsTracker(); + await logSuccess({ + eventType: "Update", + description: "manual regulations sync complete", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(200).json(STATUS_CODE[200](result)); + } catch (error) { + await logFailure({ + eventType: "Update", + description: "manual regulations sync failed", + functionName: fn, + fileName: file, + error: error as Error, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(500).json(STATUS_CODE[500]("Internal server error")); + } +} + +// --------------------------------------------------------------------------- +// GET /api/regulations-tracker/impact/:slug [any authenticated user] +// Returns the stored impact-analysis row for the org + country, plus a stale +// flag computed by comparing the stored regulation_hash to the current catalog. +// Returns 200/null when no analysis row exists yet or impact is disabled. +// --------------------------------------------------------------------------- +export async function getImpactAnalysis(req: Request, res: Response): Promise { + const fn = "getImpactAnalysis"; + logProcessing({ + description: "get regulation impact analysis", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + try { + // Honor the impact_enabled toggle — return null so the panel hides. + // Wrapped defensively: a transient settings read failure must not convert + // this GET into a 500 (it had no settings dependency before). On failure, + // default to treating impact as enabled so the panel still loads. + try { + const settings = await getSettings(req.organizationId!); + if (settings.impact_enabled === false) { + await logSuccess({ + eventType: "Read", + description: "impact analysis disabled — returning null", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(200).json(STATUS_CODE[200](null)); + } + } catch { + // Settings read failed — proceed as if impact is enabled (fail-open). + logger.warn( + "[regulations-tracker] getImpactAnalysis: settings read failed; treating as enabled", + ); + } + + // Normalize slug so reads agree with writes. + const slug = normalizeSlug(req.params.slug as string); + const row = await getImpactRow(req.organizationId!, slug); + if (!row) { + await logSuccess({ + eventType: "Read", + description: "no impact analysis row yet", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(200).json(STATUS_CODE[200](null)); + } + // Staleness: compare the hash that was current when analysis ran against + // the hash that is current in the catalog now. Wrapped defensively so that + // test mocks which stub this util to {} still pass. + let stale = false; + try { + const current = await getCountryRow(slug, req.organizationId!); + stale = !!current && current.hash !== row.regulation_hash; + } catch { + // Unable to fetch current hash — treat as not stale rather than erroring. + } + await logSuccess({ + eventType: "Read", + description: "fetched impact analysis", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(200).json( + STATUS_CODE[200]({ + result: row.result, + status: row.status, + refreshed_at: row.refreshed_at, + stale, + }), + ); + } catch (error) { + await logFailure({ + eventType: "Read", + description: "get impact analysis failed", + functionName: fn, + fileName: file, + error: error as Error, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(500).json(STATUS_CODE[500]("Internal server error")); + } +} + +// --------------------------------------------------------------------------- +// POST /api/regulations-tracker/impact/:slug/refresh [ADMIN] +// Runs the full impact-analysis pipeline for the org + country and returns the +// fresh result. Rate-limited at the route layer. +// --------------------------------------------------------------------------- +export async function refreshImpactAnalysis(req: Request, res: Response): Promise { + const fn = "refreshImpactAnalysis"; + logProcessing({ + description: "refresh regulation impact analysis", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + if (!isAdmin(req.role)) { + return res.status(403).json(STATUS_CODE[403]("Admin access required")); + } + try { + const settings = await getSettings(req.organizationId!); + if (settings.impact_enabled === false) { + await logSuccess({ + eventType: "Update", + description: "impact analysis disabled — skipping refresh", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(200).json(STATUS_CODE[200]({ status: "disabled" })); + } + // BUG 3: Normalize slug. + // BUG 2: Pass force=true so admin re-analysis is never silently skipped by cache. + const slug = normalizeSlug(req.params.slug as string); + const out = await runImpactAnalysis(req.organizationId!, slug, true); + await logSuccess({ + eventType: "Update", + description: "refreshed impact analysis", + functionName: fn, + fileName: file, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(200).json(STATUS_CODE[200](out)); + } catch (error) { + await logFailure({ + eventType: "Update", + description: "refresh impact analysis failed", + functionName: fn, + fileName: file, + error: error as Error, + userId: req.userId!, + organizationId: req.organizationId!, + }); + return res.status(500).json(STATUS_CODE[500]("Internal server error")); + } +} diff --git a/Servers/database/db.ts b/Servers/database/db.ts index c0c30fd7a9..a05472885a 100644 --- a/Servers/database/db.ts +++ b/Servers/database/db.ts @@ -101,6 +101,10 @@ import { GovernanceScenarioRuleModel } from "../domain.layer/models/governanceOs import { GovernanceOrgPreferencesModel } from "../domain.layer/models/governanceOs/governanceOrgPreferences.model"; import { GovernanceCoverageCacheModel } from "../domain.layer/models/governanceOs/governanceCoverageCache.model"; import { GovernanceScenarioActivationModel } from "../domain.layer/models/governanceOs/governanceScenarioActivation.model"; +import { RegulationCountryModel } from "../domain.layer/models/regulationsTracker/regulationCountry.model"; +import { RegulationTrackedCountryModel } from "../domain.layer/models/regulationsTracker/regulationTrackedCountry.model"; +import { RegulationTrackerSettingsModel } from "../domain.layer/models/regulationsTracker/regulationTrackerSettings.model"; +import { RegulationTrackerMetaModel } from "../domain.layer/models/regulationsTracker/regulationTrackerMeta.model"; dotenv.config(); @@ -224,6 +228,10 @@ const sequelize = new Sequelize(conf.database!, conf.username!, conf.password, { GovernanceOrgPreferencesModel, GovernanceCoverageCacheModel, GovernanceScenarioActivationModel, + RegulationCountryModel, + RegulationTrackedCountryModel, + RegulationTrackerSettingsModel, + RegulationTrackerMetaModel, ], }) as Sequelize; diff --git a/Servers/database/migrations/20260626043929-repair-files-content-json-buffer-corruption.js b/Servers/database/migrations/20260626043929-repair-files-content-json-buffer-corruption.js new file mode 100644 index 0000000000..65e8a5f26e --- /dev/null +++ b/Servers/database/migrations/20260626043929-repair-files-content-json-buffer-corruption.js @@ -0,0 +1,112 @@ +"use strict"; + +/** + * Repair `files.content` rows whose binary was stored as JSON text instead of + * raw bytes. + * + * Background + * ---------- + * An older upload path serialized the uploaded buffer with + * `JSON.stringify(buffer.toJSON())` before the INSERT, so the `bytea` column + * ended up holding the literal text `{"type":"Buffer","data":[37,80,68,70,...]}` + * instead of the file's raw bytes. Downloads returned that JSON faithfully, so + * the file opened as "damaged". The current upload code (fileUpload.utils.ts and + * file.repository.ts) binds the raw Buffer directly, so no new rows are + * corrupted — this migration only repairs the historical data. + * + * The original bytes are fully preserved inside the JSON `data` array, so the + * repair is lossless: parse the wrapper and rewrite `content` as the raw bytea. + * + * Why JavaScript instead of pure SQL + * ---------------------------------- + * A pure-SQL reconstruction (unnest the JSON `data` array to one row per byte, + * then re-aggregate) explodes a multi-MB file into millions of rows and spills + * to disk — on a constrained host it fails with "No space left on device". + * Reconstructing with `Buffer.from(obj.data)` in Node is O(bytes) in memory, + * processes one row at a time, and writes back only the small raw buffer. + * + * Safety guards (per row) + * ----------------------- + * - Only rows whose content begins with the wrapper signature + * (`{"type":"Buffer"`) are touched. Already-correct binary rows are skipped. + * - The parsed object must be a Buffer wrapper with an array `data`. + * - The reconstructed length must equal the wrapper's declared `data` length, + * otherwise the row is skipped (never overwrite with a partial reconstruction). + * + * Irreversible: `down()` is intentionally a no-op. The repaired rows hold the + * correct raw bytes; re-wrapping them as JSON text would re-introduce the + * corruption, so we do not. + */ + +const WRAPPER_PREFIX = '{"type":"Buffer"'; + +module.exports = { + async up(queryInterface) { + const sequelize = queryInterface.sequelize; + + // Identify candidate rows without pulling their (large) content. The first + // 16 bytes of the bytea are enough to detect the JSON wrapper. + const [candidates] = await sequelize.query(` + SELECT id + FROM verifywise.files + WHERE content IS NOT NULL + AND get_byte(content, 0) = 123 -- leading '{' + AND left(convert_from(substring(content FROM 1 FOR 16), 'UTF8'), 16) = '${WRAPPER_PREFIX}' + ORDER BY id + `); + + let repaired = 0; + let skipped = 0; + + for (const { id } of candidates) { + // Fetch one row's content at a time to bound memory. + const [[row]] = await sequelize.query("SELECT content FROM verifywise.files WHERE id = :id", { + replacements: { id }, + }); + if (!row || !row.content) { + skipped++; + continue; + } + + const buf = Buffer.isBuffer(row.content) ? row.content : Buffer.from(row.content); + if (!buf.subarray(0, 16).toString("latin1").startsWith(WRAPPER_PREFIX)) { + skipped++; + continue; + } + + let parsed; + try { + parsed = JSON.parse(buf.toString("utf8")); + } catch { + skipped++; + continue; + } + + if (!parsed || parsed.type !== "Buffer" || !Array.isArray(parsed.data)) { + skipped++; + continue; + } + + const raw = Buffer.from(parsed.data); + if (raw.length !== parsed.data.length) { + skipped++; + continue; + } + + await sequelize.query("UPDATE verifywise.files SET content = :content WHERE id = :id", { + replacements: { content: raw, id }, + }); + repaired++; + } + + // eslint-disable-next-line no-console + console.log( + `[repair-files-content] candidates=${candidates.length} repaired=${repaired} skipped=${skipped}`, + ); + }, + + async down() { + // Intentionally irreversible. The repaired rows now hold the correct raw + // bytes; reconstructing the JSON-text wrapper would re-corrupt valid data. + }, +}; diff --git a/Servers/database/migrations/20260626092700-create-regulations-tracker-tables.js b/Servers/database/migrations/20260626092700-create-regulations-tracker-tables.js new file mode 100644 index 0000000000..6e06ef974a --- /dev/null +++ b/Servers/database/migrations/20260626092700-create-regulations-tracker-tables.js @@ -0,0 +1,96 @@ +"use strict"; + +/** + * Regulations Tracker module tables. + * + * `regulation_countries` and `regulation_tracker_meta` are GLOBAL (no + * organization_id): the Global AI Regulations feed is public reference data, + * identical for every org. Tenancy is enforced only on + * `regulation_tracked_countries` and `regulation_tracker_settings`. + * + * Tracking links to a country by `country_slug` (the feed's stable identity), + * intentionally WITHOUT a foreign key, so a feed re-import can never + * cascade-delete durable user tracking. + */ +module.exports = { + async up(queryInterface) { + await queryInterface.sequelize.query(` + CREATE TABLE IF NOT EXISTS verifywise.regulation_countries ( + id SERIAL PRIMARY KEY, + slug VARCHAR(120) NOT NULL UNIQUE, + name VARCHAR(255) NOT NULL, + region VARCHAR(50), + regulation_count SMALLINT, + data JSONB NOT NULL, + hash VARCHAR(80) NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + removed_at TIMESTAMPTZ, + last_changed_at TIMESTAMPTZ, + last_fetched_at TIMESTAMPTZ + ); + `); + await queryInterface.sequelize.query(` + CREATE INDEX IF NOT EXISTS idx_reg_countries_active_region + ON verifywise.regulation_countries(is_active, region); + CREATE INDEX IF NOT EXISTS idx_reg_countries_name + ON verifywise.regulation_countries(name); + `); + + await queryInterface.sequelize.query(` + CREATE TABLE IF NOT EXISTS verifywise.regulation_tracked_countries ( + id SERIAL PRIMARY KEY, + organization_id INTEGER NOT NULL REFERENCES verifywise.organizations(id) ON DELETE CASCADE, + country_slug VARCHAR(120) NOT NULL, + tracked_by INTEGER REFERENCES verifywise.users(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (organization_id, country_slug) + ); + `); + await queryInterface.sequelize.query(` + CREATE INDEX IF NOT EXISTS idx_reg_tracked_org + ON verifywise.regulation_tracked_countries(organization_id); + CREATE INDEX IF NOT EXISTS idx_reg_tracked_slug + ON verifywise.regulation_tracked_countries(country_slug); + `); + + await queryInterface.sequelize.query(` + CREATE TABLE IF NOT EXISTS verifywise.regulation_tracker_settings ( + organization_id INTEGER PRIMARY KEY REFERENCES verifywise.organizations(id) ON DELETE CASCADE, + recipient_user_ids JSONB NOT NULL DEFAULT '[]'::jsonb, + recipient_emails JSONB NOT NULL DEFAULT '[]'::jsonb, + updated_by INTEGER, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ); + `); + + await queryInterface.sequelize.query(` + CREATE TABLE IF NOT EXISTS verifywise.regulation_tracker_meta ( + id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1), + seeded_at TIMESTAMPTZ, + last_good_count INTEGER, + -- Holds a "YYYY-MM-DD" day key (10 chars). VARCHAR(20) leaves headroom + -- so a future format change (e.g. ISO week-with-day) can't truncate it. + last_run_week VARCHAR(20) + ); + `); + await queryInterface.sequelize.query(` + INSERT INTO verifywise.regulation_tracker_meta (id) + VALUES (1) ON CONFLICT (id) DO NOTHING; + `); + }, + + async down(queryInterface) { + await queryInterface.sequelize.query( + "DROP TABLE IF EXISTS verifywise.regulation_tracked_countries CASCADE", + ); + await queryInterface.sequelize.query( + "DROP TABLE IF EXISTS verifywise.regulation_tracker_settings CASCADE", + ); + await queryInterface.sequelize.query( + "DROP TABLE IF EXISTS verifywise.regulation_tracker_meta CASCADE", + ); + await queryInterface.sequelize.query( + "DROP TABLE IF EXISTS verifywise.regulation_countries CASCADE", + ); + }, +}; diff --git a/Servers/database/migrations/20260626093734-seed-regulations-tracker-snapshot.js b/Servers/database/migrations/20260626093734-seed-regulations-tracker-snapshot.js new file mode 100644 index 0000000000..91ff0dbaf3 --- /dev/null +++ b/Servers/database/migrations/20260626093734-seed-regulations-tracker-snapshot.js @@ -0,0 +1,67 @@ +"use strict"; + +/** + * Seed the regulation_countries catalog from a committed snapshot on first + * install. Idempotent: skips if the table is already populated. Establishes the + * baseline so the first weekly sync detects no changes (no false notifications). + */ +const fs = require("fs"); +const path = require("path"); + +module.exports = { + async up(queryInterface) { + const existing = await queryInterface.sequelize.query( + "SELECT COUNT(*)::int AS n FROM verifywise.regulation_countries", + { type: queryInterface.sequelize.QueryTypes.SELECT }, + ); + if (existing[0].n > 0) return; // already seeded + + const snapshotPath = path.join(__dirname, "../seeds/regulations-tracker-snapshot.json"); + if (!fs.existsSync(snapshotPath)) { + throw new Error( + `Regulations Tracker seed snapshot not found at ${snapshotPath}. ` + + "Ensure Servers/database/seeds/regulations-tracker-snapshot.json is committed and bundled with the deploy artifact.", + ); + } + const snapshot = JSON.parse(fs.readFileSync(snapshotPath, "utf8")); + + for (const c of snapshot.countries) { + await queryInterface.sequelize.query( + `INSERT INTO verifywise.regulation_countries + (slug, name, region, regulation_count, data, hash, is_active, last_fetched_at) + VALUES (:slug, :name, :region, :regulation_count, :data::jsonb, :hash, TRUE, NOW()) + ON CONFLICT (slug) DO NOTHING`, + { + replacements: { + slug: c.slug, + name: c.name, + region: c.region ?? null, + regulation_count: c.regulationCount ?? null, + data: JSON.stringify(c), + hash: c.hash, + }, + }, + ); + } + + await queryInterface.sequelize.query( + `UPDATE verifywise.regulation_tracker_meta + SET seeded_at = NOW(), last_good_count = :count + WHERE id = 1`, + { replacements: { count: snapshot.countries.length } }, + ); + }, + + async down(queryInterface) { + // Only reset the seed markers. We deliberately do NOT `DELETE FROM + // regulation_countries` here: that table is global (shared by every tenant) + // and a bare DELETE would wipe the entire live catalog — every org's Browse, + // Tracked, Deadlines and Frameworks views go dark — for a mere seed rollback. + // Full teardown is the create-tables migration's down(), which DROPs the + // table outright. Rolling back just the seed leaves the catalog populated, + // which is harmless (the COUNT(*) guard in up() makes a re-run a no-op). + await queryInterface.sequelize.query( + "UPDATE verifywise.regulation_tracker_meta SET seeded_at = NULL, last_good_count = NULL WHERE id = 1", + ); + }, +}; diff --git a/Servers/database/migrations/20260626114016-add-regulations-tracker-notification-enum-values.js b/Servers/database/migrations/20260626114016-add-regulations-tracker-notification-enum-values.js new file mode 100644 index 0000000000..3f11478ec7 --- /dev/null +++ b/Servers/database/migrations/20260626114016-add-regulations-tracker-notification-enum-values.js @@ -0,0 +1,23 @@ +"use strict"; +/** + * Extend enum_notification_type with 'regulations_tracker' and + * enum_notification_entity_type with 'regulation_country' for the Regulations + * Tracker weekly digest notifications. Without these, every INSERT INTO + * notifications from syncRegulationsTracker fails with + * "invalid input value for enum ...", aborting the weekly run. + * Postgres 12+ allows ALTER TYPE ... ADD VALUE. Down is a no-op (removing an + * enum value requires recreating the type — risky for a fix-forward migration). + */ +module.exports = { + async up(queryInterface) { + await queryInterface.sequelize.query(` + ALTER TYPE verifywise.enum_notification_type ADD VALUE IF NOT EXISTS 'regulations_tracker'; + `); + await queryInterface.sequelize.query(` + ALTER TYPE verifywise.enum_notification_entity_type ADD VALUE IF NOT EXISTS 'regulation_country'; + `); + }, + async down() { + /* No-op: enum value removal requires type recreation. */ + }, +}; diff --git a/Servers/database/migrations/20260626124416-add-regulations-tracker-global-feeds.js b/Servers/database/migrations/20260626124416-add-regulations-tracker-global-feeds.js new file mode 100644 index 0000000000..5b586828fc --- /dev/null +++ b/Servers/database/migrations/20260626124416-add-regulations-tracker-global-feeds.js @@ -0,0 +1,33 @@ +"use strict"; + +/** + * Add columns to the regulation_tracker_meta singleton to cache the three + * global, non-tenant feeds the Regulations Tracker mirrors from the website: + * - horizon : the curated dated changelog (/api/regulations/horizon) + * - deadlines : forward-looking effective-date milestones (/api/regulations/deadlines) + * - frameworks : the international AI governance frameworks (/api/regulations/snapshot -> frameworks) + * + * These are public reference data identical for every org, so they live on the + * existing global singleton (id=1) rather than a tenant table. Stored as JSONB + * so the Browse/Horizon/Deadlines/Frameworks pages render from our DB + * (offline-safe), with the weekly sync refreshing them. + */ +module.exports = { + async up(queryInterface) { + await queryInterface.sequelize.query(` + ALTER TABLE verifywise.regulation_tracker_meta + ADD COLUMN IF NOT EXISTS horizon JSONB, + ADD COLUMN IF NOT EXISTS deadlines JSONB, + ADD COLUMN IF NOT EXISTS frameworks JSONB; + `); + }, + + async down(queryInterface) { + await queryInterface.sequelize.query(` + ALTER TABLE verifywise.regulation_tracker_meta + DROP COLUMN IF EXISTS horizon, + DROP COLUMN IF EXISTS deadlines, + DROP COLUMN IF EXISTS frameworks; + `); + }, +}; diff --git a/Servers/database/migrations/20260626165730-add-regulations-tracker-run-status.js b/Servers/database/migrations/20260626165730-add-regulations-tracker-run-status.js new file mode 100644 index 0000000000..e5bdd067a1 --- /dev/null +++ b/Servers/database/migrations/20260626165730-add-regulations-tracker-run-status.js @@ -0,0 +1,28 @@ +"use strict"; + +/** + * Add run-observability columns to the regulation_tracker_meta singleton so the + * app can show when the weekly sync last ran and whether it succeeded. Without + * these, a feed that is down for weeks (or failing emails) is invisible in-app — + * only the server logs know. + * + * - last_run_at : timestamp of the most recent sync attempt (any outcome) + * - last_run_status : short outcome string ("ok", "skipped: ...", "fetch failed", etc.) + */ +module.exports = { + async up(queryInterface) { + await queryInterface.sequelize.query(` + ALTER TABLE verifywise.regulation_tracker_meta + ADD COLUMN IF NOT EXISTS last_run_at TIMESTAMPTZ, + ADD COLUMN IF NOT EXISTS last_run_status VARCHAR(120); + `); + }, + + async down(queryInterface) { + await queryInterface.sequelize.query(` + ALTER TABLE verifywise.regulation_tracker_meta + DROP COLUMN IF EXISTS last_run_at, + DROP COLUMN IF EXISTS last_run_status; + `); + }, +}; diff --git a/Servers/database/migrations/20260627112358-create-regulation-impact-analysis-table.js b/Servers/database/migrations/20260627112358-create-regulation-impact-analysis-table.js new file mode 100644 index 0000000000..b337c9aa1d --- /dev/null +++ b/Servers/database/migrations/20260627112358-create-regulation-impact-analysis-table.js @@ -0,0 +1,41 @@ +"use strict"; +module.exports = { + async up(queryInterface) { + await queryInterface.sequelize.query(` + CREATE TABLE IF NOT EXISTS verifywise.regulation_impact_analysis ( + id SERIAL PRIMARY KEY, + organization_id INTEGER NOT NULL REFERENCES verifywise.organizations(id) ON DELETE CASCADE, + country_slug VARCHAR(120) NOT NULL, + regulation_hash VARCHAR(120) NOT NULL, + result JSONB, + status VARCHAR(120) NOT NULL, + model VARCHAR(255), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + refreshed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (organization_id, country_slug) + ); + `); + // NOTE: no explicit index on (organization_id, country_slug) — the UNIQUE + // constraint above already creates a B-tree index on exactly those columns, + // which serves every lookup (all reads are by org+slug). A separate index + // would be a pure duplicate (extra storage + write cost, no query benefit). + // Settings columns for the impact toggle + last-run line (§5a) + await queryInterface.sequelize.query(` + ALTER TABLE verifywise.regulation_tracker_settings + ADD COLUMN IF NOT EXISTS impact_enabled BOOLEAN NOT NULL DEFAULT true, + ADD COLUMN IF NOT EXISTS last_impact_run_at TIMESTAMPTZ; + `); + }, + async down(queryInterface) { + // ALTER TABLE IF EXISTS so an out-of-order rollback (settings table already + // dropped by an earlier migration's down) is a no-op instead of throwing. + await queryInterface.sequelize.query(` + ALTER TABLE IF EXISTS verifywise.regulation_tracker_settings + DROP COLUMN IF EXISTS impact_enabled, + DROP COLUMN IF EXISTS last_impact_run_at; + `); + await queryInterface.sequelize.query(` + DROP TABLE IF EXISTS verifywise.regulation_impact_analysis; + `); + }, +}; diff --git a/Servers/database/migrations/20260628110553-backfill-regulation-country-flags.js b/Servers/database/migrations/20260628110553-backfill-regulation-country-flags.js new file mode 100644 index 0000000000..a71fcbbe00 --- /dev/null +++ b/Servers/database/migrations/20260628110553-backfill-regulation-country-flags.js @@ -0,0 +1,50 @@ +"use strict"; + +/** + * Backfill the `flag` field into existing regulation_countries.data rows. + * + * Installs seeded before the snapshot carried per-country flags have + * data->>'flag' = NULL, so every page that derives a flag from the catalog + * (Browse, Tracked, Deadlines enrichment, etc.) renders the globe fallback. + * The current seed snapshot DOES include a top-level `flag` for all countries, + * so we re-read it and patch the flag into any row that is missing it. + * + * Idempotent: only updates rows whose data->>'flag' is NULL/empty, and only + * for slugs present in the snapshot with a flag. Re-running is a no-op once + * flags are present. Does NOT touch the hash (flag is presentation-only and + * not part of the change-detection contract), so it cannot trigger spurious + * change notifications on the next sync. + */ +const fs = require("fs"); +const path = require("path"); + +module.exports = { + async up(queryInterface) { + const snapshotPath = path.join(__dirname, "../seeds/regulations-tracker-snapshot.json"); + if (!fs.existsSync(snapshotPath)) { + // Flags are presentation-only; a missing snapshot should not fail the whole + // migration run. Skip gracefully — the next daily sync re-derives flags. + console.warn( + `[backfill-flags] snapshot not found at ${snapshotPath}; skipping flag backfill (sync will re-derive).`, + ); + return; + } + const snapshot = JSON.parse(fs.readFileSync(snapshotPath, "utf8")); + + for (const c of snapshot.countries) { + if (!c.flag) continue; // nothing to backfill for this country + await queryInterface.sequelize.query( + `UPDATE verifywise.regulation_countries + SET data = jsonb_set(COALESCE(data, '{}'::jsonb), '{flag}', to_jsonb(:flag::text), true) + WHERE slug = :slug + AND ((data->>'flag') IS NULL OR (data->>'flag') = '')`, + { replacements: { slug: c.slug, flag: c.flag } }, + ); + } + }, + + async down() { + // No-op: removing a presentation-only flag would regress the UI and the + // flag is harmless. Down is intentionally a no-op. + }, +}; diff --git a/Servers/database/seeds/regulations-tracker-snapshot.json b/Servers/database/seeds/regulations-tracker-snapshot.json new file mode 100644 index 0000000000..0d16cc9b7d --- /dev/null +++ b/Servers/database/seeds/regulations-tracker-snapshot.json @@ -0,0 +1,6858 @@ +{ + "feedVersion": 1, + "generatedAt": "2026-06-26T13:37:34.603Z", + "countries": [ + { + "slug": "european-union", + "name": "European Union", + "region": "europe", + "regulationCount": 6, + "hash": "sha256-8267827fab245c6bf91a79185262ab7d566d1d353daa5234083c4a77edce7a81", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-8267827fab245c6bf91a79185262ab7d566d1d353daa5234083c4a77edce7a81", + "regulationCount": 6 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/european-union", + "flag": "🇪🇺", + "oneLiner": "Binding high-risk AI law in force — compliance obligations are real and escalating.", + "executiveSummary": "The EU AI Act is the world’s first comprehensive, binding AI regulation and it is already enforceable. Prohibited AI practices (social scoring, certain biometric uses) became illegal in February 2025. General-purpose AI model providers must comply with transparency and safety rules by August 2025. Following the May 2026 omnibus agreement, standalone high-risk AI systems in Annex III (hiring, credit scoring, law enforcement) must comply by December 2027, and high-risk AI embedded in Annex I regulated products (medical devices, machinery) by August 2028. Penalties reach up to €35 million or 7% of global turnover — whichever is higher. Additionally, the proposed AI Liability Directive and the revised Product Liability Directive will create civil-liability pathways for individuals harmed by AI, while the Digital Services Act and Data Act layer on further obligations around transparency and data access.", + "practicalTakeaway": "If your company builds or deploys AI systems and operates in the European Union, you should immediately audit your AI portfolio against the EU AI Act’s risk tiers. Prohibited practices are already illegal. GPAI model providers must comply by August 2025. Start conformity-assessment preparation now for high-risk systems — the December 2027 (Annex III standalone) and August 2028 (Annex I embedded) deadlines require months of documentation, testing, and process changes. Budget for compliance infrastructure, appoint an AI compliance lead, and monitor the GPAI Code of Practice for evolving best-practice standards.", + "regulations": [ + { + "name": "EU AI Act (Regulation 2024/1689)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "August 2024", + "effectiveDateISO": "2024-08-01", + "dateConfidence": "approximate", + "scope": "All AI systems placed on the EU market or whose output is used in the EU, regardless of where the provider is established.", + "obligations": [ + "Classify AI systems by risk tier (unacceptable, high, limited, minimal)", + "Prohibited AI practices banned outright (social scoring, untargeted facial-recognition scraping, emotion recognition in workplaces/schools)", + "High-risk systems must pass conformity assessments, maintain technical documentation, implement risk-management systems, and ensure human oversight", + "General-purpose AI model providers must publish training-data summaries, comply with EU copyright law, and conduct model evaluations", + "Systemic-risk GPAI models require adversarial testing, incident reporting, and cybersecurity measures", + "Transparency obligations for AI systems that interact with people, generate deepfakes, or make emotion-recognition decisions" + ], + "maxPenalty": "€35 million or 7% of global annual turnover for prohibited-practice violations; €15 million or 3% for other non-compliance; ��7.5 million or 1% for supplying incorrect information", + "industryTags": ["general"], + "sourceUrl": "https://eur-lex.europa.eu/eli/reg/2024/1689/oj", + "lastVerified": "March 2026" + }, + { + "name": "AI Liability Directive (proposed)", + "type": "draft-bill", + "status": "proposed", + "effectiveDate": "Date TBD", + "effectiveDateISO": null, + "dateConfidence": "unknown", + "scope": "Civil liability claims involving AI systems within the EU, covering both providers and deployers.", + "obligations": [ + "Reversal of burden of proof: claimants benefit from a presumption of causation when an AI provider fails to disclose required information", + "Courts can order providers to disclose evidence about high-risk AI systems", + "Aligns liability framework with the risk tiers established in the EU AI Act" + ], + "maxPenalty": "Civil damages determined by national courts (no fixed regulatory cap)", + "industryTags": ["general"], + "sourceUrl": "https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A52022PC0496", + "lastVerified": "March 2026" + }, + { + "name": "Revised Product Liability Directive (Directive 2024/2853)", + "type": "binding-law", + "status": "passed-not-active", + "effectiveDate": "December 2026", + "effectiveDateISO": "2026-12-01", + "dateConfidence": "approximate", + "scope": "All products placed on the EU market, explicitly including software and AI systems as ‘products.’", + "obligations": [ + "AI software is explicitly a ‘product’ subject to strict liability for defects", + "Manufacturers liable for damage caused by defective AI outputs, including psychological harm above a defined threshold", + "Disclosure obligations triggered when claimants present plausible evidence of a defect", + "Member states must transpose the directive into national law by December 2026" + ], + "maxPenalty": "Civil damages determined by national courts; no regulatory fine cap, but strict liability applies without proof of fault", + "industryTags": ["general"], + "sourceUrl": "https://eur-lex.europa.eu/eli/dir/2024/2853/oj", + "lastVerified": "March 2026" + }, + { + "name": "GPAI Code of Practice", + "type": "voluntary-guideline", + "status": "in-force", + "effectiveDate": "November 2024", + "effectiveDateISO": "2024-11-01", + "dateConfidence": "approximate", + "scope": "Providers of general-purpose AI models operating in the EU, developed under Article 56 of the EU AI Act.", + "obligations": [ + "Publish sufficiently detailed summaries of training data", + "Implement copyright-compliance policies and respect opt-out mechanisms", + "Conduct and publish safety evaluations for systemic-risk models", + "Report serious incidents to the AI Office within defined timelines", + "Implement cybersecurity protections for model weights" + ], + "maxPenalty": "Non-compliance with the Code may be used as evidence of AI Act violation, triggering fines up to €15 million or 3% of global turnover", + "industryTags": ["general"], + "sourceUrl": "https://digital-strategy.ec.europa.eu/en/library/general-purpose-ai-code-practice", + "lastVerified": "March 2026" + }, + { + "name": "Digital Services Act — AI provisions (Regulation 2022/2065)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "February 2024", + "effectiveDateISO": "2024-02-01", + "dateConfidence": "approximate", + "scope": "Online platforms and search engines operating in the EU, specifically the use of AI in content recommendation, advertising targeting, and content moderation.", + "obligations": [ + "Disclose the main parameters of recommender-system algorithms and provide at least one option not based on profiling", + "Ban targeting ads using special-category personal data or data of minors", + "Very large platforms must assess and mitigate systemic risks from AI-driven amplification (disinformation, election interference, public health harms)", + "Provide researcher access to algorithmic data upon request from the Digital Services Coordinator" + ], + "maxPenalty": "Up to 6% of global annual turnover; periodic penalty payments up to 5% of average daily worldwide turnover", + "industryTags": ["general"], + "sourceUrl": "https://eur-lex.europa.eu/eli/reg/2022/2065/oj", + "lastVerified": "March 2026" + }, + { + "name": "Data Act — AI provisions (Regulation 2023/2854)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "September 2025", + "effectiveDateISO": "2025-09-01", + "dateConfidence": "approximate", + "scope": "Data generated by connected products and related services in the EU, including AI-generated or AI-processed data.", + "obligations": [ + "Users have the right to access and port data generated by their connected devices, including AI-processed outputs", + "Data holders must make data available in a machine-readable format on fair, reasonable, and non-discriminatory terms", + "Restrictions on unfair contractual terms related to data access", + "Trade-secret protections balanced against data-access rights, with technical safeguards permitted" + ], + "maxPenalty": "Penalties set by individual EU member states; the regulation requires they be ‘effective, proportionate, and dissuasive’", + "industryTags": ["general"], + "sourceUrl": "https://eur-lex.europa.eu/eli/reg/2023/2854/oj", + "lastVerified": "March 2026" + } + ], + "timeline": [ + { + "date": "August 2024", + "description": "EU AI Act enters into force (20 days after publication in the Official Journal)." + }, + { + "date": "February 2025", + "description": "Prohibited AI practices become enforceable. AI literacy obligations apply to all providers and deployers." + }, + { + "date": "August 2025", + "description": "General-purpose AI model obligations take effect. Governance structure (AI Office, Advisory Forum) fully operational." + }, + { + "date": "December 2026", + "description": "Article 50(2) watermarking and synthetic content disclosure obligations take effect for generative AI systems." + }, + { + "date": "December 2027", + "description": "High-risk AI systems in Annex III (standalone high-risk uses such as hiring, credit scoring, law enforcement) must comply." + }, + { + "date": "August 2028", + "description": "High-risk AI systems embedded in Annex I regulated products (medical devices, machinery, toys, lifts) must meet full compliance requirements." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "united-states", + "name": "United States", + "region": "north-america", + "regulationCount": 10, + "hash": "sha256-ab4d31f90a86ef91120d8e6d7c3030003e33969606513b01adb93a8d72ed019a", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-ab4d31f90a86ef91120d8e6d7c3030003e33969606513b01adb93a8d72ed019a", + "regulationCount": 10 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/united-states", + "flag": "🇺🇸", + "oneLiner": "No federal AI law yet — but state-level obligations are multiplying fast.", + "executiveSummary": "The U.S. has no comprehensive federal AI law. The regulatory landscape is a patchwork of executive orders, agency guidance, and state legislation. Executive Order 14179 (January 2025) replaced the prior EO 14110 and emphasizes removing barriers to AI innovation while directing agencies to use existing authority. NIST’s AI Risk Management Framework remains the de facto federal standard but is voluntary. Federal agencies like the FTC, FDA, EEOC, and OCC are using existing regulatory authority to address AI within their jurisdictions. Meanwhile, states are moving faster: Colorado passed the first comprehensive state AI act targeting high-risk systems, Illinois regulates AI in video interviews, NYC requires bias audits for automated employment tools, and California mandates transparency for AI-generated content. Engineering leaders must track obligations across every state where they operate.", + "practicalTakeaway": "If your company builds or deploys AI systems and operates in the United States, you should not be lulled by the absence of a comprehensive federal law. Map every state where your AI touches consumers or employees and check for applicable legislation — Colorado, Illinois, New York City, and California have already enacted binding requirements. Use the NIST AI RMF as your baseline framework even though it is voluntary, because regulators (FTC, EEOC, OCC) reference it in enforcement. If you operate in healthcare or financial services, sector-specific AI rules are already mandatory. Prepare for Colorado SB26-189’s January 1, 2027 effective date now: it requires developer documentation, pre-use notices, adverse-outcome explanations, and human-review workflows that take months to implement. Beyond the named state laws listed here, 40+ additional state AI bills are in various stages of legislation across the country.", + "regulations": [ + { + "name": "Executive Order 14179 — Removing Barriers to American Leadership in AI", + "type": "executive-order", + "status": "in-force", + "effectiveDate": "January 2025", + "effectiveDateISO": "2025-01-01", + "dateConfidence": "approximate", + "scope": "Federal government agencies; directs policy on AI development and use across the executive branch.", + "obligations": [ + "Revokes prior EO 14110 and its reporting and safety-testing mandates on frontier AI developers", + "Directs agencies to remove regulatory barriers that impede private-sector AI innovation", + "Requires an action plan within 180 days to sustain and enhance U.S. AI dominance", + "Maintains the focus on protecting Americans from AI-enabled fraud and deception using existing legal authorities" + ], + "maxPenalty": "Executive orders do not carry direct penalties; enforcement flows through agency rulemaking under existing statutes", + "industryTags": ["general"], + "sourceUrl": "https://www.federalregister.gov/documents/2025/01/31/2025-02172/removing-barriers-to-american-leadership-in-artificial-intelligence", + "lastVerified": "June 2026" + }, + { + "name": "NIST AI Risk Management Framework (AI RMF 1.0)", + "type": "voluntary-guideline", + "status": "in-force", + "effectiveDate": "January 2023", + "effectiveDateISO": "2023-01-01", + "dateConfidence": "approximate", + "scope": "Voluntary framework for all organizations designing, developing, deploying, or using AI systems in the United States.", + "obligations": [ + "Establish governance structures for AI risk management (Govern function)", + "Map AI risks by identifying context, stakeholders, and potential impacts (Map function)", + "Measure AI risks using quantitative and qualitative methods (Measure function)", + "Manage identified risks through prioritization, mitigation, and monitoring (Manage function)", + "Continuously monitor and document AI system behavior throughout the lifecycle" + ], + "maxPenalty": "No direct penalties (voluntary framework); however, adherence may be considered in enforcement actions by FTC and other agencies", + "industryTags": ["general"], + "sourceUrl": "https://www.nist.gov/artificial-intelligence/executive-order-safe-secure-and-trustworthy-artificial-intelligence", + "lastVerified": "March 2026" + }, + { + "name": "FTC guidance on AI and automated decision-making", + "type": "sector-regulation", + "status": "in-force", + "effectiveDate": "April 2021", + "effectiveDateISO": "2021-04-01", + "dateConfidence": "approximate", + "scope": "Any company subject to FTC jurisdiction (most U.S. businesses) using AI in consumer-facing products, marketing, or decision-making.", + "obligations": [ + "AI-driven claims and marketing must be truthful and substantiated; no deceptive AI claims", + "Companies must not use AI to engage in unfair or discriminatory practices under Section 5 of the FTC Act", + "Disclose material AI use to consumers when it affects purchasing decisions or outcomes", + "Maintain records of AI training data, model performance, and bias testing", + "FTC can order algorithmic disgorgement (deletion of models built on improperly collected data)" + ], + "maxPenalty": "$50,120 per violation per day under Section 5; FTC can also seek injunctive relief and algorithmic disgorgement", + "industryTags": ["general"], + "sourceUrl": "https://www.ftc.gov/business-guidance/blog/2021/04/aiming-truth-fairness-equity-your-companys-use-ai", + "lastVerified": "March 2026" + }, + { + "name": "FDA Software as a Medical Device (SaMD) framework", + "type": "sector-regulation", + "status": "in-force", + "effectiveDate": "January 2021", + "effectiveDateISO": "2021-01-01", + "dateConfidence": "approximate", + "scope": "AI/ML-enabled software intended for medical use, including diagnostic tools, clinical-decision-support systems, and monitoring devices.", + "obligations": [ + "Obtain premarket clearance (510(k)), De Novo classification, or premarket approval (PMA) for AI/ML medical devices", + "Implement a predetermined change-control plan for AI models that learn and update post-deployment", + "Maintain real-world performance monitoring and report adverse events", + "Follow Good Machine Learning Practice (GMLP) principles", + "Label AI/ML devices with clear descriptions of intended use, performance data, and limitations" + ], + "maxPenalty": "Product seizure, injunctions, and civil monetary penalties up to $15,000 per violation; criminal penalties for willful violations", + "industryTags": ["healthcare"], + "sourceUrl": "https://www.fda.gov/medical-devices/software-medical-device-samd/artificial-intelligence-and-machine-learning-aiml-enabled-medical-devices", + "lastVerified": "March 2026" + }, + { + "name": "EEOC guidance on AI in employment decisions", + "type": "sector-regulation", + "status": "in-force", + "effectiveDate": "May 2023", + "effectiveDateISO": "2023-05-01", + "dateConfidence": "approximate", + "scope": "Employers using AI or algorithmic tools in hiring, promotion, termination, or other employment decisions covered by Title VII, ADA, and ADEA.", + "obligations": [ + "Employers remain liable for discriminatory outcomes from AI tools even if the tool was developed by a third-party vendor", + "AI-based selection tools must be validated under the Uniform Guidelines on Employee Selection Procedures", + "Employers must provide reasonable accommodations for applicants who cannot use AI-driven assessment tools (ADA)", + "Algorithmic decision-making that causes disparate impact is unlawful unless the employer demonstrates business necessity" + ], + "maxPenalty": "Compensatory and punitive damages up to $300,000 per claim under Title VII; back pay, front pay, and injunctive relief; pattern-or-practice suits have no cap", + "industryTags": ["hr-employment"], + "sourceUrl": "https://www.eeoc.gov/eeoc-disability-related-resources/artificial-intelligence-and-ada", + "lastVerified": "June 2026" + }, + { + "name": "OCC model risk management guidance (SR 11-7, revised by Bulletin 2026-13)", + "type": "sector-regulation", + "status": "in-force", + "effectiveDate": "April 2011 (revised April 2026)", + "effectiveDateISO": "2011-04-01", + "dateConfidence": "approximate", + "scope": "All national banks, federal savings associations, and their subsidiaries using models (including AI/ML models) for decision-making. The original 2011-12 bulletin was rescinded and replaced by interagency Bulletin 2026-13 (OCC, Federal Reserve, FDIC) in April 2026.", + "obligations": [ + "Establish a model risk management framework covering model development, implementation, and use", + "Validate all models independently before deployment and on an ongoing basis", + "Document model limitations, assumptions, and performance metrics", + "Maintain a model inventory and classify models by risk tier", + "Board and senior management must oversee model risk with clear accountability" + ], + "maxPenalty": "Enforcement actions including cease-and-desist orders, civil monetary penalties up to $2 million per day for ongoing violations, and consent orders", + "industryTags": ["financial-services"], + "sourceUrl": "https://www.occ.gov/news-issuances/bulletins/2026/bulletin-2026-13.html", + "lastVerified": "June 2026" + }, + { + "name": "Colorado ADMT law (SB26-189, replacing the AI Act, SB 24-205)", + "type": "binding-law", + "status": "passed-not-active", + "effectiveDate": "January 1, 2027", + "effectiveDateISO": "2027-01-01", + "dateConfidence": "exact", + "scope": "Developers and deployers of automated decision-making technology (ADMT) that materially influences consequential decisions affecting Colorado consumers in education, employment, housing, finance, health care, insurance, and government services. SB26-189 (signed May 14, 2026) repealed and reenacted the Colorado AI Act, moved the effective date from June 30, 2026 to January 1, 2027, and narrowed the law from a risk-based framework to a transparency-and-disclosure regime.", + "obligations": [ + "Developers must provide deployers with technical documentation on intended uses, training-data categories, known limitations, and human-review instructions", + "Notify consumers when a consequential decision is made or substantially influenced by automated decision-making", + "Provide consumers with disclosures about the use of AI in consequential decisions", + "SB26-189 removed the original duty of reasonable care to prevent algorithmic discrimination, the deployer risk-management program, the impact-assessment requirement, and certain Attorney General reporting duties" + ], + "maxPenalty": "Enforced by Colorado Attorney General under the Colorado Consumer Protection Act; penalties up to $20,000 per violation with no statutory cap on aggregate fines", + "industryTags": ["general", "insurance", "hr-employment", "financial-services"], + "sourceUrl": "https://leg.colorado.gov/bills/sb26-189", + "lastVerified": "June 2026" + }, + { + "name": "Illinois AI Video Interview Act (AIVITA, 820 ILCS 42)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "January 2020", + "effectiveDateISO": "2020-01-01", + "dateConfidence": "approximate", + "scope": "Employers using AI to analyze video interviews of job applicants for positions based in Illinois.", + "obligations": [ + "Notify applicants before the interview that AI will be used to analyze their video", + "Explain how the AI works and what characteristics it evaluates", + "Obtain the applicant’s written consent before using AI analysis", + "Only share the video with persons whose expertise is necessary to evaluate the applicant", + "Destroy all video recordings within 30 days of an applicant’s request" + ], + "maxPenalty": "$1,000 per violation; enforced through private right of action; no cap on aggregate damages in class actions", + "industryTags": ["hr-employment"], + "sourceUrl": "https://www.ilga.gov/legislation/ilcs/ilcs3.asp?ActID=4015", + "lastVerified": "March 2026" + }, + { + "name": "NYC Local Law 144 (LL144) — Automated Employment Decision Tools", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "July 2023", + "effectiveDateISO": "2023-07-01", + "dateConfidence": "approximate", + "scope": "Employers and employment agencies in New York City using automated employment decision tools (AEDTs) for hiring or promotion.", + "obligations": [ + "Conduct an independent bias audit of the AEDT no more than one year before use", + "Publish a summary of the bias-audit results on the employer’s website", + "Notify candidates at least 10 business days before use of an AEDT", + "Provide candidates information about the data collected and the data-retention policy", + "Allow candidates to request an alternative selection process or accommodation" + ], + "maxPenalty": "$500 for the first violation; $500–$1,500 per subsequent violation per day; enforced by NYC Department of Consumer and Worker Protection", + "industryTags": ["hr-employment"], + "sourceUrl": "https://www.nyc.gov/site/dca/about/automated-employment-decision-tools.page", + "lastVerified": "March 2026" + }, + { + "name": "California AB 2013 — AI training data transparency", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "January 2026", + "effectiveDateISO": "2026-01-01", + "dateConfidence": "approximate", + "scope": "Developers of generative AI systems or services made available to Californians.", + "obligations": [ + "Publish on the developer’s website a high-level summary of training data used in the AI system", + "Include descriptions of data sources, data types, and whether the data includes personal information", + "Update the training-data disclosure when significant changes are made to training datasets", + "Make the disclosure accessible in a clear and easy-to-understand format" + ], + "maxPenalty": "Enforced by the California Attorney General; penalties under the Unfair Competition Law up to $2,500 per violation or $7,500 per intentional violation", + "industryTags": ["general"], + "sourceUrl": "https://leginfo.legislature.ca.gov/faces/billNavClient.xhtml?bill_id=202320240AB2013", + "lastVerified": "March 2026" + } + ], + "timeline": [ + { + "date": "January 2020", + "description": "Illinois AI Video Interview Act takes effect — first U.S. state law specifically targeting AI in hiring." + }, + { + "date": "January 2023", + "description": "NIST AI Risk Management Framework 1.0 published." + }, + { + "date": "July 2023", + "description": "NYC Local Law 144 on automated employment decision tools becomes enforceable." + }, + { + "date": "January 2025", + "description": "Executive Order 14179 signed, revoking EO 14110 and shifting federal AI policy toward innovation." + }, + { + "date": "January 2026", + "description": "California AB 2013 training-data transparency law takes effect." + }, + { + "date": "January 1, 2027", + "description": "Colorado SB26-189 (ADMT law, replacing the Colorado AI Act) takes effect." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "united-kingdom", + "name": "United Kingdom", + "region": "europe", + "regulationCount": 5, + "hash": "sha256-e434a1c5760c33a83e5afcad0eae488d1e4af2f0c108af7baa6df7be9b6f5468", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-e434a1c5760c33a83e5afcad0eae488d1e4af2f0c108af7baa6df7be9b6f5468", + "regulationCount": 5 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/united-kingdom", + "flag": "🇬🇧", + "oneLiner": "Voluntary principles today, but binding legislation is coming — monitor closely.", + "executiveSummary": "The UK has chosen a principles-based, sector-led approach to AI regulation. The government’s 2023 white paper established five cross-sector principles (safety, transparency, fairness, accountability, contestability) but left enforcement to existing regulators like the ICO, FCA, CMA, and Ofcom. The AI Safety Institute conducts frontier-model evaluations and safety research. However, the government has signaled a shift toward binding rules: the King’s Speech in July 2024 confirmed upcoming AI legislation, and a draft AI (Regulation) Bill is expected to impose mandatory requirements on the highest-risk AI systems. The ICO is already enforcing data-protection rules against AI systems under the UK GDPR, and the FCA has issued explicit guidance on AI use in financial services. Companies should treat the current voluntary framework as the floor, not the ceiling.", + "practicalTakeaway": "If your company builds or deploys AI systems and operates in the United Kingdom, you should implement the five AI principles (safety, transparency, fairness, accountability, contestability) now as your governance baseline. Comply with ICO guidance on AI and data protection immediately — the UK GDPR is actively enforced. If you operate in financial services, the FCA already expects robust AI model governance under existing rules. Track the forthcoming AI Bill closely: when it arrives, it will likely impose mandatory requirements that build on today’s voluntary framework, and companies already following best practices will have a compliance head start.", + "regulations": [ + { + "name": "Pro-Innovation Approach to AI Regulation (White Paper, CP 815)", + "type": "national-strategy", + "status": "policy-only", + "effectiveDate": "March 2023", + "effectiveDateISO": "2023-03-01", + "dateConfidence": "approximate", + "scope": "Cross-economy framework guiding all UK regulators in their approach to AI; not itself legally binding.", + "obligations": [ + "Regulators must interpret and apply five AI principles within their existing mandates: safety, transparency, fairness, accountability, and contestability", + "Regulators expected to publish AI-specific guidance for their sectors", + "Central coordination function monitors regulatory coherence across sectors", + "Sandbox and testbed programs to support responsible AI innovation" + ], + "maxPenalty": "No direct penalties (policy framework); enforcement through sector-specific regulators under existing legal powers", + "industryTags": ["general"], + "sourceUrl": "https://www.gov.uk/government/publications/ai-regulation-a-pro-innovation-approach/white-paper", + "lastVerified": "March 2026" + }, + { + "name": "AI Safety Institute (AISI)", + "type": "national-strategy", + "status": "in-force", + "effectiveDate": "November 2023", + "effectiveDateISO": "2023-11-01", + "dateConfidence": "approximate", + "scope": "Frontier AI models and advanced AI systems; operates through voluntary agreements with leading AI developers.", + "obligations": [ + "Conducts pre-deployment and post-deployment safety evaluations of frontier AI models", + "Publishes safety-evaluation reports and methodological research", + "Develops benchmarks and technical tools for AI safety testing", + "Engagement with AISI evaluations is currently voluntary for AI developers" + ], + "maxPenalty": "No enforcement power; participation is voluntary. Future AI legislation may grant statutory authority", + "industryTags": ["general"], + "sourceUrl": "https://www.aisi.gov.uk/", + "lastVerified": "March 2026" + }, + { + "name": "ICO guidance on AI and data protection", + "type": "sector-regulation", + "status": "in-force", + "effectiveDate": "March 2023", + "effectiveDateISO": "2023-03-01", + "dateConfidence": "approximate", + "scope": "Any organization processing personal data using AI systems under the UK GDPR and the Data Protection Act 2018.", + "obligations": [ + "Conduct data protection impact assessments (DPIAs) before deploying AI that processes personal data", + "Ensure lawful basis for processing — legitimate interest assessments required for AI training on personal data", + "Implement rights related to automated decision-making under Article 22 UK GDPR, including the right to human review", + "Maintain transparency: explain AI decision-making logic in privacy notices", + "Apply data-minimization and purpose-limitation principles to AI training data" + ], + "maxPenalty": "£17.5 million or 4% of global annual turnover under the UK GDPR, whichever is higher", + "industryTags": ["general"], + "sourceUrl": "https://ico.org.uk/for-organisations/uk-gdpr-guidance-and-resources/artificial-intelligence/", + "lastVerified": "March 2026" + }, + { + "name": "FCA guidance on AI in financial services (FS2/23)", + "type": "sector-regulation", + "status": "in-force", + "effectiveDate": "October 2023", + "effectiveDateISO": "2023-10-01", + "dateConfidence": "approximate", + "scope": "FCA-regulated firms using AI/ML models in consumer-facing services, trading, risk management, or compliance.", + "obligations": [ + "AI models used in consumer outcomes (credit, insurance, advice) must be explainable and fair", + "Firms must maintain robust model risk management frameworks for AI/ML models", + "Senior Management Function holders are accountable for AI governance under the Senior Managers and Certification Regime", + "Conduct regular fairness assessments to ensure AI does not create or reinforce unlawful discrimination", + "Document AI model development, validation, monitoring, and decommissioning processes" + ], + "maxPenalty": "Unlimited fines under FSMA 2000; FCA can impose requirements, suspend permissions, or pursue criminal prosecution for the most serious breaches", + "industryTags": ["financial-services", "insurance"], + "sourceUrl": "https://www.fca.org.uk/firms/innovation/ai-approach", + "lastVerified": "June 2026" + }, + { + "name": "Regulating for Growth Bill (announced; dedicated AI bill shelved)", + "type": "draft-bill", + "status": "proposed", + "effectiveDate": "Date TBD", + "effectiveDateISO": null, + "dateConfidence": "unknown", + "scope": "The May 2026 King's Speech announced a cross-economy 'Regulating for Growth Bill' rather than a standalone AI statute, confirming the UK's sector-regulator and sandbox approach to AI.", + "obligations": [ + "Provide cross-economy regulatory sandbox powers rather than a dedicated AI regime", + "Put the AI Safety Institute on a statutory footing", + "Keep AI oversight with existing sector regulators (ICO, FCA, CMA, Ofcom) under their current mandates", + "The previously floated plan to legislate directly on frontier and LLM developers has been shelved", + "Future binding AI-specific rules remain possible but are no longer the announced vehicle" + ], + "maxPenalty": "TBD — no dedicated AI statute announced; enforcement continues through existing sector regulators", + "industryTags": ["general"], + "sourceUrl": "https://www.gov.uk/government/organisations/ai-safety-institute", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "March 2023", + "description": "Government publishes ‘A Pro-Innovation Approach to AI Regulation’ white paper." + }, + { + "date": "November 2023", + "description": "AI Safety Institute launched at the Bletchley Park AI Safety Summit." + }, + { + "date": "February 2024", + "description": "Government publishes response to white paper consultation, confirming sector-led approach." + }, + { + "date": "July 2024", + "description": "King’s Speech confirms the government’s intention to introduce AI legislation." + }, + { + "date": "2025–2026", + "description": "Draft AI Bill expected for parliamentary scrutiny, with potential enactment in 2026 or 2027." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "china", + "name": "China", + "region": "asia", + "regulationCount": 6, + "hash": "sha256-d3406b58e2c63574f575eb90f670b47449f1cb8b1637ac186168dfdd7a9db548", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-d3406b58e2c63574f575eb90f670b47449f1cb8b1637ac186168dfdd7a9db548", + "regulationCount": 6 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/china", + "flag": "🇨🇳", + "oneLiner": "Six binding AI regulations already in force — the strictest regime outside the EU.", + "executiveSummary": "China has the most extensive binding AI regulation outside the European Union, with multiple sector-specific rules already in force. The Cyberspace Administration of China (CAC) has enacted regulations covering algorithmic recommendations (2022), deep synthesis / deepfakes (2022), and generative AI (2023). A mandatory AI content-labeling standard took effect in September 2025. These regulations require algorithm registration, security assessments, content moderation, and user transparency. The Personal Information Protection Law (PIPL) adds data-protection obligations that apply to AI training data. A comprehensive draft national AI Law is under development and expected to consolidate the existing rules into a single legislative framework. Penalties are enforced and regulators are active — non-compliance is not hypothetical.", + "practicalTakeaway": "If your company builds or deploys AI systems and operates in China, you must register your algorithms with the CAC before going live. This applies to recommendation engines, generative AI services, and deep synthesis tools. Ensure all AI-generated content is watermarked and labeled per the TC260 standard. AI training on personal data must comply with PIPL’s consent, impact-assessment, and data-localization requirements. Non-compliance is actively enforced: services have been suspended and fines have been levied. If you serve the Chinese market from abroad, these regulations still apply to services accessible in China. Budget for a China-specific compliance team or legal counsel.", + "regulations": [ + { + "name": "Interim Measures for the Management of Generative AI Services", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "August 2023", + "effectiveDateISO": "2023-08-01", + "dateConfidence": "approximate", + "scope": "Providers of generative AI services available to the public within mainland China.", + "obligations": [ + "Register generative AI services with the CAC before public launch through the algorithm-filing system", + "Conduct security assessments before deploying generative AI services", + "Implement content moderation to prevent generation of unlawful content", + "Label AI-generated content clearly so users and downstream parties can identify it", + "Ensure training data is lawfully obtained, respects intellectual property, and does not contain unlawful content", + "Provide mechanisms for users to report and complain about AI outputs" + ], + "maxPenalty": "Warnings, fines up to RMB 100,000 (~$14,000), suspension of services, revocation of business licenses; criminal liability for serious violations", + "industryTags": ["general"], + "sourceUrl": "http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm", + "lastVerified": "March 2026" + }, + { + "name": "Deep Synthesis Provisions (Internet Information Service Deep Synthesis Management Provisions)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "January 2023", + "effectiveDateISO": "2023-01-01", + "dateConfidence": "approximate", + "scope": "Providers and users of deep synthesis technology (deepfakes, voice cloning, AI-generated video/audio) within China.", + "obligations": [ + "Register deep synthesis algorithms with the CAC through the algorithm-filing system", + "Implement real-name verification for users of deep synthesis services", + "Add conspicuous labels to all deep synthesis content that cannot be removed", + "Maintain logs of deep synthesis operations for at least six months", + "Conduct security assessments for deep synthesis services that influence public opinion" + ], + "maxPenalty": "Warnings, fines up to RMB 100,000 (~$14,000), service suspension; referral to public security organs for criminal violations", + "industryTags": ["general"], + "sourceUrl": "http://www.cac.gov.cn/2022-12/11/c_1672221949318230.htm", + "lastVerified": "March 2026" + }, + { + "name": "Provisions on the Management of Algorithmic Recommendations for Internet Information Services", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "March 2022", + "effectiveDateISO": "2022-03-01", + "dateConfidence": "approximate", + "scope": "Internet platforms operating in China that use algorithmic recommendation systems to push content, products, or services to users.", + "obligations": [ + "File algorithmic recommendation systems with the CAC (algorithm registration system)", + "Provide users with the option to disable or modify algorithmic recommendations", + "Do not use algorithms to engage in price discrimination or unfair competitive practices", + "Protect minors by restricting addictive algorithm-driven content", + "Conduct regular algorithm assessments and maintain records", + "Do not use algorithms to spread disinformation or manipulate public opinion" + ], + "maxPenalty": "Fines up to RMB 100,000 (~$14,000) per violation; service suspension; revocation of permits for serious offenses", + "industryTags": ["general"], + "sourceUrl": "http://www.cac.gov.cn/2022-01/04/c_1642894606364259.htm", + "lastVerified": "March 2026" + }, + { + "name": "National Standard for AI-Generated Content Labeling (TC260)", + "type": "technical-standard", + "status": "in-force", + "effectiveDate": "September 2025", + "effectiveDateISO": "2025-09-01", + "dateConfidence": "approximate", + "scope": "All AI-generated content distributed online in China, including text, images, audio, and video.", + "obligations": [ + "Embed metadata labels in AI-generated content at the point of creation (invisible watermarks and visible markers)", + "Visible labels must appear on AI-generated images and videos in a standardized format", + "Platforms distributing AI-generated content must detect and display labeling information", + "Content without proper AI labeling may be removed from distribution platforms" + ], + "maxPenalty": "Penalties enforced under existing CAC regulations; platforms face fines and content removal orders", + "industryTags": ["general"], + "sourceUrl": "https://www.tc260.org.cn/", + "lastVerified": "March 2026" + }, + { + "name": "Personal Information Protection Law (PIPL)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "November 2021", + "effectiveDateISO": "2021-11-01", + "dateConfidence": "approximate", + "scope": "Any entity processing personal information of individuals in China, including for AI model training and inference.", + "obligations": [ + "Obtain consent or establish another lawful basis before processing personal information for AI training", + "Conduct personal information protection impact assessments before processing sensitive personal information", + "Implement data-localization requirements: personal information of Chinese citizens must be stored in China unless a security assessment is passed", + "Appoint a data protection officer and maintain records of processing activities", + "Provide individuals with the right to refuse automated decision-making and request explanations of AI decisions", + "Cross-border data transfers require CAC security assessments, standard contractual clauses, or certification" + ], + "maxPenalty": "Fines up to RMB 50 million (~$7 million) or 5% of the previous year’s annual revenue; personal liability for responsible individuals up to RMB 1 million (~$140,000)", + "industryTags": ["general"], + "sourceUrl": "http://www.npc.gov.cn/npc/c30834/202108/a8c4e3672c74491a80b53a172bb753fe.shtml", + "lastVerified": "March 2026" + }, + { + "name": "Draft National AI Law (Artificial Intelligence Law of the People’s Republic of China)", + "type": "draft-bill", + "status": "proposed", + "effectiveDate": "Date TBD", + "effectiveDateISO": null, + "dateConfidence": "unknown", + "scope": "Expected to be a comprehensive law covering AI development, deployment, and governance across all sectors in China.", + "obligations": [ + "Expected to consolidate existing algorithm, deep synthesis, and generative AI regulations into a unified framework", + "Likely to introduce tiered risk classification for AI systems", + "Expected to require safety assessments and algorithmic impact evaluations for high-risk AI", + "May establish new penalties and enforcement mechanisms beyond current regulations", + "Anticipated to address foundation models, autonomous systems, and AI in critical infrastructure" + ], + "maxPenalty": "TBD — expected to significantly increase penalty ceilings beyond current regulations", + "industryTags": ["general"], + "sourceUrl": "http://www.cac.gov.cn/", + "lastVerified": "March 2026" + } + ], + "timeline": [ + { + "date": "March 2022", + "description": "Algorithm Recommendation Provisions take effect — China’s first binding AI-specific regulation." + }, + { + "date": "January 2023", + "description": "Deep Synthesis Provisions become enforceable." + }, + { + "date": "August 2023", + "description": "Generative AI Interim Measures take effect." + }, + { + "date": "September 2025", + "description": "National AI content-labeling standard becomes mandatory." + }, + { + "date": "2025–2026", + "description": "Draft national AI Law expected to be finalized and enacted." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "australia", + "name": "Australia", + "region": "oceania", + "regulationCount": 5, + "hash": "sha256-7cea5a1182a1411b519d2e90e3d5872569dbb2798ae7be66c0608bc118b07416", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-7cea5a1182a1411b519d2e90e3d5872569dbb2798ae7be66c0608bc118b07416", + "regulationCount": 5 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/australia", + "flag": "🇦🇺", + "oneLiner": "Voluntary safety standard in place, mandatory guardrails under active consultation.", + "executiveSummary": "Australia currently relies on a voluntary AI Safety Standard (published June 2024) that sets ten guardrails for AI development and deployment. However, the government is actively consulting on making some or all of these guardrails mandatory, with a final policy position expected in 2025. The Privacy Act reforms, which are progressing through Parliament, will add specific provisions for automated decision-making and AI use of personal data. The ACCC has issued guidance on AI-related consumer protection, and the Australian Public Service has its own AI ethics framework governing federal agency use. Australia’s approach is transitional: companies that adopt the voluntary guardrails now will be well-positioned when mandatory rules arrive.", + "practicalTakeaway": "If your company builds or deploys AI systems and operates in Australia, you should adopt the Voluntary AI Safety Standard’s ten guardrails now as your operational baseline. Mandatory rules are coming — the government has signaled clearly that the voluntary phase is transitional. Begin conducting risk assessments and documenting your AI systems’ design and deployment decisions. Monitor the Privacy Act reforms closely: new automated-decision-making provisions will create enforceable rights for individuals affected by AI. If you sell AI products to the Australian government, compliance with the APS AI Ethics Framework is already a practical requirement for procurement eligibility.", + "regulations": [ + { + "name": "Voluntary AI Safety Standard", + "type": "voluntary-guideline", + "status": "voluntary", + "effectiveDate": "June 2024", + "effectiveDateISO": "2024-06-01", + "dateConfidence": "approximate", + "scope": "All organizations developing or deploying AI systems in Australia; voluntary but expected to become mandatory baseline.", + "obligations": [ + "Establish accountability processes and governance structures for AI systems", + "Conduct risk assessments proportionate to the AI system’s potential impact", + "Ensure transparency: provide clear information about AI capabilities and limitations", + "Test AI systems for safety, fairness, and reliability before and after deployment", + "Implement human oversight mechanisms appropriate to the risk level", + "Protect privacy and ensure lawful handling of personal data in AI systems", + "Enable contestability: provide mechanisms for individuals to challenge AI decisions", + "Maintain cybersecurity protections for AI systems and data pipelines", + "Keep records of AI system design, development, and deployment decisions", + "Engage with affected communities and stakeholders throughout the AI lifecycle" + ], + "maxPenalty": "No penalties (currently voluntary); adoption demonstrates good practice and may mitigate enforcement risk under future mandatory rules", + "industryTags": ["general"], + "sourceUrl": "https://www.industry.gov.au/publications/voluntary-ai-safety-standard", + "lastVerified": "March 2026" + }, + { + "name": "Mandatory AI Guardrails (shelved by the 2025 National AI Plan)", + "type": "draft-bill", + "status": "policy-only", + "effectiveDate": "Not proceeding as a dedicated regime", + "effectiveDateISO": null, + "dateConfidence": "unknown", + "scope": "The proposed mandatory guardrails for high-risk AI were not adopted. Australia's National AI Plan (December 2025) instead relies on uplifting existing technology-neutral laws plus voluntary guidance and an AI Safety Institute.", + "obligations": [ + "The ten guardrails remain voluntary rather than mandatory under the December 2025 National AI Plan", + "AI harms are to be addressed by strengthening existing laws (privacy, consumer, anti-discrimination) rather than a new high-risk AI Act", + "Government to stand up an AI Safety Institute and issue further voluntary guidance", + "A dedicated mandatory high-risk AI regime is not currently proceeding, though the position may be revisited" + ], + "maxPenalty": "No dedicated AI penalties; enforcement runs through existing consumer, privacy and anti-discrimination law", + "industryTags": ["general"], + "sourceUrl": "https://consult.industry.gov.au/supporting-responsible-ai", + "lastVerified": "June 2026" + }, + { + "name": "Privacy Act 1988 reforms — AI and automated decision-making provisions", + "type": "draft-bill", + "status": "proposed", + "effectiveDate": "Date TBD", + "effectiveDateISO": null, + "dateConfidence": "unknown", + "scope": "Organizations covered by the Privacy Act that use AI or automated systems to make decisions affecting individuals.", + "obligations": [ + "Right to meaningful explanation of substantially automated decisions that significantly affect individuals", + "Right to request human review of automated decisions", + "Privacy impact assessments required for AI systems processing personal information at scale", + "Enhanced transparency obligations for AI training on personal data, including purpose limitation", + "Stricter consent requirements for using personal information to train AI models" + ], + "maxPenalty": "Current Privacy Act maximum: AUD 50 million, three times the benefit obtained, or 30% of domestic turnover (whichever is greatest); reforms may increase these", + "industryTags": ["general"], + "sourceUrl": "https://www.ag.gov.au/rights-and-protections/privacy", + "lastVerified": "March 2026" + }, + { + "name": "ACCC guidance on AI and consumer protection", + "type": "sector-regulation", + "status": "in-force", + "effectiveDate": "January 2024", + "effectiveDateISO": "2024-01-01", + "dateConfidence": "approximate", + "scope": "All businesses operating in Australia that use AI in consumer-facing products, services, or marketing.", + "obligations": [ + "Do not make false or misleading representations about AI capabilities under the Australian Consumer Law", + "AI-generated content used in marketing must be accurate and not deceptive", + "Businesses remain liable for consumer harm caused by AI-driven decisions or recommendations", + "Ensure AI systems do not engage in unconscionable conduct or unfair contract terms" + ], + "maxPenalty": "AUD 50 million, three times the benefit obtained, or 30% of domestic turnover per contravention (whichever is greatest) under the Australian Consumer Law", + "industryTags": ["general"], + "sourceUrl": "https://www.accc.gov.au/by-industry/digital-platforms-and-services", + "lastVerified": "June 2026" + }, + { + "name": "Australian Public Service AI Ethics Framework", + "type": "voluntary-guideline", + "status": "in-force", + "effectiveDate": "November 2019", + "effectiveDateISO": "2019-11-01", + "dateConfidence": "approximate", + "scope": "Australian federal government agencies and departments using or procuring AI systems.", + "obligations": [ + "Apply eight AI ethics principles: accountability, fairness, privacy protection, reliability, safety, transparency, contestability, and human oversight", + "Conduct AI ethics assessments before procuring or deploying AI systems", + "Publish an AI ethics statement and designate an accountable official for AI governance", + "Maintain records of AI systems in use and their assessed risk levels", + "Ensure procurement contracts with AI vendors require compliance with the framework" + ], + "maxPenalty": "Internal government compliance mechanism; no external penalties but non-compliance may affect procurement eligibility", + "industryTags": ["public-sector"], + "sourceUrl": "https://www.industry.gov.au/publications/australias-artificial-intelligence-ethics-framework", + "lastVerified": "March 2026" + } + ], + "timeline": [ + { + "date": "November 2019", + "description": "Australian Government publishes AI Ethics Framework for the public service." + }, + { + "date": "June 2024", + "description": "Voluntary AI Safety Standard published with ten guardrails." + }, + { + "date": "September 2024", + "description": "Government launches public consultation on making AI guardrails mandatory." + }, + { + "date": "2025", + "description": "Final policy position on mandatory AI guardrails expected; Privacy Act reform bill progresses through Parliament." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "france", + "name": "France", + "region": "europe", + "regulationCount": 4, + "hash": "sha256-17f4c874b24dcc9b377319c4a79cc36628e37920ff05ba4beac9e34f1f310a31", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-17f4c874b24dcc9b377319c4a79cc36628e37920ff05ba4beac9e34f1f310a31", + "regulationCount": 4 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/france", + "flag": "🇫🇷", + "oneLiner": "Leading EU AI Act implementation with strong CNIL data enforcement and national AI strategy.", + "executiveSummary": "France is subject to the EU AI Act, which applies directly as an EU regulation. Beyond that, France has been one of the most proactive EU members on AI governance. The CNIL (Commission Nationale de l’Informatique et des Libertés) has published detailed guidance on AI and personal data, including a comprehensive action plan on generative AI. France’s National AI Strategy (‘France 2030’) allocates significant public funding to AI research and deployment. The government has also issued guidelines for public-sector AI use. CNIL actively enforces GDPR obligations related to AI training data, including high-profile investigations into web-scraping for model training.", + "practicalTakeaway": "If your company operates AI systems in France, the EU AI Act is your primary compliance framework. Beyond that, CNIL is one of Europe’s most active data protection authorities and is already investigating AI companies for GDPR violations related to training data. Conduct DPIAs for any AI system that processes personal data, document your legal basis for training data collection, and monitor CNIL’s evolving guidance on generative AI closely. If you sell AI products to the French public sector, expect transparency and impact-assessment requirements.", + "regulations": [ + { + "name": "EU AI Act (directly applicable in France)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "August 2024", + "effectiveDateISO": "2024-08-01", + "dateConfidence": "approximate", + "scope": "All AI systems placed on the French/EU market, with enforcement supported by the French AI authority designated under the Act.", + "obligations": [ + "All EU AI Act obligations apply directly: risk classification, prohibited practices, high-risk conformity assessments, GPAI transparency requirements", + "France is expected to designate a national competent authority (likely a body under the SGDSN or a new structure) for AI Act enforcement", + "Market surveillance for AI systems embedded in products will involve existing French sectoral regulators" + ], + "maxPenalty": "€35 million or 7% of global annual turnover for prohibited-practice violations (EU AI Act penalties)", + "industryTags": ["general"], + "sourceUrl": "https://eur-lex.europa.eu/eli/reg/2024/1689/oj", + "lastVerified": "March 2026" + }, + { + "name": "CNIL AI Action Plan and GDPR enforcement for AI", + "type": "sector-regulation", + "status": "in-force", + "effectiveDate": "May 2023", + "effectiveDateISO": "2023-05-01", + "dateConfidence": "approximate", + "scope": "Any organization processing personal data for AI development or deployment in France, including AI model training on personal data.", + "obligations": [ + "Conduct a data protection impact assessment (DPIA) before training AI models on personal data", + "Establish a valid legal basis under GDPR for collecting and using personal data for AI training (consent or legitimate interest)", + "Respect data subject rights including erasure and objection in the context of AI model training", + "Ensure transparency about AI data processing in privacy notices", + "CNIL has clarified that web-scraping personal data for AI training requires explicit legal basis and must respect robots.txt and opt-outs" + ], + "maxPenalty": "€20 million or 4% of global annual turnover under GDPR, enforced by CNIL", + "industryTags": ["general"], + "sourceUrl": "https://www.cnil.fr/en/topics/artificial-intelligence-ai", + "lastVerified": "June 2026" + }, + { + "name": "National AI Strategy — France 2030", + "type": "national-strategy", + "status": "in-force", + "effectiveDate": "November 2021", + "effectiveDateISO": "2021-11-01", + "dateConfidence": "approximate", + "scope": "National policy directing public investment, research priorities, and ecosystem development for AI in France.", + "obligations": [ + "Allocate €2.2 billion in public funding for AI research, talent development, and infrastructure through 2030", + "Establish national AI research institutes (including INRIA, CNRS programs) as centres of excellence", + "Support development of sovereign AI capabilities and French-language large language models", + "Promote responsible AI development through ethics advisory bodies and public engagement" + ], + "maxPenalty": "Not applicable (investment and policy strategy, not a regulatory instrument)", + "industryTags": ["general"], + "sourceUrl": "https://www.gouvernement.fr/france-2030", + "lastVerified": "March 2026" + }, + { + "name": "Public sector AI guidelines (DINUM framework)", + "type": "voluntary-guideline", + "status": "in-force", + "effectiveDate": "September 2023", + "effectiveDateISO": "2023-09-01", + "dateConfidence": "approximate", + "scope": "French government ministries, agencies, and public-service entities deploying or procuring AI systems.", + "obligations": [ + "Conduct an algorithmic impact assessment before deploying AI in public-service decision-making", + "Ensure transparency: citizens must be informed when an administrative decision involves AI processing", + "Maintain human oversight for AI-assisted administrative decisions under French administrative law", + "Publish details about algorithms used in administrative decisions upon request (under the Code des relations entre le public et l’administration)" + ], + "maxPenalty": "No direct financial penalties (internal government framework); judicial review available for affected citizens under administrative law", + "industryTags": ["public-sector"], + "sourceUrl": "https://www.numerique.gouv.fr/", + "lastVerified": "March 2026" + } + ], + "timeline": [ + { + "date": "November 2021", + "description": "France 2030 AI investment strategy launched." + }, + { + "date": "May 2023", + "description": "CNIL publishes AI action plan and guidance on AI training data." + }, + { + "date": "August 2024", + "description": "EU AI Act enters into force, directly applicable in France." + }, + { + "date": "February 2025", + "description": "EU AI Act prohibited practices enforceable in France." + }, + { + "date": "August 2025", + "description": "GPAI model obligations take effect for French providers." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "italy", + "name": "Italy", + "region": "europe", + "regulationCount": 4, + "hash": "sha256-87142eec75c2ba6b6f8fb9a383cb5874843914c80d1a7e9ff85052d1af6f5925", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-87142eec75c2ba6b6f8fb9a383cb5874843914c80d1a7e9ff85052d1af6f5925", + "regulationCount": 4 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/italy", + "flag": "🇮🇹", + "oneLiner": "First EU member state with a standalone national AI law — binding rules on top of the EU AI Act.", + "executiveSummary": "Italy is subject to the EU AI Act, which applies directly as an EU regulation, but it has also become the first EU member state to enact its own comprehensive national AI statute. Law No. 132/2025 (‘Provisions and delegations to the Government on artificial intelligence’) was approved on 23 September 2025 and entered into force on 10 October 2025. It complements the EU AI Act with sector-specific rules for health, work, justice, and public administration, principles on human oversight and transparency, copyright and criminal-law provisions, and a new criminal offence for harmful AI-generated or deepfake content. The Garante per la protezione dei dati personali actively enforces GDPR against AI systems, having previously ordered a temporary block of ChatGPT in Italy. On 10 June 2026 the Council of Ministers gave preliminary approval to the first implementing decrees under Law 132/2025, setting the supervisory split between AgID (notifying authority) and the National Cybersecurity Agency ACN (market surveillance and EU contact point), a sanctions regime calibrated below the EU AI Act ceilings, AI testing environments, and civil-liability rules; these decrees still require parliamentary committee opinions before they become final. Companies operating in Italy must therefore comply with both the EU AI Act and the additional national obligations and delegated measures flowing from Law 132/2025.", + "practicalTakeaway": "If your company operates AI systems in Italy, you must comply with both the EU AI Act and Italy’s national AI law (Law 132/2025). Map where your AI touches health, employment, justice, or public-administration use cases, because the national law adds sector-specific human-oversight and transparency duties on top of the EU baseline. Treat the new criminal offence for harmful deepfake content as a real exposure, and watch for implementing decrees that will flesh out the delegations in the law. The Garante remains one of Europe’s most assertive data-protection authorities on AI, so document your GDPR legal basis for any training data.", + "regulations": [ + { + "name": "EU AI Act (directly applicable in Italy)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "August 2024", + "effectiveDateISO": "2024-08-01", + "dateConfidence": "approximate", + "scope": "All AI systems placed on the Italian/EU market or whose output is used in Italy, with national enforcement supported by the authorities designated under Law 132/2025.", + "obligations": [ + "All EU AI Act obligations apply directly: risk classification, prohibited practices, high-risk conformity assessments, GPAI transparency requirements", + "Italy designated the Agency for Digital Italy (AgID) and the National Cybersecurity Agency (ACN) as national competent authorities under Law 132/2025", + "Market surveillance for AI embedded in regulated products involves existing Italian sectoral regulators" + ], + "maxPenalty": "€35 million or 7% of global annual turnover for prohibited-practice violations (EU AI Act penalties)", + "industryTags": ["general"], + "sourceUrl": "https://eur-lex.europa.eu/eli/reg/2024/1689/oj", + "lastVerified": "June 2026" + }, + { + "name": "Law No. 132/2025 — National AI Law (Disposizioni e deleghe in materia di intelligenza artificiale)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "October 2025", + "effectiveDateISO": "2025-10-01", + "dateConfidence": "approximate", + "scope": "Development, deployment, and use of AI systems in Italy across health, work, justice, public administration, and other sectors, layered on top of the directly applicable EU AI Act.", + "obligations": [ + "Apply principles of human oversight, transparency, traceability, and non-discrimination to AI use in regulated sectors", + "Special rules for AI in healthcare, the workplace, the justice system, and public administration, including human-decision safeguards", + "Transparency and consent requirements for AI use, with specific protections for minors", + "Copyright provisions clarifying protection for AI-assisted creative works and text-and-data-mining boundaries", + "Government delegated to adopt further implementing decrees aligning Italian law with the EU AI Act" + ], + "maxPenalty": "New criminal offence for unlawful dissemination of harmful AI-generated or AI-altered (deepfake) content, punishable by imprisonment of one to five years; aggravating circumstances added to fraud, market-manipulation, and identity-related offences committed via AI", + "industryTags": ["general", "healthcare", "hr-employment", "public-sector"], + "sourceUrl": "https://www.gazzettaufficiale.it/eli/id/2025/09/25/25G00143/sg", + "lastVerified": "June 2026" + }, + { + "name": "Garante guidance and enforcement on AI and data protection", + "type": "sector-regulation", + "status": "in-force", + "effectiveDate": "March 2023", + "effectiveDateISO": "2023-03-01", + "dateConfidence": "approximate", + "scope": "Any organization processing personal data using AI systems in Italy under the GDPR and the Italian Personal Data Protection Code.", + "obligations": [ + "Establish a valid GDPR legal basis before training AI models on personal data", + "Conduct data protection impact assessments for AI systems that process personal data at scale", + "Provide transparency to data subjects about automated processing and respect rights of access, erasure, and objection", + "Implement age-verification and safeguards where AI services may be accessed by minors" + ], + "maxPenalty": "€20 million or 4% of global annual turnover under the GDPR, enforced by the Garante; the authority has previously ordered temporary suspension of AI services in Italy", + "industryTags": ["general"], + "sourceUrl": "https://www.garanteprivacy.it/temi/intelligenza-artificiale", + "lastVerified": "June 2026" + }, + { + "name": "Italian Strategy for Artificial Intelligence (2024–2026)", + "type": "national-strategy", + "status": "in-force", + "effectiveDate": "July 2024", + "effectiveDateISO": "2024-07-01", + "dateConfidence": "approximate", + "scope": "National policy directing research, public administration, enterprise adoption, and skills development for AI in Italy.", + "obligations": [ + "Coordinate public investment in AI research, infrastructure, and talent through dedicated initiatives", + "Promote AI adoption in public administration and small and medium-sized enterprises", + "Support development of Italian-language models and responsible-AI capacity", + "Align national AI deployment with the EU AI Act and the values of human-centred AI" + ], + "maxPenalty": "Not applicable (investment and policy strategy, not a regulatory instrument)", + "industryTags": ["general"], + "sourceUrl": "https://www.agid.gov.it/it/intelligenza-artificiale", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "March 2023", + "description": "Garante orders a temporary block of ChatGPT in Italy over data-protection concerns." + }, + { + "date": "August 2024", + "description": "EU AI Act enters into force, directly applicable in Italy." + }, + { + "date": "February 2025", + "description": "EU AI Act prohibited practices enforceable in Italy." + }, + { + "date": "September 2025", + "description": "Italian Parliament approves Law No. 132/2025, the first national AI statute in an EU member state." + }, + { + "date": "October 2025", + "description": "Law No. 132/2025 enters into force." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "germany", + "name": "Germany", + "region": "europe", + "regulationCount": 5, + "hash": "sha256-3e854def71b83ca326dfcd902b97a6ed3de62deb91ea5268f0ee1dcf77e39e3a", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-3e854def71b83ca326dfcd902b97a6ed3de62deb91ea5268f0ee1dcf77e39e3a", + "regulationCount": 5 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/germany", + "flag": "🇩🇪", + "oneLiner": "EU AI Act applies directly alongside strict federal data protection and sector rules.", + "executiveSummary": "Germany is directly subject to the EU AI Act. As Europe’s largest economy, Germany’s implementation approach carries outsized influence. The federal government published an AI Strategy in 2018 (updated 2020) and an AI Standardization Roadmap through DIN/DKE. Germany’s data protection authorities (Datenschutzkonferenz / DSK) have issued AI-specific guidance on GDPR compliance, particularly around automated decision-making and profiling. The financial regulator BaFin has published expectations for AI in financial services. Germany will designate a national AI Act competent authority and is expected to be among the strictest enforcers. Companies operating in Germany should treat GDPR’s automated-decision-making rules (Article 22) as already binding for AI systems.", + "practicalTakeaway": "If your company deploys AI in Germany, the EU AI Act is your core compliance requirement, and Germany is expected to be among the strictest enforcers. GDPR enforcement through 16 independent state data protection authorities is already active for AI systems. If you operate in financial services, BaFin expects full AI model governance under MaRisk. Adopt the DIN/DKE standardization roadmap as a practical guide to aligning your AI systems with emerging European standards. Designate an AI compliance lead now — Germany’s regulatory culture favours thorough documentation and proactive governance.", + "regulations": [ + { + "name": "EU AI Act (directly applicable in Germany)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "August 2024", + "effectiveDateISO": "2024-08-01", + "dateConfidence": "approximate", + "scope": "All AI systems placed on the German/EU market; Germany will designate BNetzA or a new body as its national competent authority.", + "obligations": [ + "All EU AI Act obligations apply directly: risk classification, prohibited practices, conformity assessments, GPAI rules", + "Germany is expected to designate a national AI authority for market surveillance and enforcement", + "German sector regulators (BaFin, BfArM, Bundesnetzagentur) will enforce AI Act provisions in their domains" + ], + "maxPenalty": "€35 million or 7% of global annual turnover for prohibited-practice violations (EU AI Act penalties)", + "industryTags": ["general"], + "sourceUrl": "https://eur-lex.europa.eu/eli/reg/2024/1689/oj", + "lastVerified": "March 2026" + }, + { + "name": "German AI Strategy (updated 2020)", + "type": "national-strategy", + "status": "in-force", + "effectiveDate": "December 2020", + "effectiveDateISO": "2020-12-01", + "dateConfidence": "approximate", + "scope": "National policy framework for AI research, industry adoption, and governance in Germany.", + "obligations": [ + "Invest €5 billion in AI research and development through 2025, extended with additional funding commitments", + "Establish AI competence centres and support applied AI research at Fraunhofer, Max Planck, and university institutes", + "Promote ‘AI Made in Germany’ quality label emphasizing trustworthiness, safety, and transparency", + "Develop and adopt technical standards for AI systems through DIN/DKE standardization processes" + ], + "maxPenalty": "Not applicable (policy strategy, not a regulatory instrument)", + "industryTags": ["general"], + "sourceUrl": "https://www.ki-strategie-deutschland.de/", + "lastVerified": "March 2026" + }, + { + "name": "AI Standardization Roadmap (DIN/DKE)", + "type": "technical-standard", + "status": "in-force", + "effectiveDate": "December 2022", + "effectiveDateISO": "2022-12-01", + "dateConfidence": "approximate", + "scope": "Voluntary technical standards framework for AI systems developed and deployed in Germany, feeding into European and international standardization.", + "obligations": [ + "Provides a framework for implementing AI quality, safety, and transparency standards aligned with ISO/IEC and CEN/CENELEC", + "Maps existing standards to AI lifecycle stages and recommends adoption for AI system development", + "Identifies gaps where new standards are needed, particularly for testing, auditing, and certification of AI systems", + "Supports EU AI Act compliance by pre-aligning with expected harmonized European standards" + ], + "maxPenalty": "No penalties (voluntary standardization framework); adherence supports EU AI Act conformity assessment", + "industryTags": ["general"], + "sourceUrl": "https://www.din.de/en/innovation-and-research/artificial-intelligence", + "lastVerified": "March 2026" + }, + { + "name": "DSK guidance on AI and GDPR compliance", + "type": "sector-regulation", + "status": "in-force", + "effectiveDate": "May 2024", + "effectiveDateISO": "2024-05-01", + "dateConfidence": "approximate", + "scope": "All organizations in Germany that process personal data using AI, including for model training and automated decision-making.", + "obligations": [ + "Automated decisions with legal or significant effects require explicit safeguards under GDPR Article 22, including the right to human review", + "DPIAs mandatory before deploying AI systems that process personal data at scale or involve profiling", + "Transparency obligations: data subjects must be informed about the existence of automated decision-making, its logic, and significance", + "Purpose limitation: personal data collected for one purpose may not be repurposed for AI training without a fresh legal basis" + ], + "maxPenalty": "€20 million or 4% of global annual turnover under GDPR; German state data protection authorities enforce independently", + "industryTags": ["general"], + "sourceUrl": "https://www.datenschutzkonferenz-online.de/", + "lastVerified": "March 2026" + }, + { + "name": "BaFin guidance on AI in financial services", + "type": "sector-regulation", + "status": "in-force", + "effectiveDate": "June 2021", + "effectiveDateISO": "2021-06-01", + "dateConfidence": "approximate", + "scope": "Financial institutions supervised by BaFin that use AI/ML models in risk management, credit decisions, insurance pricing, or customer-facing services.", + "obligations": [ + "AI/ML models must be documented, validated, and subject to model risk management under MaRisk (Minimum Requirements for Risk Management)", + "Explainability requirements for AI models used in credit and insurance decisions affecting consumers", + "Senior management must be informed about AI model risks and approve deployment of high-impact AI systems", + "Ongoing monitoring and periodic validation of AI model performance and fairness" + ], + "maxPenalty": "Enforcement actions under the German Banking Act and Insurance Supervision Act; fines, conditions on business operations, or licence restrictions", + "industryTags": ["financial-services", "insurance"], + "sourceUrl": "https://www.bafin.de/SharedDocs/Downloads/EN/Aufsichtsrecht/dl_Prinzipienpapier_BDAI_en.html", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "November 2018", + "description": "German Federal Government publishes initial AI Strategy." + }, + { + "date": "December 2020", + "description": "AI Strategy updated with expanded funding commitments." + }, + { + "date": "December 2022", + "description": "DIN/DKE AI Standardization Roadmap (Edition 2) published." + }, + { + "date": "August 2024", + "description": "EU AI Act enters into force, directly applicable in Germany." + }, + { + "date": "August 2025", + "description": "GPAI model obligations take effect; Germany to designate national AI authority." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "sweden", + "name": "Sweden", + "region": "europe", + "regulationCount": 4, + "hash": "sha256-7fc2cc92296cf8edd0a4143693738cd05da71accb0a93dddd74fd6a8c20fe9e2", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-7fc2cc92296cf8edd0a4143693738cd05da71accb0a93dddd74fd6a8c20fe9e2", + "regulationCount": 4 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/sweden", + "flag": "🇸🇪", + "oneLiner": "EU AI Act applies; national focus on responsible AI in public services and healthcare.", + "executiveSummary": "Sweden is directly subject to the EU AI Act. The country has adopted a national AI approach emphasizing responsible innovation, particularly in the public sector and healthcare. The Swedish Authority for Privacy Protection (IMY) has issued guidance on AI and GDPR compliance, including specific advice on automated decision-making. The government’s AI strategy, coordinated through the AI Sweden programme, focuses on practical AI adoption while maintaining high ethical standards. Sweden has also been an early adopter of AI in public services, with guidelines from the Agency for Digital Government (DIGG) on algorithmic transparency.", + "practicalTakeaway": "If your company operates AI systems in Sweden, the EU AI Act is your primary legal obligation. IMY enforces GDPR actively and has issued AI-specific guidance — ensure you have DPIAs in place for any AI processing of personal data. If you provide AI solutions to the Swedish public sector, the DIGG guidelines set expectations for transparency and human oversight. Sweden’s AI ecosystem is collaborative, and early engagement with AI Sweden can provide useful compliance and best-practice resources.", + "regulations": [ + { + "name": "EU AI Act (directly applicable in Sweden)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "August 2024", + "effectiveDateISO": "2024-08-01", + "dateConfidence": "approximate", + "scope": "All AI systems placed on the Swedish/EU market; Sweden to designate national competent authority for AI Act enforcement.", + "obligations": [ + "All EU AI Act obligations apply directly: risk classification, prohibited practices, conformity assessments, GPAI rules", + "Sweden is expected to designate IMY or a new authority for AI Act enforcement and market surveillance", + "Swedish sector regulators will enforce AI Act provisions within their existing mandates" + ], + "maxPenalty": "€35 million or 7% of global annual turnover for prohibited-practice violations (EU AI Act penalties)", + "industryTags": ["general"], + "sourceUrl": "https://eur-lex.europa.eu/eli/reg/2024/1689/oj", + "lastVerified": "March 2026" + }, + { + "name": "Swedish National Approach to AI", + "type": "national-strategy", + "status": "in-force", + "effectiveDate": "May 2018", + "effectiveDateISO": "2018-05-01", + "dateConfidence": "approximate", + "scope": "National policy framework for AI development, adoption, and governance across Sweden.", + "obligations": [ + "Position Sweden as a leader in responsible AI innovation through public-private partnerships", + "AI Sweden programme serves as the national centre for applied AI, coordinating research and industry adoption", + "Focus on AI adoption in healthcare, public services, and industry with strong ethical guardrails", + "Invest in AI skills development and workforce transition programmes" + ], + "maxPenalty": "Not applicable (national strategy, not a regulatory instrument)", + "industryTags": ["general"], + "sourceUrl": "https://www.government.se/information-material/2018/05/national-approach-to-artificial-intelligence/", + "lastVerified": "March 2026" + }, + { + "name": "IMY guidance on AI and data protection", + "type": "sector-regulation", + "status": "in-force", + "effectiveDate": "March 2024", + "effectiveDateISO": "2024-03-01", + "dateConfidence": "approximate", + "scope": "All organizations in Sweden processing personal data using AI systems, including AI model training and automated decision-making.", + "obligations": [ + "Conduct DPIAs before deploying AI that processes personal data at scale or makes automated decisions", + "Ensure GDPR Article 22 safeguards for automated individual decision-making, including the right to human review", + "Establish valid legal basis for processing personal data for AI training purposes", + "Maintain transparency about how AI uses personal data in privacy policies and notices" + ], + "maxPenalty": "€20 million or 4% of global annual turnover under GDPR, enforced by IMY", + "industryTags": ["general"], + "sourceUrl": "https://www.imy.se/en/", + "lastVerified": "March 2026" + }, + { + "name": "DIGG guidelines on AI in public administration", + "type": "voluntary-guideline", + "status": "in-force", + "effectiveDate": "June 2023", + "effectiveDateISO": "2023-06-01", + "dateConfidence": "approximate", + "scope": "Swedish government agencies and municipalities using AI in public-service delivery and administrative decision-making.", + "obligations": [ + "Government agencies must assess AI systems for legal compliance, ethical implications, and societal impact before deployment", + "Transparency requirements: publish information about algorithms used in public decision-making processes", + "Ensure human oversight for AI-assisted administrative decisions, particularly those affecting individual rights", + "Maintain audit trails and documentation for AI-supported government decisions" + ], + "maxPenalty": "No direct penalties (government guidelines); non-compliance may result in judicial review of administrative decisions", + "industryTags": ["public-sector"], + "sourceUrl": "https://www.digg.se/en", + "lastVerified": "March 2026" + } + ], + "timeline": [ + { + "date": "May 2018", + "description": "Swedish government publishes National Approach to Artificial Intelligence." + }, + { + "date": "June 2023", + "description": "DIGG publishes guidelines on AI in public administration." + }, + { + "date": "March 2024", + "description": "IMY issues updated guidance on AI and GDPR compliance." + }, + { + "date": "August 2024", + "description": "EU AI Act enters into force, directly applicable in Sweden." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "canada", + "name": "Canada", + "region": "north-america", + "regulationCount": 5, + "hash": "sha256-e88a8dae638b31c9b38294c02b60163300347213a7709691f153dab1d0b54ba9", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-e88a8dae638b31c9b38294c02b60163300347213a7709691f153dab1d0b54ba9", + "regulationCount": 5 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/canada", + "flag": "🇨🇦", + "oneLiner": "No federal AI Act — a new national 'AI for All' strategy, plus the Treasury Board directive and a voluntary code, fill the gap.", + "executiveSummary": "Canada’s proposed Artificial Intelligence and Data Act (AIDA), introduced as Part 3 of Bill C-27, died when Parliament was prorogued in January 2025, and there is still no comprehensive federal AI law. On 4 June 2026 Prime Minister Mark Carney launched ‘AI for All,’ a five-year national AI strategy led by the Minister of AI and Digital Innovation, built around public trust, economic opportunity, and Canadian sovereignty, with programs for AI literacy, SME adoption, AI-related jobs, and compute and infrastructure build-out; it promises future legislation but does not itself enact one. In the meantime, the Treasury Board’s Directive on Automated Decision-Making (DADM) has been in force since 2019 and mandates algorithmic impact assessments for all federal government AI systems. ISED’s Voluntary Code of Conduct on the Responsible Development and Management of Advanced Generative AI Systems provides a non-binding framework for industry. Canada’s privacy legislation (PIPEDA and its proposed replacement, the Consumer Privacy Protection Act) applies to AI use of personal data. Provincial privacy laws in Quebec, Alberta, and British Columbia add additional requirements.", + "practicalTakeaway": "If your company builds or deploys AI systems in Canada, the absence of a federal AI law does not mean the absence of obligations. PIPEDA applies to all commercial use of personal data in AI systems. If you sell to the Canadian federal government, the DADM’s Algorithmic Impact Assessment is mandatory and will determine whether your system can be deployed. Adopt the ISED Voluntary Code of Conduct to demonstrate good practice. Watch for AIDA’s potential reintroduction and Quebec’s Law 25 data-protection requirements, which add additional obligations for AI using personal information of Quebec residents.", + "regulations": [ + { + "name": "Artificial Intelligence and Data Act (AIDA — stalled)", + "type": "draft-bill", + "status": "proposed", + "effectiveDate": "Date TBD", + "effectiveDateISO": null, + "dateConfidence": "unknown", + "scope": "Intended to cover high-impact AI systems across the Canadian economy with mandatory safety and transparency requirements.", + "obligations": [ + "Would have required organizations to assess and mitigate risks of high-impact AI systems before deployment", + "Would have mandated transparency measures including plain-language descriptions of AI system capabilities and limitations", + "Would have required monitoring for bias and discriminatory outcomes in AI systems", + "Would have established criminal penalties for reckless or intentional AI-caused serious harm", + "Bill died when Parliament prorogued in January 2025; reintroduction timing uncertain" + ], + "maxPenalty": "Would have included fines up to CAD $25 million or 5% of global revenue and criminal penalties including imprisonment", + "industryTags": ["general"], + "sourceUrl": "https://ised-isde.canada.ca/site/innovation-better-canada/en/artificial-intelligence-and-data-act", + "lastVerified": "March 2026" + }, + { + "name": "AI for All — National AI Strategy", + "type": "national-strategy", + "status": "policy-only", + "effectiveDate": "June 2026", + "effectiveDateISO": "2026-06-01", + "dateConfidence": "approximate", + "scope": "Whole-of-economy strategy for AI adoption, investment, infrastructure, and responsible use across Canadian public and private sectors.", + "obligations": [ + "Five-year national strategy built around public trust, economic opportunity, and Canadian sovereignty", + "Programs for AI literacy, SME adoption support, and AI-related jobs and placements", + "Compute and infrastructure build-out, including sovereign compute capacity", + "Commits to future AI and privacy legislation, but does not itself create binding AI obligations" + ], + "maxPenalty": "None — strategy and funding framework, not a binding statute", + "industryTags": ["general"], + "sourceUrl": "https://www.pm.gc.ca/en/news/news-releases/2026/06/04/prime-minister-carney-launches-ai-all-canadas-new-national-artificial", + "lastVerified": "June 2026" + }, + { + "name": "Treasury Board Directive on Automated Decision-Making (DADM)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "April 2019", + "effectiveDateISO": "2019-04-01", + "dateConfidence": "approximate", + "scope": "All Canadian federal government departments and agencies using automated systems to make or assist administrative decisions affecting individuals.", + "obligations": [ + "Complete an Algorithmic Impact Assessment (AIA) before deploying any automated decision system", + "Classify AI systems by impact level (I–IV) based on the assessment, with escalating requirements for higher tiers", + "Provide notice to affected individuals that a decision was made by or assisted by an automated system", + "Ensure meaningful human review is available for decisions at higher impact levels", + "Document and make public the assessment results; maintain ongoing monitoring and recalibration" + ], + "maxPenalty": "Internal compliance mechanism enforced by Treasury Board Secretariat; no external fines but non-compliance findings can halt system deployment", + "industryTags": ["public-sector"], + "sourceUrl": "https://www.tbs-sct.canada.ca/pol/doc-eng.aspx?id=32592", + "lastVerified": "March 2026" + }, + { + "name": "ISED Voluntary Code of Conduct for Generative AI", + "type": "voluntary-guideline", + "status": "voluntary", + "effectiveDate": "September 2023", + "effectiveDateISO": "2023-09-01", + "dateConfidence": "approximate", + "scope": "Companies developing or deploying advanced generative AI systems in Canada; adoption is voluntary.", + "obligations": [ + "Conduct safety assessments for generative AI systems before public release", + "Implement measures to mitigate harms including bias, disinformation, and harmful content generation", + "Provide transparency about AI system capabilities, limitations, and intended uses", + "Label or watermark AI-generated content where technically feasible", + "Engage in ongoing monitoring and red-teaming of generative AI systems" + ], + "maxPenalty": "No penalties (voluntary code); signatories commit publicly and non-compliance may affect reputation", + "industryTags": ["general"], + "sourceUrl": "https://ised-isde.canada.ca/site/ised/en/voluntary-code-conduct-responsible-development-and-management-advanced-generative-ai-systems", + "lastVerified": "March 2026" + }, + { + "name": "PIPEDA — AI provisions", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "April 2000", + "effectiveDateISO": "2000-04-01", + "dateConfidence": "approximate", + "scope": "Private-sector organizations collecting, using, or disclosing personal information in the course of commercial activity, including for AI training and deployment.", + "obligations": [ + "Obtain meaningful consent before collecting personal information for AI training or processing", + "Limit collection to what is necessary for the stated purpose (data minimization)", + "Individuals have the right to know how their personal information is used by AI systems and to challenge automated decisions", + "Implement safeguards appropriate to the sensitivity of personal information used in AI systems" + ], + "maxPenalty": "OPC can seek Federal Court orders; penalties under the proposed CPPA replacement would be up to CAD $25 million or 5% of global revenue", + "industryTags": ["general"], + "sourceUrl": "https://www.priv.gc.ca/en/privacy-topics/privacy-laws-in-canada/the-personal-information-protection-and-electronic-documents-act-pipeda/", + "lastVerified": "March 2026" + } + ], + "timeline": [ + { + "date": "April 2019", + "description": "Treasury Board Directive on Automated Decision-Making takes effect for federal agencies." + }, + { + "date": "June 2022", + "description": "Bill C-27 (including AIDA) introduced in Parliament." + }, + { + "date": "September 2023", + "description": "ISED publishes Voluntary Code of Conduct for Generative AI." + }, + { + "date": "January 2025", + "description": "Parliament prorogued; Bill C-27 and AIDA die on the order paper." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "india", + "name": "India", + "region": "asia", + "regulationCount": 4, + "hash": "sha256-b3acab824e4267b89db66751b062d315cb398d6979e42fea49965a7dd787a55d", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-b3acab824e4267b89db66751b062d315cb398d6979e42fea49965a7dd787a55d", + "regulationCount": 4 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/india", + "flag": "🇮🇳", + "oneLiner": "No binding AI law yet — advisory issued with focus on labeling and deepfake prevention.", + "executiveSummary": "India does not have a comprehensive AI-specific law. The government’s approach has been pro-innovation, with NITI Aayog’s National Strategy for AI (2018) and Responsible AI principles setting the policy direction. The Ministry of Electronics and IT (MeitY) issued an advisory in March 2024 requiring platforms to label AI-generated content and obtain government approval before launching under-tested AI models, though enforcement mechanisms remain unclear. The proposed Digital India Act is expected to include AI-specific provisions but has not yet been introduced. SEBI has issued guidance on AI use in financial markets. India’s Digital Personal Data Protection Act (2023) applies to AI systems processing personal data but does not contain AI-specific provisions. The regulatory environment is evolving rapidly.", + "practicalTakeaway": "If your company operates AI systems in India, the regulatory environment is light but tightening. The MeitY advisory on AI content labeling should be followed immediately. The DPDPA will impose GDPR-style data protection obligations when its rules are notified — prepare by auditing your data collection and consent practices now. If you serve the Indian financial market, SEBI’s algorithmic trading rules are already binding. Monitor the Digital India Act’s progress closely, as it is expected to introduce India’s first comprehensive AI governance framework.", + "regulations": [ + { + "name": "NITI Aayog National Strategy for AI and Responsible AI Principles", + "type": "national-strategy", + "status": "policy-only", + "effectiveDate": "June 2018", + "effectiveDateISO": "2018-06-01", + "dateConfidence": "approximate", + "scope": "National policy framework for AI development, ecosystem building, and ethical deployment across India.", + "obligations": [ + "Identifies five priority sectors for AI deployment: healthcare, agriculture, education, smart cities, and transportation", + "Establishes seven Responsible AI principles: safety and reliability, equality, inclusivity, privacy, transparency, accountability, and positive human values", + "Recommends creation of sector-specific AI centres of excellence and research institutions", + "Proposes regulatory sandbox approach for testing AI applications in controlled environments" + ], + "maxPenalty": "Not applicable (policy document, not a regulatory instrument)", + "industryTags": ["general"], + "sourceUrl": "https://www.niti.gov.in/sites/default/files/2023-03/National-Strategy-for-Artificial-Intelligence.pdf", + "lastVerified": "March 2026" + }, + { + "name": "MeitY advisory on AI-generated content and platform obligations", + "type": "voluntary-guideline", + "status": "in-force", + "effectiveDate": "March 2024", + "effectiveDateISO": "2024-03-01", + "dateConfidence": "approximate", + "scope": "Platforms and intermediaries deploying AI or generative AI services accessible to Indian users.", + "obligations": [ + "Label all AI-generated content so users can distinguish it from human-created content", + "Platforms must ensure AI-generated outputs do not violate existing IT Act provisions on harmful content", + "Obtain government approval before launching AI models that are under-tested or unreliable on the Indian market", + "Implement safeguards to prevent AI-generated deepfakes and misinformation" + ], + "maxPenalty": "Enforcement under the IT Act 2000 and IT Rules 2021; intermediary safe-harbour protections may be revoked for non-compliance", + "industryTags": ["general"], + "sourceUrl": "https://www.meity.gov.in/", + "lastVerified": "March 2026" + }, + { + "name": "Digital Personal Data Protection Act (DPDPA) 2023", + "type": "binding-law", + "status": "passed-not-active", + "effectiveDate": "Expected 2025–2026", + "effectiveDateISO": "2025-01-01", + "dateConfidence": "approximate", + "scope": "All entities processing digital personal data of individuals in India, including for AI model training and automated decision-making.", + "obligations": [ + "Obtain consent before processing personal data for AI training or deployment, with specific consent for certain categories", + "Implement data-localization requirements for certain categories of personal data", + "Appoint a data protection officer and maintain records of processing activities", + "Provide individuals with rights to access, correction, and erasure of personal data used in AI systems", + "Data fiduciaries must implement reasonable security safeguards for personal data" + ], + "maxPenalty": "Penalties up to INR 250 crore (~$30 million) per violation; Data Protection Board to enforce", + "industryTags": ["general"], + "sourceUrl": "https://www.meity.gov.in/data-protection-framework", + "lastVerified": "March 2026" + }, + { + "name": "SEBI guidance on AI/ML in securities markets", + "type": "sector-regulation", + "status": "in-force", + "effectiveDate": "January 2019", + "effectiveDateISO": "2019-01-01", + "dateConfidence": "approximate", + "scope": "SEBI-regulated entities (stock exchanges, brokers, mutual funds, portfolio managers) using AI/ML for trading, advisory, or compliance functions.", + "obligations": [ + "Register and seek approval for AI/ML-based trading algorithms with stock exchanges", + "Maintain audit trails for algorithmic trading decisions including model logic and parameters", + "Implement risk controls and circuit breakers for AI-driven automated trading systems", + "Ensure investor protection through disclosure of AI/ML use in advisory and portfolio management services" + ], + "maxPenalty": "Penalties under SEBI Act including monetary penalties, suspension of registration, and prosecution for market manipulation", + "industryTags": ["financial-services"], + "sourceUrl": "https://www.sebi.gov.in/", + "lastVerified": "March 2026" + } + ], + "timeline": [ + { + "date": "June 2018", + "description": "NITI Aayog publishes National Strategy for Artificial Intelligence." + }, + { + "date": "August 2023", + "description": "Digital Personal Data Protection Act enacted." + }, + { + "date": "March 2024", + "description": "MeitY issues advisory on AI content labeling and platform obligations." + }, + { + "date": "2025–2026", + "description": "DPDPA rules expected to be notified; Digital India Act may include AI provisions." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "japan", + "name": "Japan", + "region": "asia", + "regulationCount": 4, + "hash": "sha256-7c5fdea263b9a9915adea352bce5becefdb2604553090c70b276c6017b919f05", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-7c5fdea263b9a9915adea352bce5becefdb2604553090c70b276c6017b919f05", + "regulationCount": 4 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/japan", + "flag": "🇯🇵", + "oneLiner": "Voluntary AI guidelines with sector-specific enforcement — Basic AI Act passed in 2025.", + "executiveSummary": "Japan has taken a pragmatic, innovation-friendly approach to AI governance. The AI Guidelines for Business (published by METI in 2024, building on earlier Social Principles of Human-Centric AI from 2019) provide a comprehensive voluntary framework for AI developers and deployers. Japan passed its Basic Act on the Advancement of Responsible AI in May 2025, establishing fundamental principles and a coordination framework but stopping short of prescriptive mandates. The FSA has issued guidance on AI use in financial services. Japan’s Act on the Protection of Personal Information (APPI) applies to AI systems processing personal data. Japan is a G7 Hiroshima AI Process signatory and an active participant in international AI governance standards.", + "practicalTakeaway": "If your company develops or deploys AI in Japan, the regulatory environment is principled but not prescriptive. Adopt the METI AI Guidelines for Business as your governance baseline — while voluntary, they reflect government expectations and will likely inform future sector-specific rules under the Basic AI Act. Comply with APPI for all AI processing of personal data, and monitor FSA guidance if you operate in financial services. Japan’s approach rewards early alignment with principles, and companies that demonstrate responsible AI practices will have a smoother path as governance becomes more structured.", + "regulations": [ + { + "name": "AI Guidelines for Business (METI, 2024)", + "type": "voluntary-guideline", + "status": "in-force", + "effectiveDate": "April 2024", + "effectiveDateISO": "2024-04-01", + "dateConfidence": "approximate", + "scope": "All businesses developing, providing, or using AI systems in Japan; voluntary but widely referenced by regulators and industry.", + "obligations": [ + "Conduct risk assessments proportionate to the AI system’s potential impact on individuals and society", + "Ensure transparency: provide clear information about AI system capabilities, limitations, and decision-making logic to stakeholders", + "Implement fairness measures to prevent discriminatory outcomes from AI systems", + "Maintain human oversight mechanisms appropriate to the risk level of the AI application", + "Establish accountability structures with designated responsible persons for AI governance" + ], + "maxPenalty": "No direct penalties (voluntary guidelines); adoption demonstrates alignment with government expectations and may influence regulatory treatment", + "industryTags": ["general"], + "sourceUrl": "https://www.meti.go.jp/english/policy/economy/ai-governance/index.html", + "lastVerified": "March 2026" + }, + { + "name": "Basic Act on the Advancement of Responsible AI", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "May 2025", + "effectiveDateISO": "2025-05-01", + "dateConfidence": "approximate", + "scope": "National framework law establishing fundamental principles for AI development and use across Japan.", + "obligations": [ + "Establishes fundamental principles for responsible AI: human-centricity, safety, fairness, transparency, and accountability", + "Creates a government coordination body for AI policy across ministries", + "Requires the government to develop and publish sector-specific AI guidelines aligned with the basic principles", + "Mandates regular review of AI policy and governance measures to keep pace with technological development", + "Encourages international cooperation on AI governance standards" + ], + "maxPenalty": "Framework law without direct penalties; sector-specific implementing guidelines may include enforcement mechanisms", + "industryTags": ["general"], + "sourceUrl": "https://www8.cao.go.jp/cstp/ai/ai_act/ai_act.html", + "lastVerified": "March 2026" + }, + { + "name": "FSA guidance on AI in financial services", + "type": "sector-regulation", + "status": "in-force", + "effectiveDate": "June 2023", + "effectiveDateISO": "2023-06-01", + "dateConfidence": "approximate", + "scope": "Financial institutions supervised by Japan’s Financial Services Agency using AI/ML for credit decisions, trading, risk management, or customer services.", + "obligations": [ + "Financial institutions must ensure AI-driven decisions in lending and insurance are explainable to customers", + "Implement robust model risk management frameworks for AI/ML models used in financial services", + "Maintain fairness: AI models must not produce discriminatory outcomes in credit, insurance, or customer segmentation", + "Report to FSA on significant AI-related incidents or unexpected model behaviour affecting customers" + ], + "maxPenalty": "Enforcement under existing financial regulatory framework; FSA can issue business improvement orders, restrict operations, or revoke licences", + "industryTags": ["financial-services"], + "sourceUrl": "https://www.fsa.go.jp/en/", + "lastVerified": "March 2026" + }, + { + "name": "Act on the Protection of Personal Information (APPI) — AI provisions", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "April 2022", + "effectiveDateISO": "2022-04-01", + "dateConfidence": "approximate", + "scope": "All entities handling personal information of individuals in Japan, including for AI training and inference.", + "obligations": [ + "Obtain consent for collecting and using personal data for AI training, with enhanced consent requirements for sensitive information", + "Individuals have rights to disclosure, correction, and cessation of use of their personal data in AI systems", + "Cross-border data transfers for AI model training require consent or confirmation that the receiving country has adequate data protection", + "Pseudonymized data can be processed for AI research under relaxed conditions, but re-identification is prohibited" + ], + "maxPenalty": "Fines up to JPY 100 million (~$670,000) for organizations; PPC can issue orders and refer violations for criminal prosecution", + "industryTags": ["general"], + "sourceUrl": "https://www.ppc.go.jp/en/", + "lastVerified": "March 2026" + } + ], + "timeline": [ + { + "date": "March 2019", + "description": "Government publishes Social Principles of Human-Centric AI." + }, + { + "date": "April 2022", + "description": "Amended APPI takes effect with enhanced data protection provisions." + }, + { + "date": "April 2024", + "description": "METI publishes comprehensive AI Guidelines for Business." + }, + { + "date": "May 2025", + "description": "Basic Act on the Advancement of Responsible AI enacted." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "singapore", + "name": "Singapore", + "region": "asia", + "regulationCount": 5, + "hash": "sha256-d4f55537147885e627e2a9fdb9ee9822cac8e5d69cec1aa3ab48e0076875035a", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-d4f55537147885e627e2a9fdb9ee9822cac8e5d69cec1aa3ab48e0076875035a", + "regulationCount": 5 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/singapore", + "flag": "🇸🇬", + "oneLiner": "Model AI Governance Framework sets regional benchmark — voluntary but widely adopted.", + "executiveSummary": "Singapore has established one of the most mature AI governance ecosystems in Asia-Pacific, primarily through voluntary frameworks. The Model AI Governance Framework (2019, updated 2020) provides comprehensive guidance on responsible AI deployment. AI Verify, launched in 2022 as an AI governance testing framework and toolkit, enables organizations to validate their AI systems against governance principles. The Monetary Authority of Singapore (MAS) has issued the FEAT (Fairness, Ethics, Accountability, Transparency) principles for AI in financial services. The National AI Strategy 2.0 (2023) sets ambitious goals for AI adoption. Singapore’s PDPA (Personal Data Protection Act) applies to AI use of personal data. While no binding AI-specific law exists, Singapore’s frameworks are widely adopted across ASEAN and serve as a de facto regional standard.", + "practicalTakeaway": "If your company deploys AI in Singapore, adopt the Model AI Governance Framework as your baseline — it is the regional benchmark. Use AI Verify to test and document your AI systems’ governance compliance, generating evidence that satisfies both regulators and enterprise customers. If you operate in financial services, MAS FEAT principles are effectively mandatory expectations. Comply with PDPA for all personal data used in AI systems. Singapore’s voluntary framework is widely adopted and serves as the de facto standard across ASEAN, so compliance here positions you well for the broader Southeast Asian market.", + "regulations": [ + { + "name": "Model AI Governance Framework (2nd Edition)", + "type": "voluntary-guideline", + "status": "voluntary", + "effectiveDate": "January 2020", + "effectiveDateISO": "2020-01-01", + "dateConfidence": "approximate", + "scope": "All organizations deploying AI systems in Singapore; voluntary but widely adopted and referenced by industry and regulators across ASEAN.", + "obligations": [ + "Establish internal AI governance structures with clear accountability for AI decision-making", + "Conduct risk assessments for AI systems based on the probability and severity of potential harm", + "Ensure AI decisions are explainable: provide meaningful information about AI-driven outcomes to affected individuals", + "Implement human oversight proportionate to the risk and impact of the AI application", + "Maintain data quality and manage bias throughout the AI lifecycle" + ], + "maxPenalty": "No penalties (voluntary framework); widely considered a baseline expectation for responsible AI deployment in Singapore", + "industryTags": ["general"], + "sourceUrl": "https://www.pdpc.gov.sg/help-and-resources/2020/01/model-ai-governance-framework", + "lastVerified": "March 2026" + }, + { + "name": "AI Verify — AI Governance Testing Framework", + "type": "technical-standard", + "status": "in-force", + "effectiveDate": "May 2022", + "effectiveDateISO": "2022-05-01", + "dateConfidence": "approximate", + "scope": "AI developers and deployers seeking to validate AI systems against governance principles through standardized testing.", + "obligations": [ + "Provides a toolkit for testing AI systems against 11 governance principles including fairness, explainability, and robustness", + "Generates testing reports that can be shared with stakeholders, regulators, and customers as evidence of AI governance", + "Open-source foundation allows customization and integration into existing development pipelines", + "AI Verify Foundation (established 2023) coordinates international community development and adoption" + ], + "maxPenalty": "No penalties (voluntary testing framework); adoption demonstrates governance maturity to regulators and customers", + "industryTags": ["general"], + "sourceUrl": "https://aiverifyfoundation.sg/", + "lastVerified": "March 2026" + }, + { + "name": "MAS FEAT Principles — Fairness, Ethics, Accountability, Transparency", + "type": "sector-regulation", + "status": "in-force", + "effectiveDate": "November 2018", + "effectiveDateISO": "2018-11-01", + "dateConfidence": "approximate", + "scope": "Financial institutions regulated by MAS using AI/ML for customer-facing decisions, risk management, or compliance functions.", + "obligations": [ + "Ensure AI-driven financial decisions (credit, insurance, investment) are fair and do not produce systematically biased outcomes", + "Implement explainability for AI models: customers must be able to understand the basis of AI-driven financial decisions", + "Establish accountability with designated senior management responsible for AI governance in financial services", + "Conduct regular validation and monitoring of AI models for accuracy, fairness, and drift", + "Maintain records of AI model development, validation, and deployment decisions" + ], + "maxPenalty": "Enforcement under MAS’s existing supervisory framework; MAS can issue directions, restrict activities, or revoke licences", + "industryTags": ["financial-services"], + "sourceUrl": "https://www.mas.gov.sg/publications/monographs-or-information-paper/2018/feat", + "lastVerified": "March 2026" + }, + { + "name": "National AI Strategy 2.0", + "type": "national-strategy", + "status": "in-force", + "effectiveDate": "December 2023", + "effectiveDateISO": "2023-12-01", + "dateConfidence": "approximate", + "scope": "National policy framework for AI ecosystem development, adoption, and governance in Singapore.", + "obligations": [ + "Targets 15 areas of activity across AI infrastructure, talent, industry adoption, and governance", + "Aims to position Singapore as a global hub for AI development, testing, and deployment", + "Promotes trusted AI through governance frameworks, standards, and international cooperation", + "Invests in AI compute infrastructure and talent pipeline to support the AI ecosystem" + ], + "maxPenalty": "Not applicable (national strategy, not a regulatory instrument)", + "industryTags": ["general"], + "sourceUrl": "https://www.smartnation.gov.sg/initiatives/national-ai-strategy/", + "lastVerified": "March 2026" + }, + { + "name": "Personal Data Protection Act (PDPA) — AI provisions", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "February 2021", + "effectiveDateISO": "2021-02-01", + "dateConfidence": "approximate", + "scope": "All organizations collecting, using, or disclosing personal data in Singapore, including for AI model training and deployment.", + "obligations": [ + "Obtain consent before collecting personal data for AI training, unless a recognized exception applies (e.g., legitimate interests, business improvement)", + "Implement data protection by design: build privacy safeguards into AI systems from the design stage", + "Data breach notification: report significant breaches to PDPC within 3 days and notify affected individuals", + "Individuals have the right to access and correct personal data held by organizations, including data used in AI systems" + ], + "maxPenalty": "Fines up to SGD $1 million or 10% of annual turnover in Singapore (whichever is higher) per violation", + "industryTags": ["general"], + "sourceUrl": "https://www.pdpc.gov.sg/overview-of-pdpa/the-legislation/personal-data-protection-act", + "lastVerified": "March 2026" + } + ], + "timeline": [ + { + "date": "November 2018", + "description": "MAS publishes FEAT Principles for AI in financial services." + }, + { + "date": "January 2020", + "description": "Model AI Governance Framework (2nd Edition) published." + }, + { + "date": "May 2022", + "description": "AI Verify governance testing toolkit launched." + }, + { + "date": "December 2023", + "description": "National AI Strategy 2.0 launched with expanded scope." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "south-korea", + "name": "South Korea", + "region": "asia", + "regulationCount": 3, + "hash": "sha256-59502445694a442de87dd0be24103ea157bd710fed91051aa93fc6c915f3f571", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-59502445694a442de87dd0be24103ea157bd710fed91051aa93fc6c915f3f571", + "regulationCount": 3 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/south-korea", + "flag": "🇰🇷", + "oneLiner": "Framework Act on AI in force since January 2026 — first comprehensive AI law in Asia-Pacific.", + "executiveSummary": "South Korea enacted the Framework Act on Artificial Intelligence in December 2024, which entered into force on January 22, 2026. This makes South Korea the first country in Asia-Pacific with a comprehensive, binding AI law. The Act establishes AI impact assessments, transparency obligations, and oversight mechanisms for high-risk AI systems. It creates the National AI Committee for policy coordination. South Korea’s PIPA (Personal Information Protection Act) was amended in 2023 with AI-specific provisions covering automated decision-making. The Korea Communications Commission and PIPC (Personal Information Protection Commission) share oversight responsibilities. South Korea’s approach combines binding requirements for high-risk AI with support for innovation in lower-risk applications.", + "practicalTakeaway": "If your company operates AI systems in South Korea, the Framework Act on AI is now in force and creates binding obligations for high-risk AI. Conduct AI impact assessments for any high-risk applications (healthcare, employment, public safety) before deployment. The PIPA amendments already give individuals the right to refuse solely automated decisions, so implement human review mechanisms. South Korea is the first Asian country with a comprehensive AI law, and its approach will likely influence regulation across the region. Budget for compliance with both the AI Framework Act and PIPA’s enhanced automated-decision-making provisions.", + "regulations": [ + { + "name": "Framework Act on Artificial Intelligence", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "January 2026", + "effectiveDateISO": "2026-01-01", + "dateConfidence": "approximate", + "scope": "AI systems developed, deployed, or used in South Korea, with enhanced requirements for high-risk and high-impact AI.", + "obligations": [ + "Conduct AI impact assessments for high-risk AI systems before deployment, covering safety, human rights, and societal impact", + "Implement transparency measures: disclose AI use to affected individuals and provide information about AI system operation", + "Establish the National AI Committee to coordinate AI policy, standards, and oversight across government", + "High-risk AI applications (healthcare, public safety, employment, criminal justice) subject to enhanced oversight and documentation requirements", + "Promote trustworthy AI development through certification and standardization programmes" + ], + "maxPenalty": "Framework law establishes principles with sector-specific enforcement mechanisms; detailed penalty regulations being developed", + "industryTags": ["general"], + "sourceUrl": "https://www.law.go.kr/", + "lastVerified": "March 2026" + }, + { + "name": "PIPA amendments — automated decision-making provisions", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "March 2024", + "effectiveDateISO": "2024-03-01", + "dateConfidence": "approximate", + "scope": "All organizations processing personal information of individuals in South Korea, including through AI and automated decision-making systems.", + "obligations": [ + "Individuals have the right to refuse decisions made solely by automated processing that have significant effects on their rights", + "Organizations must provide explanations of automated decisions when requested by affected individuals", + "Enhanced consent requirements for profiling and automated processing of personal information", + "Data protection impact assessments required for large-scale automated processing of personal information", + "The PIPC oversees compliance and can investigate AI-related data protection complaints" + ], + "maxPenalty": "Fines up to 3% of related revenue or KRW 2 billion (~$1.5 million); criminal penalties including imprisonment for serious violations", + "industryTags": ["general"], + "sourceUrl": "https://www.pipc.go.kr/eng/index.do", + "lastVerified": "March 2026" + }, + { + "name": "National AI Strategy and Ethics Guidelines", + "type": "national-strategy", + "status": "in-force", + "effectiveDate": "December 2019", + "effectiveDateISO": "2019-12-01", + "dateConfidence": "approximate", + "scope": "National policy framework for AI development, industry adoption, and ethical governance in South Korea.", + "obligations": [ + "Invest in AI research and development infrastructure including national AI computing centres", + "Develop AI talent through education programmes and international recruitment", + "Establish AI ethics guidelines covering human dignity, fairness, transparency, and safety", + "Promote AI adoption across key industries including manufacturing, healthcare, and public services" + ], + "maxPenalty": "Not applicable (national strategy, not a regulatory instrument)", + "industryTags": ["general"], + "sourceUrl": "https://www.msit.go.kr/eng/index.do", + "lastVerified": "March 2026" + } + ], + "timeline": [ + { + "date": "December 2019", + "description": "Government publishes National AI Strategy." + }, + { + "date": "March 2024", + "description": "PIPA amendments with automated decision-making provisions take effect." + }, + { + "date": "December 2024", + "description": "Framework Act on Artificial Intelligence enacted by National Assembly." + }, + { + "date": "January 2026", + "description": "Framework Act on AI enters into force." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "israel", + "name": "Israel", + "region": "asia", + "regulationCount": 3, + "hash": "sha256-181565bc7408a16a5800613270999ad692c99b42d9da0ebf68fbf4c4c1e31df8", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-181565bc7408a16a5800613270999ad692c99b42d9da0ebf68fbf4c4c1e31df8", + "regulationCount": 3 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/israel", + "flag": "🇮🇱", + "oneLiner": "Policy-based approach with no binding AI law — relies on existing regulation and innovation focus.", + "executiveSummary": "Israel has no dedicated AI legislation and has favoured a light-touch, innovation-centric approach. The government published an AI policy framework through the Israel Innovation Authority and the Ministry of Innovation, Science and Technology. The Privacy Protection Authority (PPA) has issued guidance on AI and data protection, applying existing privacy law to AI systems. Israel signed the Council of Europe Framework Convention on AI in September 2024, signaling future alignment with international norms. Draft AI regulation proposals have been circulated for public comment but have not progressed to legislation. Israel’s approach relies on existing sector-specific regulators (Bank of Israel, Capital Markets Authority) to address AI within their jurisdictions.", + "practicalTakeaway": "If your company deploys AI in Israel, the current environment is permissive but changing. Follow the PPA’s data protection guidance for AI immediately — the Privacy Protection Law applies to all AI systems using personal data. Israel’s signing of the Council of Europe AI Convention signals that binding rules are on the horizon. If you operate in financial services, the Bank of Israel expects AI model governance under existing supervisory requirements. Position your AI governance practices now to be ready for future regulatory requirements, particularly if your AI products also serve the EU market.", + "regulations": [ + { + "name": "National AI Policy and Ethics Framework", + "type": "national-strategy", + "status": "policy-only", + "effectiveDate": "November 2023", + "effectiveDateISO": "2023-11-01", + "dateConfidence": "approximate", + "scope": "National policy guidance for AI development and deployment across Israel’s economy and public sector.", + "obligations": [ + "Promotes responsible AI development through voluntary ethical principles: transparency, fairness, accountability, and human oversight", + "Recommends risk-based approach to AI governance without imposing binding requirements", + "Encourages sector-specific AI guidance from existing regulators", + "Supports AI sandboxes and innovation hubs for testing AI applications in regulated sectors" + ], + "maxPenalty": "Not applicable (policy framework, not a regulatory instrument)", + "industryTags": ["general"], + "sourceUrl": "https://innovationisrael.org.il/en/", + "lastVerified": "March 2026" + }, + { + "name": "Privacy Protection Authority guidance on AI and data protection", + "type": "sector-regulation", + "status": "in-force", + "effectiveDate": "July 2024", + "effectiveDateISO": "2024-07-01", + "dateConfidence": "approximate", + "scope": "Organizations processing personal data using AI systems in Israel, under the Privacy Protection Law (1981) and its regulations.", + "obligations": [ + "AI systems processing personal data must comply with existing Privacy Protection Law requirements including registration of databases", + "Inform individuals when AI systems are used to make decisions that significantly affect them", + "Implement data-minimization principles for AI training datasets containing personal information", + "Conduct risk assessments for AI systems that process sensitive personal data at scale" + ], + "maxPenalty": "Enforcement under the Privacy Protection Law; administrative fines introduced in recent amendments, plus potential criminal penalties", + "industryTags": ["general"], + "sourceUrl": "https://www.gov.il/en/departments/the_privacy_protection_authority/", + "lastVerified": "March 2026" + }, + { + "name": "Proposed AI regulation framework", + "type": "draft-bill", + "status": "proposed", + "effectiveDate": "Date TBD", + "effectiveDateISO": null, + "dateConfidence": "unknown", + "scope": "Expected to cover high-risk AI applications in Israel with mandatory governance requirements.", + "obligations": [ + "Draft proposals suggest risk-based classification of AI systems aligned with international frameworks", + "High-risk AI applications (healthcare, financial services, public administration) may face mandatory impact assessments", + "Transparency and explainability requirements for AI systems affecting individual rights", + "Proposals influenced by EU AI Act and Council of Europe Framework Convention obligations" + ], + "maxPenalty": "TBD — legislation not yet introduced", + "industryTags": ["general"], + "sourceUrl": "https://www.gov.il/en/departments/ministry_of_innovation_science_and_technology", + "lastVerified": "March 2026" + } + ], + "timeline": [], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "malaysia", + "name": "Malaysia", + "region": "asia", + "regulationCount": 3, + "hash": "sha256-4e5fefda771583513de49550a1d1fe7fd275210bf8311f9fa2d5dcf341691db9", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-4e5fefda771583513de49550a1d1fe7fd275210bf8311f9fa2d5dcf341691db9", + "regulationCount": 3 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/malaysia", + "flag": "🇲🇾", + "oneLiner": "National AI roadmap published — governance framework under development with ASEAN alignment.", + "executiveSummary": "Malaysia published its National AI Roadmap (AI-RMAP) in 2021, targeting AI adoption across key sectors. The government has issued AI Ethics Principles through MDEC (Malaysia Digital Economy Corporation) and is developing a governance framework aligned with ASEAN regional standards. Malaysia’s PDPA (Personal Data Protection Act 2010) applies to AI use of personal data, though it lacks AI-specific provisions. The government is working on amendments to the PDPA that may include automated decision-making provisions. Malaysia’s approach is pro-adoption with emerging governance, and companies should follow the voluntary AI Ethics Principles while monitoring regulatory developments.", + "practicalTakeaway": "If your company operates AI systems in Malaysia, follow the MDEC AI Ethics Principles as your governance baseline and comply with the PDPA for all personal data used in AI systems. Malaysia’s regulatory approach is currently voluntary for AI-specific governance, but the PDPA is binding and actively enforced. Monitor PDPA amendment proposals that may introduce automated decision-making provisions. If you operate across ASEAN, Malaysia’s alignment with regional frameworks means compliance here will support your broader Southeast Asian operations.", + "regulations": [ + { + "name": "National AI Roadmap (AI-RMAP 2021–2025)", + "type": "national-strategy", + "status": "in-force", + "effectiveDate": "September 2021", + "effectiveDateISO": "2021-09-01", + "dateConfidence": "approximate", + "scope": "National policy framework for AI ecosystem development, adoption, and governance in Malaysia.", + "obligations": [ + "Targets AI adoption across seven priority sectors: agriculture, healthcare, education, transportation, public services, manufacturing, and financial services", + "Develops national AI infrastructure including compute resources and data platforms", + "Promotes AI talent development and capacity building across public and private sectors", + "Establishes governance principles aligned with ASEAN AI governance norms" + ], + "maxPenalty": "Not applicable (national strategy, not a regulatory instrument)", + "industryTags": ["general"], + "sourceUrl": "https://airmap.my/", + "lastVerified": "March 2026" + }, + { + "name": "Malaysia AI Ethics Principles (MDEC)", + "type": "voluntary-guideline", + "status": "voluntary", + "effectiveDate": "September 2024", + "effectiveDateISO": "2024-09-01", + "dateConfidence": "approximate", + "scope": "All organizations developing or deploying AI systems in Malaysia; voluntary guidance issued by MDEC.", + "obligations": [ + "Ensure AI systems are developed and used in ways that are fair, transparent, and accountable", + "Implement human oversight for AI systems that make consequential decisions", + "Protect privacy and data security in AI system design and deployment", + "Conduct bias assessments and promote inclusivity in AI applications", + "Maintain documentation of AI system development and deployment decisions" + ], + "maxPenalty": "No penalties (voluntary principles); adoption demonstrates alignment with government expectations", + "industryTags": ["general"], + "sourceUrl": "https://mdec.my/", + "lastVerified": "March 2026" + }, + { + "name": "Personal Data Protection Act 2010 (PDPA) — AI implications", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "November 2013", + "effectiveDateISO": "2013-11-01", + "dateConfidence": "approximate", + "scope": "All persons processing personal data in commercial transactions in Malaysia, including for AI training and deployment.", + "obligations": [ + "Obtain consent before processing personal data for AI purposes", + "Limit personal data collection and use to what is necessary for the stated purpose", + "Implement security measures to protect personal data used in AI systems", + "Individuals have right of access to and correction of personal data held in AI systems" + ], + "maxPenalty": "Fines up to MYR 500,000 (~$107,000) and/or imprisonment up to 3 years per violation", + "industryTags": ["general"], + "sourceUrl": "https://www.pdp.gov.my/", + "lastVerified": "March 2026" + } + ], + "timeline": [], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "indonesia", + "name": "Indonesia", + "region": "asia", + "regulationCount": 3, + "hash": "sha256-595162004f936e7c1c78566c0bd36e85fdfa80f225c8cb371e3538e9afc6e434", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-595162004f936e7c1c78566c0bd36e85fdfa80f225c8cb371e3538e9afc6e434", + "regulationCount": 3 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/indonesia", + "flag": "🇮🇩", + "oneLiner": "National AI ethics guidelines issued — binding regulation expected as digital economy grows.", + "executiveSummary": "Indonesia published its National AI Strategy (Stranas KA) in 2020, setting out a roadmap for AI development across government and industry. The Ministry of Communication and Informatics (Kominfo) has issued AI ethics guidelines. Indonesia’s Personal Data Protection Law (UU PDP), enacted in 2022, applies to AI use of personal data and includes provisions on automated decision-making. A draft government regulation specifically addressing AI governance is under development. Indonesia’s approach is moving from strategic planning toward regulatory implementation, driven by its rapidly growing digital economy and ASEAN alignment.", + "practicalTakeaway": "If your company operates AI in Indonesia, the PDP Law is now enforceable and applies to all AI processing of personal data, including automated decision-making. Implement consent mechanisms, data protection impact assessments, and cross-border transfer safeguards. Follow Kominfo’s AI ethics guidelines as a governance baseline. Indonesia’s AI-specific regulation is under development, and the government is aligning with ASEAN regional frameworks. Position your AI compliance practices now to be ready for binding requirements as they emerge.", + "regulations": [ + { + "name": "National AI Strategy (Stranas KA)", + "type": "national-strategy", + "status": "in-force", + "effectiveDate": "August 2020", + "effectiveDateISO": "2020-08-01", + "dateConfidence": "approximate", + "scope": "National framework for AI development, adoption, and governance across Indonesia.", + "obligations": [ + "Establishes AI development priorities across healthcare, public services, agriculture, and smart cities", + "Promotes AI talent development and research capacity through universities and government programmes", + "Develops national AI infrastructure including data centres and connectivity", + "Sets ethical principles for AI including transparency, accountability, and inclusivity" + ], + "maxPenalty": "Not applicable (national strategy, not a regulatory instrument)", + "industryTags": ["general"], + "sourceUrl": "https://korika.id/en/document/strategi-nasional-kecerdasan-artifisial-indonesia-2020-2045/", + "lastVerified": "March 2026" + }, + { + "name": "Personal Data Protection Law (UU PDP)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "October 2024", + "effectiveDateISO": "2024-10-01", + "dateConfidence": "approximate", + "scope": "All entities processing personal data of individuals in Indonesia, including for AI model training and automated decision-making.", + "obligations": [ + "Obtain consent for processing personal data for AI purposes; specific consent required for sensitive data", + "Individuals have the right to object to automated decision-making that produces legal or significant effects", + "Implement data protection impact assessments for high-risk processing including AI at scale", + "Cross-border transfer requirements: ensure adequate data protection in the receiving country", + "Maintain records of data processing activities and appoint a data protection officer for certain categories of processing" + ], + "maxPenalty": "Administrative fines up to 2% of annual revenue; criminal penalties including imprisonment up to 6 years and fines up to IDR 6 billion (~$380,000)", + "industryTags": ["general"], + "sourceUrl": "https://www.komnasham.go.id/", + "lastVerified": "March 2026" + }, + { + "name": "Kominfo AI ethics guidelines", + "type": "voluntary-guideline", + "status": "voluntary", + "effectiveDate": "February 2023", + "effectiveDateISO": "2023-02-01", + "dateConfidence": "approximate", + "scope": "Organizations and government agencies developing or deploying AI systems in Indonesia.", + "obligations": [ + "Apply ethical principles including transparency, fairness, accountability, and safety in AI development", + "Ensure AI systems respect human rights and Indonesian cultural values", + "Implement bias mitigation measures in AI training and deployment", + "Maintain documentation of AI system design decisions and risk assessments" + ], + "maxPenalty": "No penalties (voluntary guidelines); adoption supports compliance with future regulations", + "industryTags": ["general"], + "sourceUrl": "https://www.kominfo.go.id/", + "lastVerified": "March 2026" + } + ], + "timeline": [], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "bangladesh", + "name": "Bangladesh", + "region": "asia", + "regulationCount": 2, + "hash": "sha256-c79629a9a81b678d0c8504c8cf8f59833612a81ca4405b3591d238e5e0d86343", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-c79629a9a81b678d0c8504c8cf8f59833612a81ca4405b3591d238e5e0d86343", + "regulationCount": 2 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/bangladesh", + "flag": "🇧🇩", + "oneLiner": "National AI strategy published in 2020 — governance framework still in early stages.", + "executiveSummary": "Bangladesh published its National Strategy for Artificial Intelligence in 2020, identifying key sectors for AI adoption and setting out governance principles. The strategy is coordinated through the ICT Division and a2i (Aspire to Innovate) programme. Bangladesh does not yet have a dedicated data protection law or AI-specific legislation, though a Digital Security Act (2018) and a proposed Data Protection Act are relevant to AI. The regulatory environment for AI is nascent, with governance relying primarily on the national strategy and existing sector-specific laws. Companies operating AI in Bangladesh should follow the strategy’s ethical principles and monitor legislative developments.", + "practicalTakeaway": "If your company deploys AI systems in Bangladesh, the regulatory environment is currently light, but this should not be taken as an invitation to ignore governance. Follow the National AI Strategy’s ethical principles as your baseline. Prepare for data protection legislation that is expected to impose consent and transparency requirements for AI systems processing personal data. Given Bangladesh’s rapidly growing digital economy, AI governance requirements will likely tighten. Implementing good practices now will reduce future compliance costs.", + "regulations": [ + { + "name": "National Strategy for Artificial Intelligence Bangladesh", + "type": "national-strategy", + "status": "in-force", + "effectiveDate": "February 2020", + "effectiveDateISO": "2020-02-01", + "dateConfidence": "approximate", + "scope": "National policy framework for AI development, adoption, and governance in Bangladesh.", + "obligations": [ + "Identifies priority sectors for AI deployment: healthcare, agriculture, financial services, education, and public services", + "Promotes development of AI skills and research capacity through universities and government programmes", + "Establishes ethical principles for AI including fairness, transparency, and accountability", + "Recommends creation of an AI governance body to oversee policy implementation", + "Emphasizes importance of data infrastructure development to support AI adoption" + ], + "maxPenalty": "Not applicable (national strategy, not a regulatory instrument)", + "industryTags": ["general"], + "sourceUrl": "https://a2i.gov.bd/", + "lastVerified": "March 2026" + }, + { + "name": "Proposed Data Protection Act", + "type": "draft-bill", + "status": "proposed", + "effectiveDate": "Date TBD", + "effectiveDateISO": null, + "dateConfidence": "unknown", + "scope": "Intended to cover collection, processing, and use of personal data in Bangladesh, including for AI systems.", + "obligations": [ + "Expected to establish consent requirements for processing personal data, including for AI training", + "Would create rights for individuals to access, correct, and delete personal data", + "May include provisions on automated decision-making and profiling", + "Expected to establish a data protection authority for oversight and enforcement" + ], + "maxPenalty": "TBD — legislation not yet enacted", + "industryTags": ["general"], + "sourceUrl": "https://ictd.gov.bd/", + "lastVerified": "March 2026" + } + ], + "timeline": [], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "taiwan", + "name": "Taiwan", + "region": "asia", + "regulationCount": 3, + "hash": "sha256-cb63f4a668aa984eb137ea0967507ffe0e9915e60f93381e88207be0f660001e", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-cb63f4a668aa984eb137ea0967507ffe0e9915e60f93381e88207be0f660001e", + "regulationCount": 3 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/taiwan", + "flag": "🇹🇼", + "oneLiner": "AI Basic Act passed in 2024 — establishes governance principles with sector-specific follow-up.", + "executiveSummary": "Taiwan enacted the AI Basic Act in July 2024, establishing fundamental principles for AI governance including human rights protection, safety, privacy, transparency, and accountability. The Act is a framework law that directs government agencies to develop sector-specific regulations and guidelines. The National Science and Technology Council (NSTC) coordinates AI policy. Taiwan’s Personal Data Protection Act (PDPA) applies to AI use of personal data. The Financial Supervisory Commission (FSC) has issued guidance on AI in financial services. Taiwan’s approach is principles-based with a clear legislative foundation, and sector-specific implementing rules are under development.", + "practicalTakeaway": "If your company deploys AI in Taiwan, the AI Basic Act establishes the governance principles you must align with, even though detailed implementing regulations are still being developed. Comply with the PDPA for all personal data used in AI systems. If you operate in financial services, FSC guidance already sets expectations for AI model governance. Taiwan’s framework approach means sector-specific rules are coming — implement the basic principles now to get ahead of compliance requirements.", + "regulations": [ + { + "name": "AI Basic Act", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "July 2024", + "effectiveDateISO": "2024-07-01", + "dateConfidence": "approximate", + "scope": "National framework law establishing principles for AI development and governance across Taiwan.", + "obligations": [ + "Establishes core principles for AI governance: human rights, safety, privacy, transparency, accountability, and fairness", + "Directs government agencies to develop sector-specific AI regulations aligned with the basic principles", + "Creates coordination mechanism through NSTC for cross-agency AI policy implementation", + "Requires the government to promote AI research, talent development, and international cooperation", + "Mandates regular review and updating of AI governance measures to keep pace with technology" + ], + "maxPenalty": "Framework law without direct penalties; sector-specific implementing regulations will include enforcement mechanisms", + "industryTags": ["general"], + "sourceUrl": "https://www.nstc.gov.tw/", + "lastVerified": "March 2026" + }, + { + "name": "Personal Data Protection Act (PDPA) — AI provisions", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "October 2012", + "effectiveDateISO": "2012-10-01", + "dateConfidence": "approximate", + "scope": "All entities collecting, processing, or using personal data in Taiwan, including for AI training and deployment.", + "obligations": [ + "Obtain consent or establish legal basis for processing personal data for AI purposes", + "Implement security measures proportionate to the sensitivity of personal data used in AI systems", + "Individuals have rights to access, correct, and delete personal data processed by AI systems", + "Cross-border transfer of personal data for AI purposes must comply with PDPA restrictions" + ], + "maxPenalty": "Fines up to TWD 500,000 (~$15,000) per violation with potential for higher penalties for serious breaches; civil liability for damages", + "industryTags": ["general"], + "sourceUrl": "https://law.moj.gov.tw/", + "lastVerified": "March 2026" + }, + { + "name": "FSC guidance on AI in financial services", + "type": "sector-regulation", + "status": "in-force", + "effectiveDate": "January 2023", + "effectiveDateISO": "2023-01-01", + "dateConfidence": "approximate", + "scope": "Financial institutions supervised by Taiwan’s FSC using AI/ML in customer services, risk management, and compliance.", + "obligations": [ + "Financial institutions must establish AI governance frameworks with designated responsible officers", + "AI models used in lending, insurance, and investment decisions must be explainable to customers", + "Implement fairness testing and bias monitoring for AI models affecting consumer outcomes", + "Maintain documentation of AI model development, validation, and deployment" + ], + "maxPenalty": "Enforcement under existing financial regulation; FSC can impose fines, restrict operations, or revoke licences", + "industryTags": ["financial-services"], + "sourceUrl": "https://www.fsc.gov.tw/en/", + "lastVerified": "March 2026" + } + ], + "timeline": [ + { + "date": "July 2024", + "description": "AI Basic Act enacted, establishing governance principles." + }, + { + "date": "2025–2026", + "description": "Sector-specific AI implementing regulations under development." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "thailand", + "name": "Thailand", + "region": "asia", + "regulationCount": 3, + "hash": "sha256-2af8b568bf2a45e118596e4fa2615f936521f963bbd22fba246c7466eb9c2b39", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-2af8b568bf2a45e118596e4fa2615f936521f963bbd22fba246c7466eb9c2b39", + "regulationCount": 3 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/thailand", + "flag": "🇹🇭", + "oneLiner": "Royal decree on AI governance under development — PDPA already applies to AI data use.", + "executiveSummary": "Thailand has published AI Ethics Guidelines through the Ministry of Digital Economy and Society (MDES) and the National AI Committee. The government’s National AI Strategy and Action Plan (2022–2027) sets priorities for AI development and governance. Thailand’s PDPA (Personal Data Protection Act), fully enforced since June 2022, applies to AI use of personal data and includes provisions on automated decision-making. A royal decree on AI governance is under development but has not yet been issued. Thailand’s approach combines voluntary ethics guidelines with binding data protection law, and is aligned with ASEAN regional governance frameworks.", + "practicalTakeaway": "If your company operates AI systems in Thailand, the PDPA is your immediate binding obligation for all AI use of personal data. Follow the AI Ethics Guidelines as your governance baseline. Thailand’s regulatory approach is tightening, and a royal decree on AI governance is expected. If you operate across ASEAN, Thailand’s alignment with regional frameworks means good governance practices here support broader compliance. Implement consent mechanisms, impact assessments, and data breach procedures now.", + "regulations": [ + { + "name": "Thailand AI Ethics Guidelines", + "type": "voluntary-guideline", + "status": "voluntary", + "effectiveDate": "November 2022", + "effectiveDateISO": "2022-11-01", + "dateConfidence": "approximate", + "scope": "All organizations and government agencies developing or deploying AI in Thailand.", + "obligations": [ + "Apply ethical principles including competitiveness and sustainability, fairness, privacy and security, accountability, and transparency", + "Conduct impact assessments for AI systems that may affect individuals or society", + "Implement human oversight for high-risk AI applications", + "Ensure AI systems do not discriminate or produce unfair outcomes", + "Maintain documentation of AI design and deployment decisions" + ], + "maxPenalty": "No penalties (voluntary guidelines); adoption demonstrates alignment with government expectations", + "industryTags": ["general"], + "sourceUrl": "https://www.etda.or.th/getattachment/9d370f25-f37a-4b7c-b661-48d2d730651d/Digital-Thailand-AI-Ethics-Principle-and-Guideline.pdf.aspx", + "lastVerified": "March 2026" + }, + { + "name": "National AI Strategy and Action Plan (2022–2027)", + "type": "national-strategy", + "status": "in-force", + "effectiveDate": "July 2022", + "effectiveDateISO": "2022-07-01", + "dateConfidence": "approximate", + "scope": "National policy framework for AI ecosystem development, adoption, and governance in Thailand.", + "obligations": [ + "Develops AI infrastructure including national data platforms and computing resources", + "Promotes AI adoption across priority sectors: healthcare, agriculture, public services, and manufacturing", + "Invests in AI talent development through education and training programmes", + "Establishes governance framework aligned with ASEAN AI governance norms" + ], + "maxPenalty": "Not applicable (national strategy, not a regulatory instrument)", + "industryTags": ["general"], + "sourceUrl": "https://www.nstda.or.th/en/", + "lastVerified": "March 2026" + }, + { + "name": "Personal Data Protection Act (PDPA) — AI provisions", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "June 2022", + "effectiveDateISO": "2022-06-01", + "dateConfidence": "approximate", + "scope": "All entities collecting, using, or disclosing personal data in Thailand, including for AI systems.", + "obligations": [ + "Obtain consent before collecting personal data for AI training or processing, with explicit consent for sensitive data", + "Individuals have the right to object to profiling and automated processing that significantly affects them", + "Conduct data protection impact assessments for AI systems processing personal data at scale", + "Implement security safeguards for personal data used in AI systems", + "Data breach notification within 72 hours to the Personal Data Protection Committee" + ], + "maxPenalty": "Administrative fines up to THB 5 million (~$140,000); criminal penalties including imprisonment up to 1 year; civil liability for damages", + "industryTags": ["general"], + "sourceUrl": "https://www.pdpc.or.th/", + "lastVerified": "March 2026" + } + ], + "timeline": [], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "vietnam", + "name": "Vietnam", + "region": "asia", + "regulationCount": 3, + "hash": "sha256-94450606fae14414ecbfe44da74ba6655b4c2a65305c94412b0085d316580510", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-94450606fae14414ecbfe44da74ba6655b4c2a65305c94412b0085d316580510", + "regulationCount": 3 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/vietnam", + "flag": "🇻🇳", + "oneLiner": "Comprehensive AI Law in force since March 2026 — one of Asia's first binding AI statutes.", + "executiveSummary": "Vietnam has moved from policy to binding law: the National Assembly passed the Law on Artificial Intelligence (No. 134/2025/QH15) in December 2025 and it took effect in March 2026, making Vietnam one of the first countries in Asia with a comprehensive standalone AI statute. The law establishes a three-tier risk classification, mandatory conformity assessment for high-risk AI, prohibited practices, transparency rules for AI-generated content, a National AI Development Fund and a regulatory sandbox, with the Ministry of Science and Technology (MOST) as lead authority. It builds on the National AI Research and Development Strategy to 2030 (2021) and operates alongside the Personal Data Protection Decree (Decree 13/2023/ND-CP), which governs AI use of personal data. Transition periods run to March 2027, extended to September 2027 for health, education and finance systems.", + "practicalTakeaway": "If your company operates AI systems in Vietnam, you must now comply with the Law on Artificial Intelligence (No. 134/2025/QH15), in force since March 2026: classify your systems by risk tier, prepare conformity assessments for high-risk AI, ensure human oversight, and label AI-generated content. The Personal Data Protection Decree continues to apply to AI use of personal data. Use the transition periods (to March 2027, or September 2027 for health, education and finance) to bring existing systems into compliance.", + "regulations": [ + { + "name": "National AI Research and Development Strategy to 2030", + "type": "national-strategy", + "status": "in-force", + "effectiveDate": "January 2021", + "effectiveDateISO": "2021-01-01", + "dateConfidence": "approximate", + "scope": "National policy framework for AI development, adoption, and governance in Vietnam.", + "obligations": [ + "Targets Vietnam to be among the top 4 ASEAN countries in AI research and development by 2025", + "Develops national AI infrastructure including data centres, computing resources, and open datasets", + "Promotes AI adoption in priority sectors: healthcare, agriculture, transportation, and manufacturing", + "Invests in AI talent development through universities and international partnerships", + "Establishes a legal framework for AI governance, including ethical guidelines and standards" + ], + "maxPenalty": "Not applicable (national strategy, not a regulatory instrument)", + "industryTags": ["general"], + "sourceUrl": "https://www.most.gov.vn/en/", + "lastVerified": "March 2026" + }, + { + "name": "Personal Data Protection Decree (Decree 13/2023/ND-CP)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "July 2023", + "effectiveDateISO": "2023-07-01", + "dateConfidence": "approximate", + "scope": "All organizations processing personal data in Vietnam, including for AI training and automated decision-making.", + "obligations": [ + "Obtain consent before processing personal data for AI purposes, with enhanced requirements for sensitive data", + "Conduct data protection impact assessments for cross-border data transfers used in AI training", + "Maintain records of data processing activities and implement security measures", + "Notify individuals about AI-related processing of their personal data and provide access rights" + ], + "maxPenalty": "Administrative fines under existing cybersecurity law; specific penalties under the decree being developed through implementing regulations", + "industryTags": ["general"], + "sourceUrl": "https://mic.gov.vn/en/", + "lastVerified": "March 2026" + }, + { + "name": "Law on Artificial Intelligence (No. 134/2025/QH15)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "March 2026 (passed December 2025)", + "effectiveDateISO": "2026-03-01", + "dateConfidence": "approximate", + "scope": "Vietnam's first comprehensive standalone AI law (8 chapters, 35 articles), governing the development, provision and deployment of AI systems in Vietnam, including cross-border AI service platforms.", + "obligations": [ + "Classify AI systems into three risk tiers (high, medium, low) based on impact on rights, safety, security and scale of use", + "Subject high-risk AI systems to strict controls including mandatory pre-market conformity assessment", + "Maintain human oversight in important decision-making under a human-centric principle (AI serves, not replaces, humans)", + "Observe prohibited AI practices, including manipulative or deceptive uses causing serious harm and exploitation of vulnerable groups", + "Apply transparency and labelling rules to AI-generated content", + "Use the controlled regulatory sandbox for sensitive AI; transition periods run to March 2027 (September 2027 for health, education and finance)" + ], + "maxPenalty": "Administrative fines reported up to VND 2 billion (~USD 76,000) for organisations, with revenue-based fines for serious violations plus possible criminal and civil liability", + "industryTags": ["general", "healthcare", "financial-services", "public-sector"], + "sourceUrl": "https://www.most.gov.vn/en/", + "lastVerified": "June 2026" + } + ], + "timeline": [], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "saudi-arabia", + "name": "Saudi Arabia", + "region": "asia", + "regulationCount": 3, + "hash": "sha256-551e161cda54280f18cdc78445c7fa158d4531cdd995e986d0828678387b0ee1", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-551e161cda54280f18cdc78445c7fa158d4531cdd995e986d0828678387b0ee1", + "regulationCount": 3 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/saudi-arabia", + "flag": "🇸🇦", + "oneLiner": "SDAIA published binding AI ethics principles — governance framework tied to Vision 2030.", + "executiveSummary": "Saudi Arabia has been one of the most active Gulf states on AI governance. The Saudi Data and Artificial Intelligence Authority (SDAIA) leads AI strategy and governance. SDAIA published AI Ethics Principles in 2023 and has developed a comprehensive AI governance framework aligned with Vision 2030. The National Strategy for Data and AI (NSDAI) sets ambitious targets for AI adoption. Saudi Arabia’s Personal Data Protection Law (PDPL), effective September 2023, applies to AI systems processing personal data. SDAIA’s governance framework is increasingly treated as a mandatory expectation for companies operating in the kingdom, particularly those working with government clients.", + "practicalTakeaway": "If your company operates AI systems in Saudi Arabia, treat SDAIA’s AI Ethics Principles as a practical requirement, especially if you work with government clients. The PDPL is binding and includes AI-relevant provisions on automated decision-making and data protection. Implement consent mechanisms, impact assessments, and data localization measures. Saudi Arabia’s AI ambitions under Vision 2030 mean the regulatory framework will continue to mature rapidly. Engaging with SDAIA early can provide clarity on governance expectations.", + "regulations": [ + { + "name": "National Strategy for Data and AI (NSDAI)", + "type": "national-strategy", + "status": "in-force", + "effectiveDate": "October 2020", + "effectiveDateISO": "2020-10-01", + "dateConfidence": "approximate", + "scope": "National policy framework for AI and data ecosystem development in Saudi Arabia, coordinated by SDAIA.", + "obligations": [ + "Positions Saudi Arabia as a global AI leader by 2030 with targets for AI contribution to GDP", + "Develops national AI infrastructure including data centres, compute capacity, and talent pipelines", + "Promotes AI adoption across government services, healthcare, energy, and financial services", + "Establishes SDAIA as the central authority for AI and data governance in the kingdom" + ], + "maxPenalty": "Not applicable (national strategy, not a regulatory instrument)", + "industryTags": ["general"], + "sourceUrl": "https://sdaia.gov.sa/en/default.aspx", + "lastVerified": "March 2026" + }, + { + "name": "SDAIA AI Ethics Principles", + "type": "voluntary-guideline", + "status": "in-force", + "effectiveDate": "September 2023", + "effectiveDateISO": "2023-09-01", + "dateConfidence": "approximate", + "scope": "All organizations developing or deploying AI systems in Saudi Arabia; effectively mandatory for government contractors and SDAIA-governed entities.", + "obligations": [ + "Ensure AI systems are fair, transparent, and accountable in their operation and outcomes", + "Implement human oversight for AI systems making consequential decisions", + "Protect privacy and security in AI system design, development, and deployment", + "Conduct impact assessments for AI systems with potential societal or individual effects", + "Maintain documentation of AI governance practices and make them available for review" + ], + "maxPenalty": "Effectively mandatory for government contractors; non-compliance may result in loss of government contracts and SDAIA-related certifications", + "industryTags": ["general"], + "sourceUrl": "https://sdaia.gov.sa/en/SDAIA/about/governance/Pages/EthicalPrinciples.aspx", + "lastVerified": "March 2026" + }, + { + "name": "Personal Data Protection Law (PDPL)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "September 2023", + "effectiveDateISO": "2023-09-01", + "dateConfidence": "approximate", + "scope": "All entities processing personal data in Saudi Arabia, including for AI model training and automated decision-making.", + "obligations": [ + "Obtain consent before processing personal data for AI purposes; explicit consent for sensitive data", + "Individuals have the right to be informed about automated decision-making and to request human review", + "Implement data protection impact assessments for high-risk processing activities including AI", + "Data localization requirements for certain categories of personal data; cross-border transfers require authorization", + "Maintain records of data processing activities and appoint a data protection officer" + ], + "maxPenalty": "Fines up to SAR 5 million (~$1.3 million) per violation; criminal penalties including imprisonment for serious breaches", + "industryTags": ["general"], + "sourceUrl": "https://sdaia.gov.sa/en/SDAIA/NDMO/Pages/PersonalDataProtection.aspx", + "lastVerified": "March 2026" + } + ], + "timeline": [ + { + "date": "October 2020", + "description": "NSDAI published, positioning Saudi Arabia as an aspiring global AI leader." + }, + { + "date": "September 2023", + "description": "SDAIA AI Ethics Principles and PDPL become effective." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "united-arab-emirates", + "name": "United Arab Emirates", + "region": "asia", + "regulationCount": 4, + "hash": "sha256-b59f65796ac13acd2e25b3d5e43e3cf8040e01d0bceaff8e6313dab44784b68a", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-b59f65796ac13acd2e25b3d5e43e3cf8040e01d0bceaff8e6313dab44784b68a", + "regulationCount": 4 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/united-arab-emirates", + "flag": "🇦🇪", + "oneLiner": "World’s first Minister of AI — national strategy with sector-specific guidelines emerging.", + "executiveSummary": "The UAE was the first country to appoint a Minister of State for Artificial Intelligence (2017) and has pursued an ambitious AI strategy. The UAE National AI Strategy 2031 sets targets for AI adoption across government and the economy. Dubai has issued AI Ethics Principles and the Dubai International Financial Centre (DIFC) has enacted data protection regulations that apply to AI. The UAE’s Federal Data Protection Law (2021) applies to AI systems processing personal data. The Abu Dhabi Global Market (ADGM) has issued guidance on AI in financial services. The regulatory approach combines national strategy with emirate-level implementation and free-zone-specific rules.", + "practicalTakeaway": "If your company operates AI in the UAE, the Federal Data Protection Law is your binding baseline for personal data in AI systems. Follow Dubai’s AI Ethics Principles if you operate in Dubai, especially for government-facing services. If you operate in DIFC or ADGM free zones, their respective data protection and AI governance rules apply instead of (or in addition to) federal law. The UAE’s AI ambitions mean the ecosystem is supportive but increasingly expects governance maturity. Engage with the AI Office and free-zone regulators for clarity on sector-specific expectations.", + "regulations": [ + { + "name": "UAE National AI Strategy 2031", + "type": "national-strategy", + "status": "in-force", + "effectiveDate": "October 2017", + "effectiveDateISO": "2017-10-01", + "dateConfidence": "approximate", + "scope": "National policy framework for AI adoption, development, and governance across the UAE.", + "obligations": [ + "Positions AI as a core driver of economic growth and government efficiency by 2031", + "Targets AI integration across nine priority sectors: transport, health, energy, education, technology, water, space, environment, and traffic", + "Invests in AI research, infrastructure, and talent development", + "Promotes responsible AI through governance frameworks and ethical guidelines" + ], + "maxPenalty": "Not applicable (national strategy, not a regulatory instrument)", + "industryTags": ["general"], + "sourceUrl": "https://ai.gov.ae/", + "lastVerified": "March 2026" + }, + { + "name": "Dubai AI Ethics Principles and Guidelines", + "type": "voluntary-guideline", + "status": "in-force", + "effectiveDate": "January 2019", + "effectiveDateISO": "2019-01-01", + "dateConfidence": "approximate", + "scope": "Government entities and organizations operating in Dubai that develop or deploy AI systems.", + "obligations": [ + "Apply seven ethical principles: fairness, accountability, transparency, explainability, human control, safety, and privacy", + "Conduct AI impact assessments before deploying AI in government services", + "Implement human oversight mechanisms for AI-driven decisions affecting individuals", + "Ensure AI systems are accessible and inclusive", + "Maintain audit trails for AI-supported government decisions" + ], + "maxPenalty": "No direct penalties (voluntary guidelines for most private entities); effectively mandatory for Dubai government services", + "industryTags": ["general", "public-sector"], + "sourceUrl": "https://www.smartdubai.ae/", + "lastVerified": "March 2026" + }, + { + "name": "Federal Data Protection Law (Federal Decree-Law No. 45 of 2021)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "January 2022", + "effectiveDateISO": "2022-01-01", + "dateConfidence": "approximate", + "scope": "All entities processing personal data in the UAE (outside free zones with their own data protection regimes), including for AI systems.", + "obligations": [ + "Obtain consent before processing personal data for AI purposes; explicit consent for sensitive data categories", + "Implement appropriate technical and organizational measures to protect personal data in AI systems", + "Individuals have rights to access, rectify, and erase personal data used in AI systems", + "Cross-border data transfers require adequate protections or approved transfer mechanisms", + "Conduct data protection impact assessments for high-risk processing activities" + ], + "maxPenalty": "Administrative fines up to AED 2 million (~$545,000) per violation; additional penalties under sector-specific regulations", + "industryTags": ["general"], + "sourceUrl": "https://tdra.gov.ae/", + "lastVerified": "March 2026" + }, + { + "name": "ADGM guidance on AI in financial services", + "type": "sector-regulation", + "status": "in-force", + "effectiveDate": "February 2024", + "effectiveDateISO": "2024-02-01", + "dateConfidence": "approximate", + "scope": "Financial institutions regulated by ADGM (Abu Dhabi Global Market) using AI/ML in operations, risk management, or customer services.", + "obligations": [ + "Implement AI governance frameworks with designated senior officers responsible for AI oversight", + "Ensure AI models used in financial decisions are explainable and subject to independent validation", + "Conduct fairness testing for AI systems affecting consumer outcomes in lending, insurance, and investment", + "Maintain documentation of AI model development, validation, and monitoring processes" + ], + "maxPenalty": "Enforcement under ADGM’s regulatory framework; powers include fines, licence conditions, and revocation", + "industryTags": ["financial-services"], + "sourceUrl": "https://www.adgm.com/", + "lastVerified": "March 2026" + } + ], + "timeline": [ + { + "date": "October 2017", + "description": "UAE appoints world’s first Minister of AI; National AI Strategy 2031 launched." + }, + { + "date": "January 2019", + "description": "Dubai AI Ethics Principles published." + }, + { + "date": "January 2022", + "description": "Federal Data Protection Law takes effect." + }, + { + "date": "February 2024", + "description": "ADGM issues guidance on AI in financial services." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "brazil", + "name": "Brazil", + "region": "south-america", + "regulationCount": 3, + "hash": "sha256-9592d43a2f595cdbdb66fc23e4da0943fc4e1515f3f9c586e3cec2f846f073ea", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-9592d43a2f595cdbdb66fc23e4da0943fc4e1515f3f9c586e3cec2f846f073ea", + "regulationCount": 3 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/brazil", + "flag": "🇧🇷", + "oneLiner": "AI regulation bill approved by Senate — risk-based framework expected to become law.", + "executiveSummary": "Brazil is on track to become the first Latin American country with a comprehensive AI law. The AI Bill (PL 2338/2023) was approved by the Senate in December 2024 and is progressing through the Chamber of Deputies. The bill establishes a risk-based framework inspired by the EU AI Act. Brazil’s LGPD (Lei Geral de Proteção de Dados), in force since 2020, already applies to AI systems processing personal data and includes provisions on automated decision-making. The Brazilian National AI Strategy (EBIA), published in 2021, sets the policy direction. The National Data Protection Authority (ANPD) has issued guidance on AI and data protection. Brazil’s approach combines active legislation with existing data protection enforcement.", + "practicalTakeaway": "If your company operates AI in Brazil, the LGPD is already binding and includes the right to review automated decisions (Article 20). Implement human review mechanisms for consequential AI decisions now. The AI Bill is progressing toward enactment and will introduce EU-style risk classification — start preparing by identifying which of your AI systems would be classified as high-risk. ANPD is actively enforcing data protection obligations. Brazil’s market size makes compliance essential for any company targeting Latin America.", + "regulations": [ + { + "name": "AI Bill PL 2338/2023 (Marco Legal da Inteligência Artificial)", + "type": "draft-bill", + "status": "proposed", + "effectiveDate": "Expected 2025–2026", + "effectiveDateISO": "2025-01-01", + "dateConfidence": "approximate", + "scope": "AI systems developed, deployed, or used in Brazil, with enhanced requirements for high-risk applications.", + "obligations": [ + "Classify AI systems by risk level: unacceptable risk (prohibited), high risk (regulated), and general AI (lighter obligations)", + "High-risk AI systems must undergo impact assessments and maintain documentation on safety, fairness, and transparency", + "Establish transparency requirements: users must be informed when interacting with AI systems", + "Create a governance and oversight authority for AI regulation", + "Ensure human oversight for AI decisions with significant effects on individuals’ rights" + ], + "maxPenalty": "Expected penalties up to 2% of the company’s revenue in Brazil (up to BRL 50 million, ~$10 million), suspension of AI operations, and prohibition of processing data", + "industryTags": ["general"], + "sourceUrl": "https://www25.senado.leg.br/web/atividade/materias/-/materia/163054", + "lastVerified": "March 2026" + }, + { + "name": "LGPD — Lei Geral de Proteção de Dados (General Data Protection Law)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "September 2020", + "effectiveDateISO": "2020-09-01", + "dateConfidence": "approximate", + "scope": "All organizations processing personal data in Brazil or of individuals in Brazil, including for AI systems.", + "obligations": [ + "Obtain consent or establish legal basis for processing personal data for AI training and deployment", + "Individuals have the right to request review of automated decisions that affect their interests (Article 20)", + "Provide clear information about the criteria and procedures used in automated decision-making", + "Conduct data protection impact assessments (Relatório de Impacto) for AI systems processing personal data at scale", + "ANPD can request explanations of automated decision-making logic from data controllers" + ], + "maxPenalty": "Fines up to 2% of the company’s revenue in Brazil (capped at BRL 50 million, ~$10 million per violation); ANPD can also issue warnings and block data processing", + "industryTags": ["general"], + "sourceUrl": "https://www.gov.br/anpd/pt-br", + "lastVerified": "March 2026" + }, + { + "name": "Brazilian National AI Strategy (EBIA)", + "type": "national-strategy", + "status": "in-force", + "effectiveDate": "April 2021", + "effectiveDateISO": "2021-04-01", + "dateConfidence": "approximate", + "scope": "National policy framework for AI development, adoption, and governance in Brazil.", + "obligations": [ + "Establishes nine priority axes: legislation and ethical use, AI governance, international aspects, AI workforce, R&D, application in public and productive sectors, and public security", + "Promotes responsible AI development aligned with human rights and democratic values", + "Invests in AI research infrastructure and talent development", + "Supports AI adoption in government services and key economic sectors" + ], + "maxPenalty": "Not applicable (national strategy, not a regulatory instrument)", + "industryTags": ["general"], + "sourceUrl": "https://www.gov.br/mcti/pt-br/acompanhe-o-mcti/transformacaodigital/inteligencia-artificial", + "lastVerified": "March 2026" + } + ], + "timeline": [ + { + "date": "September 2020", + "description": "LGPD enters into force with AI-relevant provisions." + }, + { + "date": "April 2021", + "description": "Brazilian National AI Strategy (EBIA) published." + }, + { + "date": "December 2024", + "description": "AI Bill PL 2338/2023 approved by the Senate." + }, + { + "date": "2025–2026", + "description": "AI Bill expected to be approved by Chamber of Deputies and enacted." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "argentina", + "name": "Argentina", + "region": "south-america", + "regulationCount": 2, + "hash": "sha256-5b11a6109f58eb5854449d97932197b0f87197d9d99bc07273177e6fb455e625", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-5b11a6109f58eb5854449d97932197b0f87197d9d99bc07273177e6fb455e625", + "regulationCount": 2 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/argentina", + "flag": "🇦🇷", + "oneLiner": "No binding AI law — national AI plan promotes ethical use with soft governance measures.", + "executiveSummary": "Argentina has taken a policy-based approach to AI governance. The National AI Plan (Plan Nacional de Inteligencia Artificial), published in 2019, established priorities for AI development and ethical governance. Argentina’s Personal Data Protection Law (Ley 25.326) applies to AI systems processing personal data, and the data protection authority (AAIP) has issued guidance on automated decision-making. While there is no AI-specific legislation, draft bills have been introduced in Congress. Argentina is an active participant in OECD and UNESCO AI governance frameworks and has endorsed their principles.", + "practicalTakeaway": "If your company deploys AI in Argentina, the Personal Data Protection Law applies to all AI processing of personal data, and the AAIP is an active regulator. Implement consent and data security measures for AI systems. While there is no AI-specific binding law, Argentina’s participation in OECD and UNESCO frameworks means governance expectations are higher than the legal minimum. Monitor legislative developments for potential AI-specific bills. Argentina’s EU-adequate data protection status makes compliance here relevant if you also serve the EU market.", + "regulations": [ + { + "name": "National AI Plan (Plan Nacional de Inteligencia Artificial)", + "type": "national-strategy", + "status": "in-force", + "effectiveDate": "August 2019", + "effectiveDateISO": "2019-08-01", + "dateConfidence": "approximate", + "scope": "National policy framework for AI development, research, and governance in Argentina.", + "obligations": [ + "Promotes responsible AI development with emphasis on ethical use and social impact", + "Identifies priority sectors for AI deployment: public administration, health, agriculture, and industry", + "Invests in AI research through universities and CONICET (National Scientific and Technical Research Council)", + "Recommends development of AI governance guidelines and standards", + "Promotes AI skills development and digital literacy programmes" + ], + "maxPenalty": "Not applicable (national strategy, not a regulatory instrument)", + "industryTags": ["general"], + "sourceUrl": "https://www.boletinoficial.gob.ar/detalleAviso/primera/314465/20240924", + "lastVerified": "March 2026" + }, + { + "name": "Personal Data Protection Law (Ley 25.326) — AI implications", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "November 2000", + "effectiveDateISO": "2000-11-01", + "dateConfidence": "approximate", + "scope": "All entities processing personal data in Argentina, including for AI model training and automated decision-making.", + "obligations": [ + "Obtain consent for processing personal data for AI purposes, unless a legal exception applies", + "Register databases containing personal data with the AAIP", + "Implement adequate security measures for personal data used in AI systems", + "Individuals have rights to access, rectify, and delete personal data processed by AI systems", + "AAIP has issued guidance interpreting existing law to cover automated profiling and decision-making" + ], + "maxPenalty": "Administrative fines, database suspension, and criminal penalties for severe violations; Argentina’s data protection is recognized as adequate by the EU", + "industryTags": ["general"], + "sourceUrl": "https://www.argentina.gob.ar/aaip", + "lastVerified": "March 2026" + } + ], + "timeline": [], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "chile", + "name": "Chile", + "region": "south-america", + "regulationCount": 4, + "hash": "sha256-19eaa1410316b1575f44dcb79ea94e732dfba0cc03812cee482ba490a6c1ebe5", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-19eaa1410316b1575f44dcb79ea94e732dfba0cc03812cee482ba490a6c1ebe5", + "regulationCount": 4 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/chile", + "flag": "🇨🇱", + "oneLiner": "AI policy and neuro-rights constitutional amendment make Chile a regional governance leader.", + "executiveSummary": "Chile has distinguished itself as a regional leader in AI governance. In 2021, Chile became the first country in the world to enshrine neuro-rights in its constitution, protecting mental integrity against AI and neurotechnology. The National AI Policy (2021) provides a comprehensive governance framework. Chile enacted Law 21.719 on Personal Data Protection in 2024, modernizing its data protection framework with GDPR-inspired provisions that apply to AI. A draft AI Bill (Proyecto de Ley sobre Inteligencia Artificial) is under legislative discussion. Chile’s approach combines constitutional innovation with practical governance frameworks.", + "practicalTakeaway": "If your company operates AI in Chile, the neuro-rights constitutional amendment is immediately relevant if you develop neurotechnology or brain-computer interfaces. The new data protection law (effective December 2026) will introduce GDPR-style obligations including rights related to automated decision-making — start preparing now. Follow the National AI Policy’s principles as your governance baseline. Chile’s governance leadership in Latin America means compliance here positions you well for the regional market.", + "regulations": [ + { + "name": "National AI Policy (Política Nacional de Inteligencia Artificial)", + "type": "national-strategy", + "status": "in-force", + "effectiveDate": "November 2021", + "effectiveDateISO": "2021-11-01", + "dateConfidence": "approximate", + "scope": "National policy framework for AI development, adoption, and governance in Chile.", + "obligations": [ + "Establishes principles for responsible AI: respect for human rights, safety, transparency, accountability, and inclusivity", + "Promotes AI adoption across priority sectors including healthcare, agriculture, mining, and public services", + "Invests in AI research and talent development through universities and public programmes", + "Recommends creation of sector-specific AI governance guidelines", + "Aligns with OECD AI Principles and international governance standards" + ], + "maxPenalty": "Not applicable (national policy, not a regulatory instrument)", + "industryTags": ["general"], + "sourceUrl": "https://www.minciencia.gob.cl/areas/inteligencia-artificial/politica-nacional-de-inteligencia-artificial/", + "lastVerified": "March 2026" + }, + { + "name": "Constitutional amendment on neuro-rights (Law 21.383)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "October 2021", + "effectiveDateISO": "2021-10-01", + "dateConfidence": "approximate", + "scope": "Constitutional protection of mental integrity and neural data of all persons in Chile, including against AI and neurotechnology interference.", + "obligations": [ + "Protects mental integrity as a constitutional right against AI and neurotechnology interference", + "Prohibits AI systems from manipulating or altering individuals’ mental states without consent", + "Neural data generated by brain-computer interfaces is classified as personal data under data protection law" + ], + "maxPenalty": "Constitutional right enforced through courts; violations subject to constitutional remedies and potential criminal liability", + "industryTags": ["general", "healthcare"], + "sourceUrl": "https://www.bcn.cl/leychile/navegar?idNorma=1168725", + "lastVerified": "March 2026" + }, + { + "name": "Law 21.719 on Personal Data Protection (2024)", + "type": "binding-law", + "status": "passed-not-active", + "effectiveDate": "December 2026", + "effectiveDateISO": "2026-12-01", + "dateConfidence": "approximate", + "scope": "All entities processing personal data of individuals in Chile, including for AI model training and automated decision-making.", + "obligations": [ + "Modernizes Chile’s data protection framework with GDPR-inspired provisions", + "Establishes an independent data protection authority (Agencia de Protección de Datos Personales)", + "Individuals have the right to not be subject to solely automated decisions with significant effects, including the right to human review", + "Mandatory data protection impact assessments for high-risk processing including AI at scale", + "Enhanced consent requirements and rights to access, rectification, erasure, and portability" + ], + "maxPenalty": "Fines up to UTM 20,000 (~$1.5 million); data processing suspension; the new data protection authority will enforce", + "industryTags": ["general"], + "sourceUrl": "https://www.bcn.cl/leychile/navegar?idNorma=1198789", + "lastVerified": "March 2026" + }, + { + "name": "Draft AI Bill (Proyecto de Ley sobre Inteligencia Artificial)", + "type": "draft-bill", + "status": "proposed", + "effectiveDate": "Date TBD", + "effectiveDateISO": null, + "dateConfidence": "unknown", + "scope": "Expected to cover AI development and deployment in Chile with risk-based governance requirements.", + "obligations": [ + "Draft proposes risk-based classification of AI systems with enhanced requirements for high-risk applications", + "Transparency obligations for AI systems that interact with individuals or make consequential decisions", + "Expected to require AI impact assessments for high-risk use cases", + "Aligns with OECD AI Principles and regional best practices" + ], + "maxPenalty": "TBD — bill under legislative discussion", + "industryTags": ["general"], + "sourceUrl": "https://www.senado.cl/", + "lastVerified": "March 2026" + } + ], + "timeline": [ + { + "date": "October 2021", + "description": "Chile becomes first country to enshrine neuro-rights in its constitution." + }, + { + "date": "November 2021", + "description": "National AI Policy published." + }, + { + "date": "2024", + "description": "Personal Data Protection Law (21.719) enacted; transition period until December 2026." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "colombia", + "name": "Colombia", + "region": "south-america", + "regulationCount": 3, + "hash": "sha256-f9cbff35a5ed66e0ce1033771217c426db7a0c82fe5905b21d87053c55eaafe9", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-f9cbff35a5ed66e0ce1033771217c426db7a0c82fe5905b21d87053c55eaafe9", + "regulationCount": 3 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/colombia", + "flag": "🇨🇴", + "oneLiner": "AI ethics framework and national strategy issued — binding regulation under discussion.", + "executiveSummary": "Colombia published its National AI Policy (CONPES 3975) in 2019, establishing a comprehensive framework for AI adoption and governance. The policy is coordinated through the Ministry of ICT and the Presidential Council for Digital Transformation. Colombia’s data protection framework (Law 1581/2012 and Law 1266/2008) applies to AI use of personal data, with the Superintendencia de Industria y Comercio (SIC) as the enforcement authority. Colombia has issued AI ethics recommendations through government bodies and is an OECD member, aligning with OECD AI Principles. While no AI-specific binding legislation has been enacted, Colombia’s governance approach is among the most developed in Latin America.", + "practicalTakeaway": "If your company operates AI in Colombia, the data protection framework (Law 1581/2012) is actively enforced by SIC and applies to all AI processing of personal data. Register your databases, implement consent mechanisms, and ensure individual rights can be exercised. Follow the CONPES 3975 AI Policy principles and ethics recommendations as your governance baseline. Colombia’s OECD membership means governance expectations are aligned with international standards. Monitor legislative developments for potential AI-specific regulation.", + "regulations": [ + { + "name": "National AI Policy (CONPES 3975)", + "type": "national-strategy", + "status": "in-force", + "effectiveDate": "November 2019", + "effectiveDateISO": "2019-11-01", + "dateConfidence": "approximate", + "scope": "National policy framework for AI development, adoption, and governance in Colombia.", + "obligations": [ + "Establishes an action plan for AI development covering infrastructure, talent, governance, and sectoral adoption", + "Promotes AI adoption in priority sectors: agriculture, health, justice, education, and public administration", + "Creates an AI governance framework emphasizing transparency, accountability, and human-centricity", + "Develops ethical guidelines for AI aligned with OECD AI Principles", + "Invests in AI research capacity through universities and ColCiencias (now MinCiencias)" + ], + "maxPenalty": "Not applicable (national policy, not a regulatory instrument)", + "industryTags": ["general"], + "sourceUrl": "https://colaboracion.dnp.gov.co/CDT/Conpes/Econ%C3%B3micos/3975.pdf", + "lastVerified": "March 2026" + }, + { + "name": "Data Protection Law (Law 1581/2012) — AI provisions", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "October 2012", + "effectiveDateISO": "2012-10-01", + "dateConfidence": "approximate", + "scope": "All entities processing personal data in Colombia, including for AI model training and automated decisions.", + "obligations": [ + "Obtain consent (authorisation) before processing personal data for AI purposes", + "Individuals have rights to access, update, and delete personal data used in AI systems", + "Implement security measures adequate to protect personal data in AI processing", + "Register databases with the SIC National Registry of Databases", + "SIC has interpreted existing law to cover profiling and automated decision-making obligations" + ], + "maxPenalty": "Fines up to 2,000 minimum monthly wages (~$500,000); SIC can also issue corrective orders and suspend processing activities", + "industryTags": ["general"], + "sourceUrl": "https://www.sic.gov.co/sobre-la-proteccion-de-datos-personales", + "lastVerified": "March 2026" + }, + { + "name": "Colombian AI Ethics Recommendations (2021)", + "type": "voluntary-guideline", + "status": "voluntary", + "effectiveDate": "December 2021", + "effectiveDateISO": "2021-12-01", + "dateConfidence": "approximate", + "scope": "Government agencies and organizations developing or deploying AI in Colombia.", + "obligations": [ + "Apply ethical principles including transparency, justice and equity, non-maleficence, responsibility, and privacy", + "Conduct impact assessments for AI systems that may affect individuals or communities", + "Ensure human oversight for AI-assisted decisions with significant consequences", + "Promote inclusive AI development that considers Colombia’s diverse population" + ], + "maxPenalty": "No penalties (voluntary recommendations); adoption demonstrates alignment with government expectations", + "industryTags": ["general"], + "sourceUrl": "https://www.mintic.gov.co/", + "lastVerified": "March 2026" + } + ], + "timeline": [], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "peru", + "name": "Peru", + "region": "south-america", + "regulationCount": 3, + "hash": "sha256-1f9206b958c6b7599dc9fb5a0b7fc428815f5739416d974d77ea1e3fcdc5bae3", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-1f9206b958c6b7599dc9fb5a0b7fc428815f5739416d974d77ea1e3fcdc5bae3", + "regulationCount": 3 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/peru", + "flag": "🇵🇪", + "oneLiner": "AI promotion law enacted in 2023 — governance framework focused on public-sector adoption.", + "executiveSummary": "Peru enacted Law 31814 (Ley de promoción del uso de la inteligencia artificial) in July 2023, making it one of the first Latin American countries with AI-specific legislation. The law promotes responsible AI development and use, particularly in the public sector, and directs government agencies to adopt AI ethics principles. Peru’s Personal Data Protection Law (Law 29733) applies to AI systems processing personal data, with the National Authority for Personal Data Protection (ANPDP) as the enforcement body. Peru’s National AI Strategy sets broader goals for AI ecosystem development. The approach combines legislation, policy, and existing data protection enforcement.", + "practicalTakeaway": "If your company operates AI in Peru, comply with the Personal Data Protection Law for all AI processing of personal data. Law 31814 signals the government’s commitment to AI governance and creates practical expectations for public-sector AI. If you provide AI products to the Peruvian government, transparent and accountable AI practices are a requirement. Peru’s governance framework is maturing, and companies that establish good practices now will be positioned for any binding requirements that follow.", + "regulations": [ + { + "name": "Law 31814 — AI Promotion Law", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "July 2023", + "effectiveDateISO": "2023-07-01", + "dateConfidence": "approximate", + "scope": "Promotes responsible AI development and use across Peru, with specific provisions for public-sector AI adoption.", + "obligations": [ + "Government entities must incorporate AI to improve efficiency and quality of public services", + "AI use in the public sector must comply with principles of transparency, accountability, non-discrimination, and human oversight", + "Directs the executive branch to develop AI governance guidelines and standards", + "Promotes AI research, education, and talent development", + "Encourages private-sector AI adoption with responsible governance practices" + ], + "maxPenalty": "Promotional law without direct penalty provisions; enforcement of AI governance principles through existing legal frameworks", + "industryTags": ["general", "public-sector"], + "sourceUrl": "https://www.gob.pe/pcm", + "lastVerified": "March 2026" + }, + { + "name": "National AI Strategy", + "type": "national-strategy", + "status": "in-force", + "effectiveDate": "May 2021", + "effectiveDateISO": "2021-05-01", + "dateConfidence": "approximate", + "scope": "National policy framework for AI ecosystem development, adoption, and governance in Peru.", + "obligations": [ + "Establishes priority areas for AI adoption: health, agriculture, education, public administration, and environment", + "Invests in AI infrastructure and data ecosystems", + "Develops AI talent through education and training programmes", + "Creates ethical guidelines for AI aligned with international best practices" + ], + "maxPenalty": "Not applicable (national strategy, not a regulatory instrument)", + "industryTags": ["general"], + "sourceUrl": "https://www.gob.pe/pcm", + "lastVerified": "March 2026" + }, + { + "name": "Personal Data Protection Law (Law 29733) — AI implications", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "March 2011", + "effectiveDateISO": "2011-03-01", + "dateConfidence": "approximate", + "scope": "All entities processing personal data in Peru, including for AI model training and automated decision-making.", + "obligations": [ + "Obtain consent before processing personal data for AI purposes, with specific requirements for sensitive data", + "Register personal data banks with the ANPDP", + "Implement security measures for personal data processed by AI systems", + "Individuals have rights to access, rectify, cancel, and oppose processing of personal data in AI systems" + ], + "maxPenalty": "Administrative fines up to 100 UIT (~$130,000); the ANPDP can also order correction or cessation of processing", + "industryTags": ["general"], + "sourceUrl": "https://www.gob.pe/anpd", + "lastVerified": "March 2026" + } + ], + "timeline": [], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "nigeria", + "name": "Nigeria", + "region": "africa", + "regulationCount": 3, + "hash": "sha256-9bb19a8f24e05f7fb304cccd45822028fbd3423cb54e65313c33ad83cf2d943b", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-9bb19a8f24e05f7fb304cccd45822028fbd3423cb54e65313c33ad83cf2d943b", + "regulationCount": 3 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/nigeria", + "flag": "🇳🇬", + "oneLiner": "National AI strategy published — NITDA developing binding AI governance framework.", + "executiveSummary": "Nigeria published its National AI Strategy in 2020 and has been one of Africa’s most active countries on AI governance. The National Information Technology Development Agency (NITDA) has issued a draft AI Ethics Framework and is developing binding governance requirements. Nigeria’s Data Protection Act (NDP Act) 2023 replaced the earlier NDPR and applies to AI systems processing personal data. The Nigeria Data Protection Commission (NDPC) was established in 2023 as the independent enforcement body. Nigeria’s approach is evolving from policy to binding regulation, driven by its position as Africa’s largest digital economy.", + "practicalTakeaway": "If your company operates AI in Nigeria, the NDP Act 2023 is binding and applies to all AI processing of personal data. Register with the NDPC, implement consent mechanisms, and prepare for automated-decision-making rights. Nigeria’s AI governance framework is actively developing under NITDA, and the draft AI Ethics Framework signals the direction of future binding rules. As Africa’s largest digital economy, Nigeria’s governance approach will likely influence the broader continent. Establishing compliance now positions you for both Nigerian requirements and pan-African expansion.", + "regulations": [ + { + "name": "National AI Strategy", + "type": "national-strategy", + "status": "in-force", + "effectiveDate": "November 2020", + "effectiveDateISO": "2020-11-01", + "dateConfidence": "approximate", + "scope": "National policy framework for AI development, adoption, and governance in Nigeria.", + "obligations": [ + "Identifies priority sectors for AI: agriculture, healthcare, education, and public services", + "Promotes AI research and development through universities and NITDA programmes", + "Establishes ethical principles for AI: fairness, transparency, accountability, safety, and inclusivity", + "Recommends creation of an AI governance body and regulatory framework", + "Addresses digital infrastructure needs to support AI deployment" + ], + "maxPenalty": "Not applicable (national strategy, not a regulatory instrument)", + "industryTags": ["general"], + "sourceUrl": "https://nitda.gov.ng/", + "lastVerified": "March 2026" + }, + { + "name": "Nigeria Data Protection Act (NDP Act) 2023", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "June 2023", + "effectiveDateISO": "2023-06-01", + "dateConfidence": "approximate", + "scope": "All entities processing personal data of individuals in Nigeria, including for AI model training and deployment.", + "obligations": [ + "Obtain appropriate lawful basis for processing personal data for AI purposes, including consent for certain categories", + "Conduct data protection impact assessments for high-risk processing including AI systems at scale", + "Individuals have rights related to automated decision-making, including the right to meaningful information about the logic involved", + "Implement appropriate technical and organizational security measures for personal data in AI systems", + "Register as a data controller or processor with the NDPC" + ], + "maxPenalty": "Fines up to NGN 10 million (~$6,500) or 2% of annual gross revenue for major data controllers; additional penalties for serious violations", + "industryTags": ["general"], + "sourceUrl": "https://ndpc.gov.ng/", + "lastVerified": "March 2026" + }, + { + "name": "NITDA draft AI Ethics Framework", + "type": "voluntary-guideline", + "status": "proposed", + "effectiveDate": "Date TBD", + "effectiveDateISO": null, + "dateConfidence": "unknown", + "scope": "All organizations developing or deploying AI systems in Nigeria; draft under public consultation.", + "obligations": [ + "Apply ethical principles including fairness, transparency, accountability, privacy, and human oversight", + "Conduct impact assessments for AI systems with potential societal or individual effects", + "Implement bias detection and mitigation measures in AI systems", + "Maintain documentation of AI governance practices", + "Framework expected to become binding once finalized" + ], + "maxPenalty": "Currently voluntary; expected to include enforcement mechanisms when finalized", + "industryTags": ["general"], + "sourceUrl": "https://nitda.gov.ng/", + "lastVerified": "March 2026" + } + ], + "timeline": [], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "egypt", + "name": "Egypt", + "region": "africa", + "regulationCount": 3, + "hash": "sha256-a938b6cda9cab5728cf2afb6ff4287a4208b17293c5bcf56e92fbfcda52e6c9b", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-a938b6cda9cab5728cf2afb6ff4287a4208b17293c5bcf56e92fbfcda52e6c9b", + "regulationCount": 3 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/egypt", + "flag": "🇪🇬", + "oneLiner": "National AI strategy launched with ethics charter — binding rules not yet in place.", + "executiveSummary": "Egypt published its National AI Strategy in 2021, developed by the National Council for AI under the Presidency. The strategy covers AI development, ethics, capacity building, and governance. Egypt enacted a Data Protection Law (Law 151/2020) that applies to AI systems processing personal data, though the implementing regulations have been slow to materialize. The National Council for AI has published an AI Ethics Charter. Egypt’s approach is policy-driven with an emerging regulatory framework. The government is investing in AI infrastructure and talent, positioning Egypt as a regional AI hub in Africa and the Middle East.", + "practicalTakeaway": "If your company operates AI in Egypt, monitor the Data Protection Law’s implementing regulations, which will determine practical enforcement. Follow the National AI Ethics Charter as your governance baseline. Egypt’s strategic position as a regional AI hub means the governance framework will mature. Implement consent mechanisms and data security measures now to prepare for enforcement. If you serve Egyptian government clients, alignment with the National AI Strategy’s principles will be important for procurement and partnership opportunities.", + "regulations": [ + { + "name": "National AI Strategy", + "type": "national-strategy", + "status": "in-force", + "effectiveDate": "July 2021", + "effectiveDateISO": "2021-07-01", + "dateConfidence": "approximate", + "scope": "National policy framework for AI development, adoption, and governance in Egypt.", + "obligations": [ + "Promotes AI adoption across priority sectors: healthcare, agriculture, Arabic language processing, and government services", + "Develops AI talent through universities, training programmes, and international partnerships", + "Establishes ethical guidelines for AI including respect for human rights, transparency, and accountability", + "Invests in AI infrastructure including data centres and research facilities", + "Coordinates through the National Council for AI, chaired by the Cabinet" + ], + "maxPenalty": "Not applicable (national strategy, not a regulatory instrument)", + "industryTags": ["general"], + "sourceUrl": "https://mcit.gov.eg/en/Artificial_Intelligence", + "lastVerified": "March 2026" + }, + { + "name": "Data Protection Law (Law 151/2020)", + "type": "binding-law", + "status": "passed-not-active", + "effectiveDate": "Expected upon implementing regulations", + "effectiveDateISO": null, + "dateConfidence": "unknown", + "scope": "All entities processing personal data in Egypt, including for AI model training and automated decision-making.", + "obligations": [ + "Obtain consent before processing personal data for AI purposes, with enhanced requirements for sensitive data", + "Individuals have the right to know about automated processing of their personal data", + "Implement security measures for personal data used in AI systems", + "Data protection impact assessments expected for high-risk processing under implementing regulations", + "Cross-border data transfers require authorization from the data protection centre" + ], + "maxPenalty": "Fines from EGP 100,000 to EGP 5 million (~$3,200 to $162,000); criminal penalties including imprisonment for serious violations", + "industryTags": ["general"], + "sourceUrl": "https://mcit.gov.eg/en/Publication/Publication_Summary/9341", + "lastVerified": "March 2026" + }, + { + "name": "National AI Ethics Charter", + "type": "voluntary-guideline", + "status": "voluntary", + "effectiveDate": "November 2023", + "effectiveDateISO": "2023-11-01", + "dateConfidence": "approximate", + "scope": "Government agencies and organizations developing or deploying AI in Egypt.", + "obligations": [ + "Apply ethical principles including transparency, fairness, accountability, and human well-being", + "Ensure AI systems respect Egyptian cultural values and social norms", + "Implement human oversight for AI systems affecting individual rights", + "Promote inclusive AI development that benefits all segments of Egyptian society" + ], + "maxPenalty": "No penalties (voluntary charter); adoption supports alignment with government expectations", + "industryTags": ["general"], + "sourceUrl": "https://mcit.gov.eg/en/Artificial_Intelligence", + "lastVerified": "March 2026" + } + ], + "timeline": [], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "mauritius", + "name": "Mauritius", + "region": "africa", + "regulationCount": 3, + "hash": "sha256-83e760993d0357c5c72d475e0b5f2ea1e38d862a57b29d55a2cdbf5b5b23d584", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-83e760993d0357c5c72d475e0b5f2ea1e38d862a57b29d55a2cdbf5b5b23d584", + "regulationCount": 3 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/mauritius", + "flag": "🇲🇺", + "oneLiner": "First African country with a national AI strategy and AI Council — voluntary framework in effect.", + "executiveSummary": "Mauritius was one of the first African countries to publish a National AI Strategy (2018) and establish an AI Council. The strategy positions Mauritius as an AI hub for Africa and the Indian Ocean region. Mauritius’s Data Protection Act 2017, enforced by the Data Protection Office, applies to AI systems processing personal data and includes provisions on automated decision-making. The Mauritius AI Council has published guidance on responsible AI development and deployment. The country’s approach is pragmatic, combining voluntary AI governance principles with binding data protection enforcement.", + "practicalTakeaway": "If your company operates AI in Mauritius, the Data Protection Act 2017 is binding and includes provisions on automated decision-making. Register your data processing with the Data Protection Office and implement consent mechanisms. Follow the AI Council’s guidance as your governance baseline. Mauritius’s positioning as a regional AI hub means the governance framework will likely strengthen. For companies targeting the African market, Mauritius’s governance standards serve as a useful benchmark.", + "regulations": [ + { + "name": "National AI Strategy", + "type": "national-strategy", + "status": "in-force", + "effectiveDate": "October 2018", + "effectiveDateISO": "2018-10-01", + "dateConfidence": "approximate", + "scope": "National policy framework for AI development, adoption, and governance in Mauritius.", + "obligations": [ + "Positions Mauritius as a regional AI hub for Africa and the Indian Ocean region", + "Identifies priority sectors: financial services, healthcare, agriculture, and public services", + "Promotes AI research and talent development through universities and international partnerships", + "Establishes the Mauritius AI Council to coordinate AI policy and governance", + "Develops ethical guidelines for responsible AI deployment" + ], + "maxPenalty": "Not applicable (national strategy, not a regulatory instrument)", + "industryTags": ["general"], + "sourceUrl": "https://ncb.govmu.org/", + "lastVerified": "March 2026" + }, + { + "name": "Data Protection Act 2017", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "January 2018", + "effectiveDateISO": "2018-01-01", + "dateConfidence": "approximate", + "scope": "All entities processing personal data in Mauritius, including for AI model training and automated decision-making.", + "obligations": [ + "Register data processing operations with the Data Protection Office", + "Obtain consent before processing personal data for AI purposes; explicit consent for sensitive data", + "Individuals have the right not to be subject to decisions based solely on automated processing that significantly affects them", + "Implement appropriate technical and organizational security measures for personal data in AI systems", + "Individuals have rights to access, rectify, and object to processing of personal data" + ], + "maxPenalty": "Fines up to MUR 200,000 (~$4,400) and/or imprisonment up to 5 years per violation", + "industryTags": ["general"], + "sourceUrl": "https://dataprotection.govmu.org/", + "lastVerified": "March 2026" + }, + { + "name": "Mauritius AI Council guidance", + "type": "voluntary-guideline", + "status": "voluntary", + "effectiveDate": "March 2022", + "effectiveDateISO": "2022-03-01", + "dateConfidence": "approximate", + "scope": "Organizations developing or deploying AI in Mauritius; voluntary guidance from the AI Council.", + "obligations": [ + "Apply ethical principles including transparency, fairness, accountability, and human well-being", + "Conduct impact assessments for AI systems with potential significant effects on individuals or society", + "Implement human oversight for AI decision-making in high-risk applications", + "Promote AI literacy and public engagement on AI governance" + ], + "maxPenalty": "No penalties (voluntary guidance); adoption demonstrates governance maturity", + "industryTags": ["general"], + "sourceUrl": "https://ncb.govmu.org/", + "lastVerified": "March 2026" + } + ], + "timeline": [], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "new-zealand", + "name": "New Zealand", + "region": "oceania", + "regulationCount": 4, + "hash": "sha256-d2649c6dfe39bb57393ac42e577f91031cf4b4cd36eb475a93c7fd5e57b088e1", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-d2649c6dfe39bb57393ac42e577f91031cf4b4cd36eb475a93c7fd5e57b088e1", + "regulationCount": 4 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/new-zealand", + "flag": "🇳🇿", + "oneLiner": "Algorithm Charter governs public-sector AI — broader mandatory rules under consideration.", + "executiveSummary": "New Zealand has been a leader in algorithmic transparency through the Algorithm Charter for Aotearoa New Zealand, which commits government agencies to transparent, accountable use of algorithms. The government has published AI guidance and principles. The Privacy Act 2020 applies to AI systems processing personal information and includes provisions on automated decision-making. The Office of the Privacy Commissioner has issued specific guidance on AI and privacy. New Zealand’s approach is principles-based, with strong public-sector accountability mechanisms and active privacy enforcement. The government has signaled interest in additional AI governance measures, monitoring developments in Australia and internationally.", + "practicalTakeaway": "If your company operates AI in New Zealand, the Privacy Act 2020 is binding and the Privacy Commissioner actively issues AI-specific guidance. Conduct privacy impact assessments for AI systems processing personal information. If you sell AI to government agencies, the Algorithm Charter sets transparency and accountability expectations that are effectively requirements for procurement. New Zealand’s emphasis on cultural considerations, including Māori data sovereignty, is distinctive — address this in your governance practices. Monitor developments as New Zealand may adopt additional governance measures aligned with Australia’s evolving framework.", + "regulations": [ + { + "name": "Algorithm Charter for Aotearoa New Zealand", + "type": "voluntary-guideline", + "status": "in-force", + "effectiveDate": "July 2020", + "effectiveDateISO": "2020-07-01", + "dateConfidence": "approximate", + "scope": "New Zealand government agencies that use algorithms and AI systems in decision-making and service delivery.", + "obligations": [ + "Commit to transparency: publish information about how algorithms are used in government decisions", + "Identify and manage potential bias in algorithms used for government services", + "Provide reasonable opportunity for individuals to understand and challenge algorithmic decisions", + "Indicate how algorithms are assessed for compliance with the Treaty of Waitangi, Privacy Act, Human Rights Act, and other legislation", + "Maintain human oversight: ensure algorithms support but do not replace human decision-making on matters affecting individuals’ rights" + ], + "maxPenalty": "No direct penalties (voluntary charter); signatory agencies publicly commit and are accountable through reporting mechanisms", + "industryTags": ["public-sector"], + "sourceUrl": "https://data.govt.nz/toolkit/data-ethics/government-algorithm-transparency-and-accountability/algorithm-charter", + "lastVerified": "March 2026" + }, + { + "name": "Privacy Act 2020 — AI provisions", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "December 2020", + "effectiveDateISO": "2020-12-01", + "dateConfidence": "approximate", + "scope": "All agencies (government and private sector) collecting, using, or disclosing personal information in New Zealand, including for AI systems.", + "obligations": [ + "Collect personal information for AI purposes only where necessary for a lawful purpose connected with the agency’s functions", + "Inform individuals about the purposes for which their personal information will be used in AI systems", + "Take reasonable steps to ensure personal information used in AI systems is accurate, complete, and up to date", + "Mandatory breach notification to the Privacy Commissioner for breaches that pose a risk of harm", + "Individuals have the right to access and correct personal information held in AI systems" + ], + "maxPenalty": "Privacy Commissioner can issue compliance notices; fines up to NZD $10,000 per offence for non-compliance with notices; Human Rights Review Tribunal can award damages", + "industryTags": ["general"], + "sourceUrl": "https://www.privacy.org.nz/privacy-act-2020/", + "lastVerified": "March 2026" + }, + { + "name": "Office of the Privacy Commissioner — AI guidance", + "type": "sector-regulation", + "status": "in-force", + "effectiveDate": "August 2023", + "effectiveDateISO": "2023-08-01", + "dateConfidence": "approximate", + "scope": "All organizations using AI systems that process personal information in New Zealand.", + "obligations": [ + "Conduct privacy impact assessments before deploying AI that processes personal information", + "Ensure transparency about AI use, including telling individuals when AI is used to make decisions about them", + "Implement measures to address bias in AI training data and model outputs", + "Consider the cultural context, including Māori data sovereignty principles, in AI design and deployment" + ], + "maxPenalty": "Enforcement under the Privacy Act 2020; compliance notices and potential damages", + "industryTags": ["general"], + "sourceUrl": "https://www.privacy.org.nz/resources-and-learning/a-z-topics/ai/", + "lastVerified": "March 2026" + }, + { + "name": "Government AI guidance and principles", + "type": "voluntary-guideline", + "status": "in-force", + "effectiveDate": "March 2024", + "effectiveDateISO": "2024-03-01", + "dateConfidence": "approximate", + "scope": "New Zealand government agencies considering or implementing AI in public services.", + "obligations": [ + "Apply principles of accountability, transparency, fairness, and privacy protection in AI systems", + "Assess AI systems for risks including bias, privacy impacts, and effects on vulnerable populations", + "Ensure procurement of AI systems includes governance and accountability requirements in contracts", + "Maintain human oversight for AI-assisted government decisions affecting individual rights" + ], + "maxPenalty": "No direct penalties (government guidance); non-compliance may result in judicial review of government decisions", + "industryTags": ["public-sector"], + "sourceUrl": "https://data.govt.nz/toolkit/data-ethics/", + "lastVerified": "March 2026" + } + ], + "timeline": [ + { + "date": "July 2020", + "description": "Algorithm Charter for Aotearoa New Zealand launched." + }, + { + "date": "December 2020", + "description": "Privacy Act 2020 takes effect with enhanced breach notification and enforcement." + }, + { + "date": "August 2023", + "description": "Office of the Privacy Commissioner publishes AI-specific guidance." + }, + { + "date": "March 2024", + "description": "Government publishes updated AI guidance and principles." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "spain", + "name": "Spain", + "region": "europe", + "regulationCount": 3, + "hash": "sha256-a68200c1d1c2edba8586471a7f70849abd08c6047a51bcbf6440d8138c6c328a", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-a68200c1d1c2edba8586471a7f70849abd08c6047a51bcbf6440d8138c6c328a", + "regulationCount": 3 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/spain", + "flag": "🇪🇸", + "oneLiner": "EU AI Act applies directly while Spain stands up AESIA and advances its own national AI governance law.", + "executiveSummary": "As an EU member state, Spain is directly bound by the EU AI Act (Regulation 2024/1689), whose obligations phase in through 2026 and 2027. Spain was the first EU country to create a dedicated AI regulator, the Agencia Española de Supervisión de la Inteligencia Artificial (AESIA), headquartered in A Coruña and now operational. In December 2025 AESIA published 16 compliance guides interpreting the EU AI Act for the Spanish market. Spain is also advancing its own national statute, the draft Organic Law for the good use and governance of AI, which cleared the Council of Ministers and was sent to Congress on 26 May 2026; it designates national supervisory authorities and sets penalties aligned to the EU AI Act tiers. The country also runs an AI regulatory sandbox under Royal Decree 817/2023, and the data-protection authority (AEPD) actively enforces against AI systems that process personal data.", + "practicalTakeaway": "If your company builds or deploys AI systems or operates in Spain, treat the EU AI Act as your binding baseline, map any high-risk use cases now, watch AESIA's guidance and the pending national governance law for Spain-specific supervisory and content-labelling duties, and consider the national sandbox to validate compliance before the 2026–2027 deadlines.", + "regulations": [ + { + "name": "EU AI Act (directly applicable in Spain)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "August 2024", + "effectiveDateISO": "2024-08-01", + "dateConfidence": "approximate", + "scope": "Directly applicable in Spain to providers, deployers, importers and distributors of AI systems and general-purpose AI models, on a risk-based basis.", + "obligations": [ + "Comply with prohibited-practice bans (in force since February 2025) and AI-literacy duties", + "Meet general-purpose AI model obligations (in force since August 2025), including technical documentation and copyright policy", + "Run conformity assessments, risk management and human oversight for high-risk AI systems before market placement", + "Register high-risk systems in the EU database and apply CE marking where required", + "Provide transparency notices for chatbots, emotion recognition and AI-generated or manipulated content (deepfakes)" + ], + "maxPenalty": "Up to €35 million or 7% of total worldwide annual turnover (whichever is higher) for prohibited-practice breaches (EU AI Act penalties)", + "industryTags": [ + "general", + "healthcare", + "hr-employment", + "public-sector", + "financial-services" + ], + "sourceUrl": "https://eur-lex.europa.eu/eli/reg/2024/1689/oj", + "lastVerified": "June 2026" + }, + { + "name": "Draft Organic Law for the good use and governance of AI", + "type": "draft-bill", + "status": "proposed", + "effectiveDate": "Date TBD (sent to Congress May 2026)", + "effectiveDateISO": "2026-05-01", + "dateConfidence": "approximate", + "scope": "National framework adapting the EU AI Act to Spanish law: designates supervisory authorities, defines the sanctions regime, and adds national transparency duties such as labelling of AI-generated content.", + "obligations": [ + "Label AI-generated or AI-manipulated audiovisual content to combat disinformation", + "Submit non-product-sector high-risk AI (employment, biometrics, education) to AESIA supervision", + "Coordinate with AEPD, the Bank of Spain, CNMV and the CGPJ as sector supervisors depending on domain", + "Use the national AI regulatory sandbox operated by AESIA for testing high-risk systems", + "Observe bans on subliminal manipulation and unlawful biometric categorisation mirrored from the EU AI Act" + ], + "maxPenalty": "Aligned to EU AI Act tiers — up to €35 million or 7% of worldwide annual turnover for the most serious infringements (subject to final parliamentary text)", + "industryTags": ["general", "hr-employment", "public-sector", "financial-services"], + "sourceUrl": "https://aesia.digital.gob.es/en/es", + "lastVerified": "June 2026" + }, + { + "name": "Royal Decree 817/2023 — National AI Regulatory Sandbox", + "type": "sector-regulation", + "status": "in-force", + "effectiveDate": "November 2023", + "effectiveDateISO": "2023-11-01", + "dateConfidence": "approximate", + "scope": "Establishes a controlled testing environment to trial compliance with EU AI Act requirements for high-risk AI systems; open to AI providers and public and private deployers.", + "obligations": [ + "Apply within the published call window to participate in the supervised testing environment", + "Perform self-assessment of high-risk AI conformity requirements during the trial", + "Evaluate the post-market monitoring plan for participating AI systems", + "Contribute to good-practice reports and technical implementation guides produced by the programme" + ], + "maxPenalty": "No direct fines (voluntary participation framework); the sandbox does not exempt participants from EU AI Act duties", + "industryTags": ["general", "public-sector"], + "sourceUrl": "https://www.boe.es/diario_boe/txt.php?id=BOE-A-2023-22767", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "November 2023", + "description": "Royal Decree 817/2023 launches Spain's national AI regulatory sandbox." + }, + { + "date": "August 2024", + "description": "EU AI Act enters into force, directly applicable in Spain." + }, + { + "date": "March 2025", + "description": "Council of Ministers approves the draft Organic Law on the good use and governance of AI (first reading)." + }, + { + "date": "December 2025", + "description": "AESIA publishes 16 EU AI Act compliance guides." + }, + { + "date": "May 2026", + "description": "Council of Ministers sends the AI governance bill to Congress, starting parliamentary procedure." + }, + { + "date": "August 2026", + "description": "EU AI Act high-risk system obligations begin to apply." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "switzerland", + "name": "Switzerland", + "region": "europe", + "regulationCount": 4, + "hash": "sha256-09739ca651f597dac4187c004bdce1c5260b8be876b66dcee1b27dcd5cb4228c", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-09739ca651f597dac4187c004bdce1c5260b8be876b66dcee1b27dcd5cb4228c", + "regulationCount": 4 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/switzerland", + "flag": "🇨🇭", + "oneLiner": "Switzerland skips a horizontal AI law, opting for sector-specific rules plus ratification of the Council of Europe AI treaty.", + "executiveSummary": "Switzerland has deliberately chosen not to enact a single, horizontal AI statute. On 12 February 2025 the Federal Council decided on a sector-specific regulatory approach, keeping AI oversight within existing domain laws and limiting cross-sector rules mainly to areas touching fundamental rights such as data protection. At the same time it decided to ratify the Council of Europe Framework Convention on Artificial Intelligence (CETS 225), which Switzerland signed on 27 March 2025. The Federal Department of Justice and Police, with DETEC and the FDFA, must prepare an implementation bill and send it for consultation by the end of 2026. In the meantime, the revised Federal Act on Data Protection (FADP, in force since 1 September 2023), enforced by the Federal Data Protection and Information Commissioner (FDPIC/EDÖB), governs AI that processes personal data, and sector regulators such as FINMA address AI in their domains.", + "practicalTakeaway": "If your company develops or uses AI or operates in Switzerland, comply with the revised FADP for any personal-data processing, follow your sector regulator's expectations (such as FINMA in finance), and track the Federal Council's implementation bill — due for consultation by end 2026 — that will translate the Council of Europe AI Convention into binding Swiss law.", + "regulations": [ + { + "name": "Federal Council decision on AI regulation (sector-specific approach)", + "type": "national-strategy", + "status": "policy-only", + "effectiveDate": "February 2025", + "effectiveDateISO": "2025-02-01", + "dateConfidence": "approximate", + "scope": "Sets Switzerland's overall direction: no horizontal AI act; targeted adjustments within existing sector laws, with cross-cutting rules focused on fundamental rights and data protection.", + "obligations": [ + "Comply with existing sector-specific laws as applied to AI rather than a single AI statute", + "Prepare for forthcoming legal adjustments implementing the Council of Europe AI Convention", + "Prioritise fundamental-rights and data-protection considerations in AI deployments", + "Monitor the implementation bill expected for consultation by the end of 2026" + ], + "maxPenalty": "No standalone AI penalties; enforcement runs through existing sectoral and data-protection law", + "industryTags": ["general", "public-sector", "financial-services"], + "sourceUrl": "https://www.admin.ch/en/nsb?id=104110", + "lastVerified": "June 2026" + }, + { + "name": "Council of Europe Framework Convention on AI (CETS 225) — signed by Switzerland", + "type": "binding-law", + "status": "passed-not-active", + "effectiveDate": "Date TBD (signed March 2025; implementation bill due end 2026)", + "effectiveDateISO": "2025-03-01", + "dateConfidence": "approximate", + "scope": "First binding international AI treaty; once ratified and implemented, it binds Swiss public-sector AI activities and private actors per the implementing law, ensuring AI respects human rights, democracy and the rule of law.", + "obligations": [ + "Ensure AI lifecycle activities are consistent with human rights, democracy and the rule of law", + "Provide accountability, transparency and oversight mechanisms for AI systems", + "Enable remedies and procedural safeguards for persons affected by AI", + "Carry out risk and impact assessments as specified by implementing Swiss legislation" + ], + "maxPenalty": "To be determined by the Swiss implementing legislation (the Convention itself sets no fixed penalties)", + "industryTags": ["general", "public-sector"], + "sourceUrl": "https://www.coe.int/en/web/artificial-intelligence/the-framework-convention-on-artificial-intelligence", + "lastVerified": "June 2026" + }, + { + "name": "Federal Act on Data Protection (revised FADP / nFADP)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "September 2023", + "effectiveDateISO": "2023-09-01", + "dateConfidence": "approximate", + "scope": "Switzerland's data-protection law, enforced by the FDPIC (EDÖB); applies to AI systems that process personal data, including automated decision-making and profiling.", + "obligations": [ + "Provide transparency and information about automated individual decisions with significant effects", + "Conduct data protection impact assessments for high-risk processing, including certain AI use", + "Apply data-protection-by-design and by-default to AI systems", + "Maintain records of processing activities and report data breaches to the FDPIC", + "Respect data-subject rights of access, objection and human review where applicable" + ], + "maxPenalty": "Fines up to CHF 250,000 against responsible private individuals for certain violations", + "industryTags": ["general", "financial-services", "healthcare", "hr-employment"], + "sourceUrl": "https://www.edoeb.admin.ch/en", + "lastVerified": "June 2026" + }, + { + "name": "FINMA supervision of AI in financial services", + "type": "sector-regulation", + "status": "in-force", + "effectiveDate": "Ongoing (Guidance 08/2024 and supervisory practice)", + "effectiveDateISO": "2024-01-01", + "dateConfidence": "approximate", + "scope": "FINMA addresses AI governance, risk management and accountability for supervised banks, insurers and other financial institutions through existing prudential rules and guidance.", + "obligations": [ + "Establish clear governance and accountability for AI models used in financial services", + "Manage model risk, including data quality, robustness and explainability", + "Ensure human oversight and controls over AI-driven decisions", + "Monitor for and document AI-related operational and reputational risks" + ], + "maxPenalty": "FINMA supervisory measures (enforcement actions, licence restrictions); no fixed AI fine schedule", + "industryTags": ["financial-services", "insurance"], + "sourceUrl": "https://www.finma.ch/en/", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "September 2023", + "description": "Revised Federal Act on Data Protection (FADP) enters into force." + }, + { + "date": "September 2024", + "description": "Council of Europe opens the Framework Convention on AI (CETS 225) for signature." + }, + { + "date": "February 2025", + "description": "Federal Council adopts a sector-specific AI approach and decides to ratify the CoE AI Convention." + }, + { + "date": "March 2025", + "description": "Switzerland signs the Council of Europe Framework Convention on AI." + }, + { + "date": "End of 2026", + "description": "Implementation bill due to be sent for public consultation." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "turkey", + "name": "Turkey", + "region": "europe", + "regulationCount": 3, + "hash": "sha256-89bf5f4e0fe4f115db881e9adb08b6cbf5d41ba0619133345feb7e29088cbf1b", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-89bf5f4e0fe4f115db881e9adb08b6cbf5d41ba0619133345feb7e29088cbf1b", + "regulationCount": 3 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/turkey", + "flag": "🇹🇷", + "oneLiner": "Turkey has a risk-based AI law bill in parliament but still relies on data-protection law and a national AI strategy.", + "executiveSummary": "Turkey has no comprehensive AI law in force. A risk-based Artificial Intelligence Law Bill (Yapay Zeka Kanun Teklifi, TBMM Esas No. 2/2234) was submitted to the Grand National Assembly on 24 June 2024 and remains under parliamentary commission review, with a further AI-related amendment bill (Esas No. 2/3358) submitted in November 2025. The draft sets out principles of safety, transparency, fairness, accountability and privacy, and would introduce risk management and conformity/registration duties for high-risk systems plus labelling of synthetic content. Until a law passes, AI is governed mainly by the Personal Data Protection Law (KVKK Law No. 6698), enforced by the data-protection authority KVKK, which addresses automated decision-making and profiling, with the telecoms regulator BTK relevant to network and telecommunications aspects. Policy direction is set by the National AI Strategy, co-developed by the Digital Transformation Office (CBDDO) and the Ministry of Industry and Technology.", + "practicalTakeaway": "If your company builds or deploys AI or operates in Turkey, comply now with the KVKK data-protection regime for any personal-data processing, watch the pending Artificial Intelligence Law Bill (2/2234) for risk-based conformity, registration and deepfake-labelling duties, and align public-facing projects with the National AI Strategy while monitoring KVKK and BTK guidance.", + "regulations": [ + { + "name": "Artificial Intelligence Law Bill (Yapay Zeka Kanun Teklifi, TBMM 2/2234)", + "type": "draft-bill", + "status": "proposed", + "effectiveDate": "Date TBD (submitted to TBMM June 2024)", + "effectiveDateISO": "2024-06-01", + "dateConfidence": "approximate", + "scope": "Proposed horizontal, risk-based framework for the safe, ethical and transparent development, deployment and use of AI systems, with stricter duties for high-risk systems.", + "obligations": [ + "Apply core principles of safety, transparency, fairness, accountability and privacy across the AI lifecycle", + "Implement risk management and conformity assessment for high-risk AI systems before deployment", + "Register high-risk AI systems as required by the framework", + "Label AI-generated or manipulated content (deepfakes) for transparency", + "Coordinate with KVKK on privacy and with BTK on telecommunications and network-integrity matters" + ], + "maxPenalty": "Draft provides significant administrative fines (fixed amounts and turnover-based percentages); exact figures provisional pending the final text", + "industryTags": ["general", "public-sector", "healthcare"], + "sourceUrl": "https://www.tbmm.gov.tr", + "lastVerified": "June 2026" + }, + { + "name": "Personal Data Protection Law (KVKK, Law No. 6698)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "April 2016 (amended 2024)", + "effectiveDateISO": "2016-04-01", + "dateConfidence": "approximate", + "scope": "Turkey's data-protection law, enforced by the KVKK authority; governs AI systems that process personal data, including automated decision-making and profiling.", + "obligations": [ + "Establish a lawful basis for processing personal data used to train or run AI systems", + "Inform data subjects and uphold their rights regarding automated decisions and profiling", + "Comply with rules on cross-border data transfers (revised under the 2024 amendments)", + "Implement adequate technical and organisational data-security measures", + "Register with VERBIS where required and meet reporting obligations to the KVKK authority" + ], + "maxPenalty": "Administrative fines under Law No. 6698 (periodically revalued); plus potential criminal liability for unlawful data processing", + "industryTags": ["general", "financial-services", "healthcare", "hr-employment"], + "sourceUrl": "https://www.kvkk.gov.tr/", + "lastVerified": "June 2026" + }, + { + "name": "National Artificial Intelligence Strategy 2021–2025", + "type": "national-strategy", + "status": "policy-only", + "effectiveDate": "August 2021", + "effectiveDateISO": "2021-08-01", + "dateConfidence": "approximate", + "scope": "National policy framework co-led by the Digital Transformation Office (CBDDO) and the Ministry of Industry and Technology, with annual action plans.", + "obligations": [ + "Align public-sector AI initiatives with the strategy's priorities and action plans", + "Support workforce, governance and ecosystem targets", + "Advance trustworthy and ethical AI principles in national projects", + "Coordinate implementation via the national AI steering committee" + ], + "maxPenalty": "No penalties (policy framework, not binding law)", + "industryTags": ["general", "public-sector"], + "sourceUrl": "https://cbddo.gov.tr/en/", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "August 2021", + "description": "National AI Strategy 2021–2025 published by CBDDO and the Ministry of Industry and Technology." + }, + { + "date": "June 2024", + "description": "Artificial Intelligence Law Bill (Esas No. 2/2234) submitted to the Grand National Assembly." + }, + { + "date": "2024", + "description": "KVKK data-protection amendments take effect, including revised cross-border transfer rules." + }, + { + "date": "November 2025", + "description": "Further AI-related amendment bill (Esas No. 2/3358) submitted to parliament." + }, + { + "date": "Mid-2026", + "description": "AI Law Bill remains under commission review; no unified AI law yet in force." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "rwanda", + "name": "Rwanda", + "region": "africa", + "regulationCount": 2, + "hash": "sha256-9451648a5039e21da3b67db528d245874b4e57e11d4e1342a49025dcea30ca0f", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-9451648a5039e21da3b67db528d245874b4e57e11d4e1342a49025dcea30ca0f", + "regulationCount": 2 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/rwanda", + "flag": "🇷🇼", + "oneLiner": "Rwanda leads Africa with a comprehensive national AI policy, governing AI mainly through strategy and existing data-protection law.", + "executiveSummary": "Rwanda became the first African country to adopt a comprehensive National AI Policy, approved by Cabinet on 20 April 2023 and led by the Ministry of ICT and Innovation (MINICT). The policy is a strategy and ethics framework aligned with UNESCO's Recommendation on the Ethics of AI rather than a binding AI law, setting priorities across skills, infrastructure, data, and trustworthy public- and private-sector adoption. There is currently no dedicated binding AI statute; AI activities that process personal data fall under Law No. 058/2021 relating to the protection of personal data and privacy, in force since 15 October 2021. That law is enforced by the National Cyber Security Authority through its Data Protection Office and carries administrative and criminal penalties. Rwanda has signalled it will establish a dedicated national AI agency to operationalise the policy.", + "practicalTakeaway": "If your company builds or deploys AI that processes personal data and operates in Rwanda, treat the National AI Policy as directional guidance while ensuring hard compliance with Law No. 058/2021 — register with the Data Protection Office, secure a lawful basis, honour data-subject rights, and prepare breach-notification procedures.", + "regulations": [ + { + "name": "National AI Policy", + "type": "national-strategy", + "status": "policy-only", + "effectiveDate": "April 2023", + "effectiveDateISO": "2023-04-01", + "dateConfidence": "approximate", + "scope": "Whole-of-government policy framework guiding AI adoption across public and private sectors; sets vision, ethics principles, and six priority areas. Not legally binding on private actors.", + "obligations": [ + "Adopt trustworthy, human-centred and ethical AI aligned with UNESCO AI ethics principles", + "Prioritise AI skills development and high AI literacy across the workforce", + "Build reliable compute infrastructure and a robust national data strategy", + "Encourage responsible AI adoption in the public sector with appropriate oversight", + "Promote widely beneficial and inclusive private-sector AI adoption" + ], + "maxPenalty": "None — non-binding national policy with no statutory penalties", + "industryTags": ["general", "public-sector"], + "sourceUrl": "https://www.minict.gov.rw/ai-policy", + "lastVerified": "June 2026" + }, + { + "name": "Law No. 058/2021 on the Protection of Personal Data and Privacy", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "October 2021", + "effectiveDateISO": "2021-10-01", + "dateConfidence": "approximate", + "scope": "Governs processing of personal data by public and private bodies in Rwanda, including AI systems that collect or process personal data. Enforced by the National Cyber Security Authority (NCSA) via the Data Protection Office.", + "obligations": [ + "Register as a data controller or processor with the supervisory authority", + "Establish a lawful basis and obtain consent where required for processing", + "Honour data-subject rights including access, rectification and objection", + "Notify the authority and affected subjects of personal-data breaches", + "Apply data-security and accountability safeguards to automated processing", + "Meet conditions for cross-border transfers of personal data" + ], + "maxPenalty": "Administrative fine of RWF 2–5 million or up to 1% of prior-year global turnover; criminal offences punishable by 1–3 years imprisonment and a RWF 7–10 million fine", + "industryTags": ["general", "financial-services", "healthcare", "public-sector"], + "sourceUrl": "https://dpo.gov.rw/legal-framework", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "October 2021", + "description": "Law No. 058/2021 on the protection of personal data and privacy enters into force." + }, + { + "date": "April 2023", + "description": "Cabinet approves the National AI Policy — the first comprehensive national AI policy by an African country." + }, + { + "date": "2023–2024", + "description": "MINICT begins implementing the policy's six priority areas with partners including C4IR Rwanda." + }, + { + "date": "October 2025", + "description": "Data-protection law's transition period concludes; full compliance with Law No. 058/2021 expected." + }, + { + "date": "2026", + "description": "Government signals establishment of a dedicated national AI agency to operationalise the policy." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "south-africa", + "name": "South Africa", + "region": "africa", + "regulationCount": 2, + "hash": "sha256-5d3d7de8dab2ce7eef38c4bc1c164eae30c0eec75b5b2d60a4e39157b8b5f829", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-5d3d7de8dab2ce7eef38c4bc1c164eae30c0eec75b5b2d60a4e39157b8b5f829", + "regulationCount": 2 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/south-africa", + "flag": "🇿🇦", + "oneLiner": "South Africa governs AI through a draft national policy framework plus binding data-protection rules on automated decisions.", + "executiveSummary": "South Africa published its National AI Policy Framework through the Department of Communications and Digital Technologies (DCDT) in August 2024, intended as a principle- and risk-based precursor to a full national AI policy and future legislation. The framework is not yet binding law; it emphasises human-centred AI, alignment with the Protection of Personal Information Act (POPIA), risk-tiering of high-impact systems, and multi-stakeholder governance. In the meantime, AI that makes decisions about individuals is already regulated: POPIA has been fully in force since 1 July 2021, and section 71 restricts decisions based solely on automated processing, including profiling. POPIA is enforced by the Information Regulator, which can issue enforcement and infringement notices and has begun levying fines.", + "practicalTakeaway": "If your company deploys AI that profiles people or makes automated decisions and operates in South Africa, comply with POPIA now — especially section 71 safeguards on solely-automated decisions, breach notification, and Information Officer registration — while monitoring the DCDT AI Policy Framework as it advances toward binding law.", + "regulations": [ + { + "name": "National AI Policy Framework", + "type": "national-strategy", + "status": "proposed", + "effectiveDate": "August 2024", + "effectiveDateISO": "2024-08-01", + "dateConfidence": "approximate", + "scope": "Foundational, principle- and risk-based roadmap to guide South Africa's AI policy and future legislation across sectors. Non-binding precursor document released by the DCDT.", + "obligations": [ + "Adopt human-centred, ethical and transparent AI principles", + "Align AI governance with existing data-protection law (POPIA)", + "Apply risk-tiering and additional scrutiny to high-impact AI systems", + "Invest in skills, research and innovation capacity", + "Use multi-stakeholder governance and coordinate with the Information Regulator" + ], + "maxPenalty": "None — non-binding policy framework with no statutory penalties (statute pending)", + "industryTags": ["general", "public-sector"], + "sourceUrl": "https://www.dcdt.gov.za/sa-national-ai-policy-framework.html", + "lastVerified": "June 2026" + }, + { + "name": "Protection of Personal Information Act (POPIA, Act 4 of 2013)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "July 2021", + "effectiveDateISO": "2021-07-01", + "dateConfidence": "approximate", + "scope": "Governs processing of personal information by responsible parties in South Africa; section 71 specifically regulates decisions based solely on automated processing and profiling. Enforced by the Information Regulator.", + "obligations": [ + "Process personal information lawfully under the eight POPIA conditions", + "Restrict decisions based solely on automated processing that have legal or significant effects (s71)", + "Provide data subjects with safeguards, including the right to make representations on automated decisions", + "Honour data-subject rights of access, correction and objection", + "Notify the Information Regulator and data subjects of security compromises", + "Appoint and register an Information Officer and meet cross-border transfer conditions" + ], + "maxPenalty": "Administrative fine of up to R10 million (s109); criminal offences punishable by a fine of up to R10 million and/or imprisonment of up to 10 years (s107)", + "industryTags": [ + "general", + "financial-services", + "healthcare", + "insurance", + "hr-employment", + "public-sector" + ], + "sourceUrl": "https://inforegulator.org.za/protection-of-personal-information-act-4-of-2013/", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "July 2021", + "description": "POPIA becomes fully enforceable, including section 71 on automated decision-making." + }, + { + "date": "August 2024", + "description": "DCDT publishes the National AI Policy Framework as a precursor to full AI policy and legislation." + }, + { + "date": "2024–2025", + "description": "Public comment on the draft framework; the Information Regulator issues early POPIA fines." + }, + { + "date": "2025", + "description": "Framework refined toward a formal national AI policy and prospective statutory design." + }, + { + "date": "2026", + "description": "AI policy work continues; POPIA remains the binding instrument governing automated decisions." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "philippines", + "name": "Philippines", + "region": "asia", + "regulationCount": 2, + "hash": "sha256-2a953b45493fbd2201550df95e209c65680c7dc93cfc8c2ccbbb2cf7573eecb9", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-2a953b45493fbd2201550df95e209c65680c7dc93cfc8c2ccbbb2cf7573eecb9", + "regulationCount": 2 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/philippines", + "flag": "🇵🇭", + "oneLiner": "The Philippines steers AI through a national strategy and DTI roadmap while binding data-privacy law governs AI today.", + "executiveSummary": "The Philippines approved its National AI Strategy (NAIS-PH) when President Marcos endorsed it in May 2025, setting a whole-of-government roadmap to 2028 across infrastructure, workforce, innovation, ethics and priority-sector deployment. This builds on the Department of Trade and Industry's National AI Strategy Roadmap 2.0 (NAISR 2.0), adopted in July 2024, and the DTI-hosted Center for AI Research (CAIR). These instruments are strategy and policy, not binding AI law. The binding baseline is the Data Privacy Act of 2012 (RA 10173), enforced by the National Privacy Commission (NPC), which has issued advisories touching on AI and automated processing. Several AI-specific regulation bills remain pending in Congress.", + "practicalTakeaway": "If your company builds or uses AI that processes personal data and operates in the Philippines, comply with the Data Privacy Act now — secure a lawful basis, register processing systems, appoint a DPO, and follow NPC advisories on automated processing — while tracking NAIS-PH and pending AI bills for future binding obligations.", + "regulations": [ + { + "name": "National AI Strategy for the Philippines (NAIS-PH)", + "type": "national-strategy", + "status": "policy-only", + "effectiveDate": "May 2025", + "effectiveDateISO": "2025-05-01", + "dateConfidence": "approximate", + "scope": "Whole-of-government AI strategy and roadmap through 2028 covering infrastructure, workforce, innovation ecosystem, ethical frameworks and deployment in priority sectors such as healthcare and agriculture. Non-binding.", + "obligations": [ + "Pursue AI infrastructure and compute capacity development", + "Build workforce AI skills and reskilling programmes", + "Foster an AI innovation ecosystem and research via DTI's CAIR", + "Develop ethical and responsible AI policy frameworks", + "Target strategic AI deployment in priority sectors" + ], + "maxPenalty": "None — non-binding national strategy with no statutory penalties", + "industryTags": ["general", "public-sector", "healthcare"], + "sourceUrl": "https://www.dti.gov.ph/", + "lastVerified": "June 2026" + }, + { + "name": "Data Privacy Act of 2012 (Republic Act No. 10173)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "September 2012", + "effectiveDateISO": "2012-09-01", + "dateConfidence": "approximate", + "scope": "Governs processing of personal information by controllers and processors in the Philippines, including AI systems. Enforced by the National Privacy Commission (NPC), which has issued advisories on automated processing and AI.", + "obligations": [ + "Process personal data lawfully with consent or another lawful criterion", + "Uphold transparency, legitimate purpose and proportionality principles", + "Honour data-subject rights including access, correction and objection", + "Implement organisational, physical and technical security measures", + "Notify the NPC and affected subjects of qualifying personal-data breaches", + "Register data-processing systems and appoint a Data Protection Officer where required" + ], + "maxPenalty": "Imprisonment of up to 6 years and fines up to ₱5,000,000 for combined or serial violations; individual offences carry 1–7 years imprisonment and fines of ₱500,000–₱2,000,000", + "industryTags": [ + "general", + "financial-services", + "healthcare", + "hr-employment", + "public-sector" + ], + "sourceUrl": "https://lawphil.net/statutes/repacts/ra2012/ra_10173_2012.html", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "September 2012", + "description": "Data Privacy Act of 2012 (RA 10173) takes effect, creating the National Privacy Commission." + }, + { + "date": "July 2024", + "description": "DTI adopts the National AI Strategy Roadmap 2.0 (NAISR 2.0), updating the 2021 roadmap for generative AI." + }, + { + "date": "May 2025", + "description": "President Marcos approves the National AI Strategy for the Philippines (NAIS-PH), targeting 2028." + }, + { + "date": "2025", + "description": "NPC continues issuing advisories on AI and automated processing; AI regulation bills remain pending in Congress." + }, + { + "date": "2026", + "description": "Implementation of NAIS-PH proceeds; the Data Privacy Act remains the binding baseline for AI." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "netherlands", + "name": "Netherlands", + "region": "europe", + "regulationCount": 3, + "hash": "sha256-711c8cb12734d87393e1ba7d9bc290af56bc0f9d32566efc447fe33769496e6f", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-711c8cb12734d87393e1ba7d9bc290af56bc0f9d32566efc447fe33769496e6f", + "regulationCount": 3 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/netherlands", + "flag": "🇳🇱", + "oneLiner": "The EU AI Act applies directly, enforced alongside a Dutch algorithm watchdog hardened by the childcare-benefits scandal.", + "executiveSummary": "As an EU member state, the Netherlands is bound directly by the EU AI Act (Regulation 2024/1689), whose obligations phase in between 2025 and 2027. Dutch algorithmic oversight is coordinated by the Autoriteit Persoonsgegevens (AP), the data protection authority, which since January 2023 also hosts a dedicated algorithm watchdog, the Directie Coördinatie Algoritmes (DCA). The government maintains a public algorithm register at algoritmes.overheid.nl listing the automated systems used by public bodies. This strict, transparency-first posture was driven by the toeslagenaffaire (childcare-benefits scandal), in which a discriminatory risk-scoring algorithm wrongly accused thousands of families of fraud. The Netherlands has not yet formally designated its full set of national competent authorities for AI Act enforcement; an implementing act is moving through the legislative pipeline in 2026 and proposes a decentralised model with the AP in a coordinating role.", + "practicalTakeaway": "If your company builds or deploys AI systems and operates in the Netherlands, map your systems against the EU AI Act risk tiers, eliminate prohibited uses now, prepare high-risk documentation and transparency labelling, and watch the AP's guidance and the forthcoming implementing act for which Dutch regulator will supervise you.", + "regulations": [ + { + "name": "EU AI Act (directly applicable in the Netherlands)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "August 2024", + "effectiveDateISO": "2024-08-01", + "dateConfidence": "approximate", + "scope": "All providers, deployers, importers and distributors of AI systems placed on the EU market or affecting people in the EU, risk-tiered from prohibited to minimal.", + "obligations": [ + "Prohibited practices (social scoring, untargeted facial-recognition scraping) banned since February 2025", + "AI literacy duties for providers and deployers since February 2025", + "General-purpose AI model transparency and documentation obligations since August 2025", + "High-risk systems face conformity assessment, risk management, logging and human oversight duties", + "Article 50 transparency duties (labelling deepfakes and AI interactions) apply from August 2026" + ], + "maxPenalty": "Up to €35 million or 7% of total worldwide annual turnover, whichever is higher (EU AI Act penalties)", + "industryTags": ["general", "public-sector", "financial-services", "healthcare"], + "sourceUrl": "https://eur-lex.europa.eu/eli/reg/2024/1689/oj", + "lastVerified": "June 2026" + }, + { + "name": "Algorithm oversight by the Autoriteit Persoonsgegevens (DCA)", + "type": "sector-regulation", + "status": "in-force", + "effectiveDate": "January 2023", + "effectiveDateISO": "2023-01-01", + "dateConfidence": "approximate", + "scope": "Coordination of supervision over algorithms and AI used by public and private bodies, with a focus on transparency, non-discrimination and fundamental rights.", + "obligations": [ + "Signals and monitors risks from algorithms across sectors via the DCA coordination department", + "Promotes transparency and explainability of automated decision-making", + "Issues guidance and annual AI and algorithm risk reporting", + "Coordinates with sector regulators ahead of formal AI Act competent-authority designation" + ], + "maxPenalty": "No standalone algorithm fines; enforced via GDPR (up to €20 million or 4% of turnover) and, going forward, the EU AI Act", + "industryTags": ["general", "public-sector"], + "sourceUrl": "https://www.autoriteitpersoonsgegevens.nl/en/themes/algorithms-ai/eu-ai-act", + "lastVerified": "June 2026" + }, + { + "name": "Dutch government algorithm register", + "type": "national-strategy", + "status": "in-force", + "effectiveDate": "December 2022", + "effectiveDateISO": "2022-12-01", + "dateConfidence": "approximate", + "scope": "Public transparency register documenting the algorithms and automated systems used by Dutch government bodies.", + "obligations": [ + "Government bodies publish descriptions of impactful algorithms they use", + "Each entry covers purpose, data, oversight and the legal basis of the system", + "Supports public scrutiny and accountability of automated public-sector decisions", + "Aligns Dutch practice with EU AI Act transparency expectations for public bodies" + ], + "maxPenalty": "Transparency instrument; no direct penalties", + "industryTags": ["public-sector", "general"], + "sourceUrl": "https://algoritmes.overheid.nl/en", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "2019–2021", + "description": "The childcare-benefits scandal exposes a discriminatory fraud-risk algorithm, collapsing the cabinet in January 2021." + }, + { + "date": "January 2023", + "description": "Autoriteit Persoonsgegevens launches the Directie Coördinatie Algoritmes (DCA) algorithm watchdog." + }, + { + "date": "August 2024", + "description": "EU AI Act enters into force, directly binding the Netherlands." + }, + { + "date": "February 2025", + "description": "EU AI Act prohibited-practice ban and AI literacy duties begin to apply." + }, + { + "date": "2026", + "description": "Dutch implementing act advances to designate national competent authorities for AI Act enforcement." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "ireland", + "name": "Ireland", + "region": "europe", + "regulationCount": 3, + "hash": "sha256-3909d137d214b76928a81cf24a9e0102385f30c0042b9eec01694acbf0a5e066", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-3909d137d214b76928a81cf24a9e0102385f30c0042b9eec01694acbf0a5e066", + "regulationCount": 3 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/ireland", + "flag": "🇮🇪", + "oneLiner": "The EU AI Act applies directly, enforced through a distributed network of regulators with the data watchdog for Big Tech at its centre.", + "executiveSummary": "As an EU member state, Ireland is bound directly by the EU AI Act (Regulation 2024/1689), with obligations phasing in from 2025. In September 2025 Ireland became one of the first EU countries to designate its national competent authorities, choosing a distributed model that spreads enforcement across fifteen existing sectoral regulators, coordinated by the Department of Enterprise, Tourism and Employment. Because Google, Meta, TikTok and X all base their EU headquarters in Ireland, the Data Protection Commission (DPC) acts as a lead EU GDPR regulator for Big Tech and has become increasingly active on AI, publishing guidance on large language models and data protection. Ireland's national AI strategy, 'AI — Here for Good', was refreshed in November 2024 to add EU AI Act implementation governance.", + "practicalTakeaway": "If your company builds or deploys AI systems and operates in Ireland, identify which of the fifteen sectoral regulators supervises your use case, comply with the EU AI Act risk tiers, and treat the Data Protection Commission as a serious enforcer of GDPR over AI training and deployment, especially if you run an EU-headquartered platform.", + "regulations": [ + { + "name": "EU AI Act (directly applicable in Ireland)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "August 2024", + "effectiveDateISO": "2024-08-01", + "dateConfidence": "approximate", + "scope": "All providers, deployers, importers and distributors of AI systems placed on the EU market or affecting people in the EU, risk-tiered from prohibited to minimal.", + "obligations": [ + "Prohibited AI practices banned since February 2025", + "AI literacy duties for providers and deployers since February 2025", + "General-purpose AI model obligations since August 2025", + "High-risk systems face conformity assessment, logging and human-oversight duties", + "Article 50 transparency duties for deepfakes and AI interactions from August 2026" + ], + "maxPenalty": "Up to €35 million or 7% of total worldwide annual turnover, whichever is higher (EU AI Act penalties)", + "industryTags": ["general", "financial-services", "healthcare", "public-sector"], + "sourceUrl": "https://eur-lex.europa.eu/eli/reg/2024/1689/oj", + "lastVerified": "June 2026" + }, + { + "name": "Ireland's distributed AI Act competent authorities", + "type": "sector-regulation", + "status": "in-force", + "effectiveDate": "September 2025", + "effectiveDateISO": "2025-09-01", + "dateConfidence": "approximate", + "scope": "National enforcement of the EU AI Act distributed across fifteen existing sectoral regulators, coordinated by the Department of Enterprise, Tourism and Employment.", + "obligations": [ + "Fifteen regulators (including the Central Bank, Data Protection Commission, Coimisiún na Meán, HPRA) supervise AI within their sectors", + "A national single point of contact sits within the Department of Enterprise, Tourism and Employment", + "A national implementation committee coordinates cross-regulator enforcement", + "Designated bodies oversee market surveillance and fundamental-rights protection for high-risk AI" + ], + "maxPenalty": "Enforces EU AI Act penalties: up to €35 million or 7% of worldwide annual turnover", + "industryTags": ["general", "financial-services", "healthcare", "public-sector"], + "sourceUrl": "https://enterprise.gov.ie/en/news-and-events/department-news/2025/september/20250916.html", + "lastVerified": "June 2026" + }, + { + "name": "Data Protection Commission AI and LLM guidance", + "type": "voluntary-guideline", + "status": "in-force", + "effectiveDate": "July 2024", + "effectiveDateISO": "2024-07-01", + "dateConfidence": "approximate", + "scope": "Guidance on how data protection law (GDPR) applies to AI systems and large language models, from Ireland's lead EU regulator for Big Tech.", + "obligations": [ + "Establish a lawful basis before processing personal data to train or run AI and LLMs", + "Apply data-minimisation and transparency to AI training datasets", + "Honour data-subject rights including against automated decision-making", + "Conduct data protection impact assessments for high-risk AI processing" + ], + "maxPenalty": "GDPR fines up to €20 million or 4% of total worldwide annual turnover, whichever is higher", + "industryTags": ["general", "financial-services"], + "sourceUrl": "https://www.dataprotection.ie/en/dpc-guidance/blogs/AI-LLMs-and-Data-Protection", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "July 2021", + "description": "Ireland launches its national AI strategy, 'AI — Here for Good'." + }, + { + "date": "July 2024", + "description": "Data Protection Commission publishes guidance on AI, LLMs and data protection." + }, + { + "date": "August 2024", + "description": "EU AI Act enters into force, directly binding Ireland." + }, + { + "date": "November 2024", + "description": "National AI Strategy Refresh adds EU AI Act implementation governance." + }, + { + "date": "September 2025", + "description": "Ireland designates fifteen national competent authorities for AI Act enforcement under a distributed model." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "norway", + "name": "Norway", + "region": "europe", + "regulationCount": 3, + "hash": "sha256-c3dbe360c31dc7bee21a8f19b25ee06636ad7120085af5d96ca7c33d5d235cc2", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-c3dbe360c31dc7bee21a8f19b25ee06636ad7120085af5d96ca7c33d5d235cc2", + "regulationCount": 3 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/norway", + "flag": "🇳🇴", + "oneLiner": "An EEA member preparing a national AI law to mirror the EU AI Act, with a pioneering data-protection sandbox already running.", + "executiveSummary": "Norway is an EEA member, not an EU member, so the EU AI Act does not yet apply automatically; as of mid-2026 it has not been formally incorporated into the EEA Agreement and remains in the pre-incorporation pipeline awaiting a Joint Committee Decision. Rather than wait, Norway is legislating in parallel with a national AI act, consulted in 2025 and targeted to take effect around late summer 2026 to align with EU rules. Norway published a National Strategy for Artificial Intelligence in January 2020, and its data protection authority, Datatilsynet, runs a well-known regulatory sandbox for responsible AI, made a permanent offering in 2023. In March 2025 the government set out national governance for AI Act implementation, designating the Norwegian Communications Authority (Nkom) as coordinating market-surveillance authority.", + "practicalTakeaway": "If your company builds or deploys AI systems and operates in Norway, design now to EU AI Act standards because Norway is aligning to them, watch for the national AI act commencement around late summer 2026, and consider Datatilsynet's sandbox if you need regulatory clarity on a data-intensive AI product.", + "regulations": [ + { + "name": "EU AI Act via the EEA Agreement (pending incorporation)", + "type": "binding-law", + "status": "proposed", + "effectiveDate": "Date TBD", + "effectiveDateISO": null, + "dateConfidence": "unknown", + "scope": "The EU AI Act is expected to extend to Norway once incorporated into the EEA Agreement; until then it does not directly bind Norwegian actors.", + "obligations": [ + "Same risk-tiered duties as the EU regime would apply once incorporated", + "Norwegian providers selling into the EU are already in scope of the EU AI Act extraterritorially", + "Incorporation requires an EEA Joint Committee Decision, not yet adopted as of mid-2026", + "Norway is preparing national implementation in parallel rather than waiting on incorporation" + ], + "maxPenalty": "Reference EU AI Act cap is €35 million or 7% of worldwide turnover; Norwegian commencement and penalty regime not yet settled", + "industryTags": ["general", "financial-services", "healthcare", "public-sector"], + "sourceUrl": "https://www.regjeringen.no/en/whats-new/gjor-norge-klar-for-trygg-og-innovativ-ki-bruk/id3093081/", + "lastVerified": "June 2026" + }, + { + "name": "National Strategy for Artificial Intelligence", + "type": "national-strategy", + "status": "policy-only", + "effectiveDate": "January 2020", + "effectiveDateISO": "2020-01-01", + "dateConfidence": "approximate", + "scope": "Non-binding national strategy setting Norway's principles and ambitions for responsible AI development and adoption.", + "obligations": [ + "Promote ethical, trustworthy and human-centred AI", + "Build AI competence, data infrastructure and research capacity", + "Encourage responsible public-sector AI adoption", + "Frame later regulatory and sandbox initiatives" + ], + "maxPenalty": "None (policy framework)", + "industryTags": ["general", "public-sector"], + "sourceUrl": "https://www.regjeringen.no/en/documents/nasjonal-strategi-for-kunstig-intelligens/id2685594/", + "lastVerified": "June 2026" + }, + { + "name": "Datatilsynet regulatory sandbox for AI", + "type": "voluntary-guideline", + "status": "voluntary", + "effectiveDate": "2020 (made permanent 2023)", + "effectiveDateISO": "2020-01-01", + "dateConfidence": "approximate", + "scope": "Voluntary supervised environment run by the data protection authority for developing responsible, privacy-compliant AI.", + "obligations": [ + "Participants work with the regulator to test AI against data-protection requirements", + "Focus on transparency, fairness and lawful processing in AI systems", + "Published exit reports share lessons publicly", + "Voluntary participation; offers regulatory guidance rather than approval" + ], + "maxPenalty": "No sandbox-specific penalties; GDPR fines up to €20 million or 4% of turnover apply generally", + "industryTags": ["general", "public-sector", "healthcare"], + "sourceUrl": "https://www.datatilsynet.no/en/regulations-and-tools/sandbox-for-artificial-intelligence/", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "January 2020", + "description": "Norway publishes its National Strategy for Artificial Intelligence." + }, + { + "date": "December 2020", + "description": "Datatilsynet opens its regulatory sandbox for responsible AI." + }, + { + "date": "2023", + "description": "The AI sandbox becomes a permanent Datatilsynet offering." + }, + { + "date": "March 2025", + "description": "Government sets national AI Act implementation governance and designates Nkom as coordinating authority." + }, + { + "date": "Late summer 2026 (targeted)", + "description": "National AI act expected to take effect, aligning Norway with the EU AI Act." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "mexico", + "name": "Mexico", + "region": "north-america", + "regulationCount": 3, + "hash": "sha256-f2a9e007a64c83876f45ec095233956e8030401d3621947b907439da5cb47982", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-f2a9e007a64c83876f45ec095233956e8030401d3621947b907439da5cb47982", + "regulationCount": 3 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/mexico", + "flag": "🇲🇽", + "oneLiner": "Mexico has no binding federal AI law yet, governing AI through dozens of pending bills and a freshly overhauled data-protection regime.", + "executiveSummary": "As of mid-2026 Mexico has no binding, comprehensive federal AI law in force, though dozens of AI-related initiatives are pending across the Senado and Cámara de Diputados. The most consequential recent change is a sweeping data-protection reform: in March 2025 Mexico published new federal and general data-protection laws after a constitutional reform that abolished the autonomous regulator INAI and moved data-protection oversight into the executive-branch Secretaría Anticorrupción y Buen Gobierno. Several AI bills are circulating, including a national AI law introduced in early 2026, alongside sectoral measures amending labour and copyright law to address AI use of performers' image and voice. The overall posture is fragmented and sectoral, with no single AI law or governing AI authority yet operational, so binding obligations today flow from data-protection law rather than AI-specific rules.", + "practicalTakeaway": "If your company builds or deploys AI systems and operates in Mexico, comply with the 2025 data-protection laws now (noting oversight moved to the Secretaría Anticorrupción y Buen Gobierno after INAI's abolition) and monitor the many pending AI bills, since no binding federal AI law yet exists but sectoral labour and copyright AI rules are moving.", + "regulations": [ + { + "name": "Data-protection reform laws (LFPDPPP and LGPDPPSO, 2025)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "March 2025", + "effectiveDateISO": "2025-03-01", + "dateConfidence": "approximate", + "scope": "Federal private-sector and general public-sector data-protection laws governing processing of personal data, including by automated and AI systems.", + "obligations": [ + "Obtain a lawful basis and provide privacy notices for personal-data processing", + "Honour data-subject (ARCO) rights over automated processing", + "Apply data-minimisation, security and accountability principles", + "Oversight now sits with the Secretaría Anticorrupción y Buen Gobierno after INAI's abolition" + ], + "maxPenalty": "Data-protection fines under the LFPDPPP, scaled by violation; figures set by the consolidated law text", + "industryTags": ["general", "financial-services", "healthcare"], + "sourceUrl": "https://www.diputados.gob.mx/LeyesBiblio/ref/lfpdppp/LFPDPPP_orig_20mar25.pdf", + "lastVerified": "June 2026" + }, + { + "name": "National AI law (proposed)", + "type": "draft-bill", + "status": "proposed", + "effectiveDate": "Date TBD", + "effectiveDateISO": null, + "dateConfidence": "unknown", + "scope": "Proposed national AI law introduced in the Senado in early 2026 and referred to committee; not enacted. One of dozens of competing AI initiatives before Congress.", + "obligations": [ + "Would establish national AI governance and oversight structures", + "Would set duties for developers and deployers of AI systems", + "Status: pending in committee, contents not finalised", + "Mexico has no single AI authority yet operational" + ], + "maxPenalty": "Not enacted; proposed penalties not in force", + "industryTags": ["general", "public-sector"], + "sourceUrl": "https://www.diputados.gob.mx/", + "lastVerified": "June 2026" + }, + { + "name": "AI amendments to labour and copyright law (sectoral)", + "type": "draft-bill", + "status": "proposed", + "effectiveDate": "Date TBD", + "effectiveDateISO": null, + "dateConfidence": "unknown", + "scope": "Reforms amending the Federal Labour Law and Federal Copyright Law to govern AI use of performers' image and voice.", + "obligations": [ + "Would require consent for AI replication of a performer's image or voice", + "Would extend labour and copyright protections to AI-generated likenesses", + "Advanced through the Cámara de Diputados in 2026; final enactment status unconfirmed", + "Sectoral in scope, not a general AI law" + ], + "maxPenalty": "To be set by the amended statutes if enacted", + "industryTags": ["general", "hr-employment"], + "sourceUrl": "https://www.diputados.gob.mx/", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "2024", + "description": "A non-binding national AI agenda for 2024–2030 is presented in the Senado." + }, + { + "date": "March 2025", + "description": "Constitutional reform abolishes INAI; new federal and general data-protection laws are published." + }, + { + "date": "Early 2026", + "description": "A national AI law is introduced in the Senado and referred to committee." + }, + { + "date": "2026", + "description": "AI amendments to labour and copyright law advance through the Cámara de Diputados." + }, + { + "date": "Pending", + "description": "No comprehensive federal AI law enacted as of mid-2026." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "ukraine", + "name": "Ukraine", + "region": "europe", + "regulationCount": 3, + "hash": "sha256-82d775811b5db68dc6c3e63e90c12d5cb014d5469584183647847b9a277e6703", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-82d775811b5db68dc6c3e63e90c12d5cb014d5469584183647847b9a277e6703", + "regulationCount": 3 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/ukraine", + "flag": "🇺🇦", + "oneLiner": "Ukraine uses a voluntary, bottom-up AI roadmap, preparing industry before a future EU AI Act-aligned law ahead of EU accession.", + "executiveSummary": "Ukraine has no binding AI law in force; its approach is a deliberately pro-innovation, bottom-up roadmap led by the Ministry of Digital Transformation. In 2024 the ministry presented a White Paper on AI Regulation setting out a two-stage plan: first equip industry with non-binding tools (a White Paper, voluntary codes of conduct, a regulatory sandbox and capacity building), then later adopt a law harmonised with the EU AI Act. This sequencing is tied to Ukraine's EU candidate status and the goal of aligning its legislation with the EU acquis ahead of accession. A voluntary Code of Conduct on ethical AI use was endorsed by leading Ukrainian tech companies in late 2024. In May 2025 Ukraine signed the Council of Europe Framework Convention on AI. All current commitments are voluntary, with no statutory penalties yet in place.", + "practicalTakeaway": "If your company builds or deploys AI systems and operates in Ukraine, adopt the voluntary code and design to EU AI Act standards now, because Ukraine's roadmap is explicitly steering toward an EU-aligned law as part of its EU accession path, even though no binding AI law or penalties yet exist.", + "regulations": [ + { + "name": "White Paper on AI Regulation (roadmap)", + "type": "national-strategy", + "status": "policy-only", + "effectiveDate": "2024", + "effectiveDateISO": "2024-01-01", + "dateConfidence": "approximate", + "scope": "Non-binding policy roadmap from the Ministry of Digital Transformation outlining a two-stage path to AI regulation aligned with the EU AI Act.", + "obligations": [ + "Stage one: prepare industry with voluntary tools, sandbox and guidance", + "Stage two: draft an AI law harmonised with the EU AI Act", + "Roadmap milestones run through 2027 ahead of EU accession", + "No binding obligations or penalties imposed at this stage" + ], + "maxPenalty": "None (voluntary roadmap)", + "industryTags": ["general", "public-sector"], + "sourceUrl": "https://oecd.ai/en/dashboards/policy-initiatives/roadmap-for-ai-regulation-in-ukraine-a-bottom-up-approach", + "lastVerified": "June 2026" + }, + { + "name": "Voluntary Code of Conduct on responsible AI", + "type": "voluntary-guideline", + "status": "voluntary", + "effectiveDate": "December 2024", + "effectiveDateISO": "2024-12-01", + "dateConfidence": "approximate", + "scope": "Non-binding self-regulation instrument for ethical and responsible AI, endorsed by leading Ukrainian technology companies.", + "obligations": [ + "Signatories commit to ethical, human-centred AI development", + "Promote transparency and accountability in AI systems", + "Voluntary self-commitment ahead of formal legislation", + "Part of stage-one preparation under the White Paper roadmap" + ], + "maxPenalty": "None (voluntary commitment)", + "industryTags": ["general"], + "sourceUrl": "https://oecd.ai/en/dashboards/policy-initiatives/roadmap-for-ai-regulation-in-ukraine-a-bottom-up-approach", + "lastVerified": "June 2026" + }, + { + "name": "Council of Europe Framework Convention on AI (signed)", + "type": "binding-law", + "status": "passed-not-active", + "effectiveDate": "Signed May 2025", + "effectiveDateISO": "2025-05-01", + "dateConfidence": "approximate", + "scope": "International treaty on AI, human rights, democracy and the rule of law; signed by Ukraine but requiring ratification and domestic implementation.", + "obligations": [ + "Commit to AI safeguards for human rights and democratic values", + "Plan to apply the Council of Europe risk-assessment methodology", + "Signature does not equal ratification; domestic implementation still required", + "Reinforces alignment toward EU and Council of Europe standards" + ], + "maxPenalty": "Set by future domestic implementing law", + "industryTags": ["general", "public-sector"], + "sourceUrl": "https://www.coe.int/en/web/artificial-intelligence/the-framework-convention-on-artificial-intelligence", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "2023", + "description": "Ministry of Digital Transformation develops a bottom-up roadmap for AI regulation." + }, + { + "date": "2024", + "description": "Ukraine presents its White Paper on AI Regulation outlining a two-stage approach." + }, + { + "date": "December 2024", + "description": "Leading Ukrainian tech companies endorse a voluntary Code of Conduct on responsible AI." + }, + { + "date": "May 2025", + "description": "Ukraine signs the Council of Europe Framework Convention on AI." + }, + { + "date": "By 2027", + "description": "Roadmap targets drafting of an EU AI Act-aligned law ahead of EU accession." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "poland", + "name": "Poland", + "region": "europe", + "regulationCount": 3, + "hash": "sha256-558c18f80ac538ab47d79a9464461099483faa753934a865b9ea3cb4fb99a7b0", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-558c18f80ac538ab47d79a9464461099483faa753934a865b9ea3cb4fb99a7b0", + "regulationCount": 3 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/poland", + "flag": "🇵🇱", + "oneLiner": "As an EU member, Poland applies the EU AI Act directly while advancing its own Act on AI Systems and a dedicated AI regulator.", + "executiveSummary": "Poland is governed first and foremost by the EU AI Act (Regulation 2024/1689), which is directly applicable without national transposition and phases in between 2025 and 2027. To operationalise it, Poland is finalising a domestic Act on AI Systems that would designate a brand-new Commission for the Development and Security of Artificial Intelligence (KRBSI) as the national competent and market-surveillance authority and single point of contact. The Council of Ministers adopted the draft proposal in March 2026, but it still must pass through Parliament, so the national act is not yet in force. Personal data aspects of AI remain supervised by the data-protection authority UODO under the GDPR. Companies should treat the EU AI Act obligations as live today and prepare for the additional Polish supervisory layer once enacted.", + "practicalTakeaway": "If your company builds or deploys AI systems and operates in Poland, comply with the directly-applicable EU AI Act now — classify systems by risk, meet high-risk and transparency duties, and keep GDPR/UODO obligations covered — while tracking the pending Act on AI Systems so you are ready to engage the KRBSI regulator once it is enacted.", + "regulations": [ + { + "name": "EU AI Act (directly applicable in Poland)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "August 2024", + "effectiveDateISO": "2024-08-01", + "dateConfidence": "approximate", + "scope": "Providers, deployers, importers and distributors placing AI systems or general-purpose AI models on the EU market or whose output is used in the EU, including those based in Poland.", + "obligations": [ + "Classify each AI system by risk tier (prohibited, high-risk, limited, minimal)", + "Withdraw prohibited practices such as social scoring and untargeted facial-recognition scraping", + "Meet high-risk requirements: risk management, data governance, technical documentation, logging, human oversight and conformity assessment", + "Apply transparency duties for chatbots, emotion recognition and AI-generated or deepfake content", + "Comply with general-purpose AI model obligations including technical documentation and copyright policy" + ], + "maxPenalty": "Up to €35 million or 7% of total worldwide annual turnover, whichever is higher (EU AI Act penalties)", + "industryTags": [ + "general", + "financial-services", + "healthcare", + "hr-employment", + "public-sector" + ], + "sourceUrl": "https://eur-lex.europa.eu/eli/reg/2024/1689/oj", + "lastVerified": "June 2026" + }, + { + "name": "Act on Artificial Intelligence Systems (proposed)", + "type": "draft-bill", + "status": "proposed", + "effectiveDate": "Date TBD (Council of Ministers adopted the proposal March 2026)", + "effectiveDateISO": "2026-03-01", + "dateConfidence": "approximate", + "scope": "National implementation framework for the EU AI Act in Poland, establishing supervisory institutions, procedures and sanctions for entities operating in the Polish market.", + "obligations": [ + "Recognise the Commission for the Development and Security of AI (KRBSI) as the competent and market-surveillance authority", + "Use KRBSI as the national single point of contact for AI Act matters", + "Cooperate with KRBSI administrative proceedings, inspections and information requests", + "Respond to citizen complaints about AI-driven decisions channelled through the Commission", + "Prepare for corrective orders including suspension, recall or withdrawal of non-compliant systems" + ], + "maxPenalty": "References EU AI Act fines (up to €35M / 7% turnover); up to PLN 500,000 proposed for obstructing inspections (subject to final parliamentary text)", + "industryTags": ["general", "financial-services", "public-sector"], + "sourceUrl": "https://www.gov.pl/web/cyfryzacja", + "lastVerified": "June 2026" + }, + { + "name": "GDPR, enforced by UODO", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "May 2018", + "effectiveDateISO": "2018-05-01", + "dateConfidence": "approximate", + "scope": "Any processing of personal data by AI systems in Poland, supervised by the President of the Personal Data Protection Office (UODO).", + "obligations": [ + "Establish a lawful basis for personal data used to train or run AI systems", + "Conduct a data protection impact assessment for high-risk processing", + "Honour data-subject rights, including safeguards against solely automated decisions with legal effect", + "Apply data minimisation, purpose limitation and storage limitation to AI datasets", + "Report qualifying personal-data breaches to UODO within 72 hours" + ], + "maxPenalty": "Up to €20 million or 4% of total worldwide annual turnover, whichever is higher", + "industryTags": [ + "general", + "financial-services", + "healthcare", + "hr-employment", + "public-sector" + ], + "sourceUrl": "https://uodo.gov.pl", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "August 2024", + "description": "EU AI Act enters into force, directly applicable in Poland." + }, + { + "date": "October 2024", + "description": "Poland publishes the first draft Act on AI Systems proposing the KRBSI regulator." + }, + { + "date": "February 2025", + "description": "EU AI Act prohibitions on unacceptable-risk practices become applicable." + }, + { + "date": "June 2025", + "description": "Revised draft of the Act on AI Systems released, refining KRBSI's powers." + }, + { + "date": "March 2026", + "description": "Council of Ministers adopts the Act on AI Systems proposal for submission to Parliament." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "kenya", + "name": "Kenya", + "region": "africa", + "regulationCount": 2, + "hash": "sha256-766ea35411615695ce418baec53ca4dee7fcaefb0ade976864f6d7089ee090ee", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-766ea35411615695ce418baec53ca4dee7fcaefb0ade976864f6d7089ee090ee", + "regulationCount": 2 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/kenya", + "flag": "🇰🇪", + "oneLiner": "Kenya steers AI through a national strategy while its binding Data Protection Act governs automated decisions and profiling.", + "executiveSummary": "Kenya has no dedicated AI statute yet; its direction is set by the Kenya Artificial Intelligence Strategy 2025–2030, launched by the Ministry of Information, Communications and the Digital Economy in March 2025. The strategy is a policy roadmap built around AI infrastructure, data, talent, governance, investment and ethics, signalling intent rather than imposing enforceable obligations. The binding rules that actually constrain AI today come from the Data Protection Act, 2019, enforced by the Office of the Data Protection Commissioner (ODPC), which grants individuals the right not to be subject to solely automated decisions and requires impact assessments for high-risk processing. Organisations deploying AI in Kenya should anchor compliance in the Data Protection Act while aligning to the strategy's responsible-AI principles.", + "practicalTakeaway": "If your company deploys AI involving personal data and operates in Kenya, comply with the Data Protection Act 2019 — register with the ODPC where required, run impact assessments, and provide human review for automated decisions — while aligning your AI governance to the responsible-AI principles of the 2025–2030 national strategy.", + "regulations": [ + { + "name": "Kenya Artificial Intelligence Strategy 2025–2030", + "type": "national-strategy", + "status": "policy-only", + "effectiveDate": "March 2025", + "effectiveDateISO": "2025-03-01", + "dateConfidence": "approximate", + "scope": "National policy roadmap guiding government, industry and academia on AI research, infrastructure, governance and ethical deployment across sectors.", + "obligations": [ + "Align AI initiatives with the strategy's responsible, secure and inclusive deployment principles", + "Support development of AI digital infrastructure and trusted data ecosystems", + "Observe emerging governance and ethics guardrails set out across the six strategic pillars", + "Reference existing frameworks (Constitution 2010, Data Protection Act 2019, National ICT Policy) when deploying AI" + ], + "maxPenalty": "None — non-binding national strategy with no statutory penalties", + "industryTags": ["general", "public-sector"], + "sourceUrl": "https://ict.go.ke/sites/default/files/2025-03/Kenya%20AI%20Strategy%202025%20-%202030.pdf", + "lastVerified": "June 2026" + }, + { + "name": "Data Protection Act, 2019 (Act No. 24 of 2019)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "November 2019", + "effectiveDateISO": "2019-11-01", + "dateConfidence": "approximate", + "scope": "Any data controller or processor handling personal data of individuals in Kenya, including AI-driven profiling and automated decision-making.", + "obligations": [ + "Provide a lawful basis and register with the ODPC where required before processing personal data", + "Honour the right not to be subject to a decision based solely on automated processing with legal or significant effects", + "Notify data subjects in writing when a decision is based solely on automated processing", + "Carry out a data protection impact assessment before high-risk processing, including profiling", + "Uphold data-subject rights of access, correction, deletion and objection", + "Report personal-data breaches to the ODPC and affected individuals" + ], + "maxPenalty": "Up to KES 5 million, or in the case of an undertaking up to 1% of annual turnover, whichever is lower", + "industryTags": [ + "general", + "financial-services", + "healthcare", + "hr-employment", + "public-sector" + ], + "sourceUrl": "https://www.odpc.go.ke/", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "November 2019", + "description": "Data Protection Act, 2019 enacted, creating the ODPC." + }, + { + "date": "2021", + "description": "Data Protection (General) Regulations operationalise rights on automated decisions and profiling." + }, + { + "date": "March 2025", + "description": "Ministry of ICT launches the Kenya Artificial Intelligence Strategy 2025–2030." + }, + { + "date": "2025–2030", + "description": "Strategy implementation period, with AI governance and possible AI/robotics policy under development." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "qatar", + "name": "Qatar", + "region": "asia", + "regulationCount": 3, + "hash": "sha256-1ee1c9bbc5a8d6b08154d08922b603e5dd8be513d4873a02acfb1ebe23504de0", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-1ee1c9bbc5a8d6b08154d08922b603e5dd8be513d4873a02acfb1ebe23504de0", + "regulationCount": 3 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/qatar", + "flag": "🇶🇦", + "oneLiner": "Qatar guides AI through a national strategy and ethics guidelines, with binding controls coming from its personal-data privacy law.", + "executiveSummary": "Qatar has no standalone binding AI law; it relies on a combination of strategy and ethics guidance overlaid on its data-protection statute. The Qatar National AI Strategy, first adopted in 2019, sets the 'AI+X' vision and the pillars of national AI development. In 2025 the Ministry of Communications and Information Technology (MCIT) issued non-binding Principles and Guidelines for the Ethical Use of Artificial Intelligence, covering transparency, human oversight, safety, privacy and alignment with existing Qatari law. The binding obligations that affect AI today flow from the Personal Data Privacy Protection Law (Law No. 13 of 2016), Qatar's foundational data-privacy statute.", + "practicalTakeaway": "If your company develops or deploys AI and operates in Qatar, comply with the Personal Data Privacy Protection Law (Law No. 13 of 2016) for any personal-data processing and adopt the MCIT ethical-AI principles — transparency, human oversight and safety — as your governance baseline, since no binding standalone AI law yet exists.", + "regulations": [ + { + "name": "Personal Data Privacy Protection Law (Law No. 13 of 2016)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "2017", + "effectiveDateISO": "2017-01-01", + "dateConfidence": "approximate", + "scope": "Controllers and processors handling personal data of individuals in Qatar, including AI systems that process or profile personal data.", + "obligations": [ + "Process personal data lawfully, fairly and transparently with a valid basis", + "Obtain consent and apply heightened protection for data of special nature", + "Honour data-subject rights of access, rectification, erasure and objection", + "Implement appropriate technical and organisational security safeguards", + "Notify the competent department of breaches affecting personal data", + "Apply additional safeguards when processing involves automated or profiling activities" + ], + "maxPenalty": "Administrative fines up to QAR 5,000,000 for violations of the law", + "industryTags": [ + "general", + "financial-services", + "healthcare", + "hr-employment", + "public-sector" + ], + "sourceUrl": "https://www.mcit.gov.qa/en/", + "lastVerified": "June 2026" + }, + { + "name": "Principles and Guidelines for the Ethical Use of AI (MCIT)", + "type": "voluntary-guideline", + "status": "voluntary", + "effectiveDate": "2025", + "effectiveDateISO": "2025-01-01", + "dateConfidence": "approximate", + "scope": "Voluntary ethical framework for public and private entities developing or deploying AI in Qatar.", + "obligations": [ + "Ensure transparency and explainability of AI systems", + "Maintain meaningful human oversight over AI-driven decisions", + "Protect privacy and personal data in line with Qatari law", + "Design for safety, security and avoidance of harm", + "Align AI use with fundamental rights and non-discrimination" + ], + "maxPenalty": "None — voluntary guidelines with no statutory penalties", + "industryTags": ["general", "public-sector", "healthcare", "financial-services"], + "sourceUrl": "https://www.mcit.gov.qa/wp-content/uploads/sites/4/2025/04/AI-Guidelines-_-En.pdf", + "lastVerified": "June 2026" + }, + { + "name": "Qatar National Artificial Intelligence Strategy", + "type": "national-strategy", + "status": "policy-only", + "effectiveDate": "October 2019", + "effectiveDateISO": "2019-10-01", + "dateConfidence": "approximate", + "scope": "National roadmap accelerating ethical AI adoption across government, industry and academia under the 'AI+X' vision.", + "obligations": [ + "Align AI initiatives with the strategy's national pillars and ethical-adoption goals", + "Support development of AI talent, data and infrastructure capacity", + "Pursue AI deployment that is ethical and aligned with national priorities", + "Coordinate with sector regulators as the framework matures" + ], + "maxPenalty": "None — non-binding national strategy with no statutory penalties", + "industryTags": ["general", "public-sector"], + "sourceUrl": "https://www.mcit.gov.qa/en/", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "2016", + "description": "Personal Data Privacy Protection Law (Law No. 13 of 2016) issued." + }, + { + "date": "October 2019", + "description": "Qatar adopts its National Artificial Intelligence Strategy." + }, + { + "date": "2025", + "description": "MCIT releases Principles and Guidelines for the Ethical Use of AI." + }, + { + "date": "2025 onward", + "description": "Qatar continues mapping AI-specific governance on top of its data-privacy framework." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "kazakhstan", + "name": "Kazakhstan", + "region": "asia", + "regulationCount": 3, + "hash": "sha256-00a9f86a7e149a21308461e74d8af493512da9a3d61568ee4c32b8e4a0dcc790", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-00a9f86a7e149a21308461e74d8af493512da9a3d61568ee4c32b8e4a0dcc790", + "regulationCount": 3 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/kazakhstan", + "flag": "🇰🇿", + "oneLiner": "Kazakhstan enacted a dedicated Law on Artificial Intelligence, banning manipulative and social-scoring systems and creating a national AI regulator.", + "executiveSummary": "Kazakhstan has a binding, enacted standalone AI statute: Law No. 230-VIII 'On Artificial Intelligence', signed by the President on 17 November 2025 and in force since 18 January 2026, making it one of the first jurisdictions after the EU to pass dedicated AI legislation. The law sets principles of security, transparency and accountability, regulates both traditional and generative AI, and directly bans manipulative systems, exploitation of vulnerabilities, social scoring, non-consensual emotion recognition and discriminatory biometric classification. It is supervised by the Ministry of Artificial Intelligence and Digital Development, and owners and holders of AI systems must manage risks and maintain documentation proportionate to each system's impact. The law builds on the Concept for AI Development 2024–2029 and operates alongside Kazakhstan's personal-data protection regime.", + "practicalTakeaway": "If your company builds or operates AI systems and operates in Kazakhstan, comply now with the in-force Law No. 230-VIII — withdraw any banned manipulative, social-scoring or discriminatory-biometric uses, label AI-generated outputs, and maintain risk-management documentation under the Ministry of Artificial Intelligence and Digital Development — alongside the personal-data law.", + "regulations": [ + { + "name": "Law No. 230-VIII 'On Artificial Intelligence'", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "January 2026 (signed November 2025)", + "effectiveDateISO": "2026-01-01", + "dateConfidence": "approximate", + "scope": "Owners and holders (operators) of AI systems operating in Kazakhstan, covering both traditional and generative AI across consumer protection, personal data and media labelling.", + "obligations": [ + "Do not create or operate AI using manipulative techniques affecting the subconscious or distorting behaviour", + "Do not deploy social scoring or exploit age/disability vulnerabilities to cause harm", + "Do not perform non-consensual emotion recognition or discriminatory biometric classification", + "Manage the risks of AI systems proportionate to their impact on safety, rights and public order", + "Maintain documentation for AI systems per the list approved by the authorised body", + "Inform users about synthetic (AI-generated) outputs that may mislead them" + ], + "maxPenalty": "Administrative fines from 15 to 200 MCI, with possible suspension or prohibition of the AI system's operation", + "industryTags": [ + "general", + "financial-services", + "healthcare", + "hr-employment", + "public-sector" + ], + "sourceUrl": "https://adilet.zan.kz/eng/docs/Z2500000230", + "lastVerified": "June 2026" + }, + { + "name": "Concept for the Development of Artificial Intelligence 2024–2029", + "type": "national-strategy", + "status": "policy-only", + "effectiveDate": "2024", + "effectiveDateISO": "2024-01-01", + "dateConfidence": "approximate", + "scope": "National policy concept setting Kazakhstan's strategic direction for AI infrastructure, adoption and governance through 2029.", + "obligations": [ + "Align AI initiatives with national development priorities and the concept's roadmap", + "Support build-out of AI computing infrastructure, data and talent", + "Advance responsible and human-centric AI adoption across sectors", + "Underpin the legislative and institutional framework later codified in the AI Law" + ], + "maxPenalty": "None — non-binding policy concept with no statutory penalties", + "industryTags": ["general", "public-sector"], + "sourceUrl": "https://www.gov.kz/?lang=en", + "lastVerified": "June 2026" + }, + { + "name": "Law on Personal Data and its Protection (No. 94-V)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "2013", + "effectiveDateISO": "2013-01-01", + "dateConfidence": "approximate", + "scope": "Collection and processing of personal data in Kazakhstan, including by AI systems that handle or profile personal data.", + "obligations": [ + "Obtain consent or another lawful basis before collecting and processing personal data", + "Apply security measures to protect personal data used by AI systems", + "Honour data-subject rights regarding their personal data", + "Observe localisation and cross-border transfer requirements for personal data" + ], + "maxPenalty": "Administrative fines under the Code of Administrative Offences, with possible suspension of processing", + "industryTags": ["general", "financial-services", "healthcare", "public-sector"], + "sourceUrl": "https://adilet.zan.kz/eng/docs/Z1300000094", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "2024", + "description": "Kazakhstan adopts the Concept for AI Development 2024–2029." + }, + { + "date": "March 2025", + "description": "Draft Law 'On Artificial Intelligence' presented to the Mazhilis." + }, + { + "date": "November 2025", + "description": "President signs Law No. 230-VIII 'On Artificial Intelligence'." + }, + { + "date": "January 2026", + "description": "The AI Law enters into force." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "pakistan", + "name": "Pakistan", + "region": "asia", + "regulationCount": 3, + "hash": "sha256-5314f2655a5da6d16b132d67db0a231077cb1e7e65140d256d1e69400de93a23", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-5314f2655a5da6d16b132d67db0a231077cb1e7e65140d256d1e69400de93a23", + "regulationCount": 3 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/pakistan", + "flag": "🇵🇰", + "oneLiner": "Pakistan sets AI direction through its approved National AI Policy 2025 while a binding personal-data protection law is still in draft.", + "executiveSummary": "Pakistan's AI governance is currently policy-led rather than statute-led. The federal cabinet approved the National AI Policy 2025 on 30 July 2025, issued by the Ministry of IT and Telecommunication (MoITT), built on a six-pillar framework spanning AI innovation, awareness, secure systems, sectoral transformation, infrastructure and international partnerships. Implementation is overseen by an AI Council chaired by the federal IT minister. The policy is a roadmap and does not itself impose enforceable AI-specific penalties; it directs alignment with Pakistan's evolving data-protection regime — the draft Personal Data Protection Bill and a proposed national data-protection commission — and with the Prevention of Electronic Crimes Act (PECA). Because the data-protection statute remains a bill, binding obligations on AI are still forming.", + "practicalTakeaway": "If your company deploys AI and operates in Pakistan, adopt the responsible-AI principles of the National AI Policy 2025, comply with PECA for any electronic-crime exposure, and prepare data-handling practices for the draft Personal Data Protection Bill so you are ready once it becomes binding law.", + "regulations": [ + { + "name": "National Artificial Intelligence Policy 2025", + "type": "national-strategy", + "status": "policy-only", + "effectiveDate": "July 2025", + "effectiveDateISO": "2025-07-01", + "dateConfidence": "approximate", + "scope": "National policy roadmap guiding AI adoption, infrastructure, skills and ethical governance across public and private sectors in Pakistan.", + "obligations": [ + "Align AI initiatives with the policy's six pillars and responsible-use principles", + "Pursue ethical, inclusive and secure AI adoption consistent with global standards", + "Engage with the AI Council overseeing implementation", + "Align AI data practices with Pakistan's data-protection and PECA frameworks as they evolve", + "Support national AI skills, infrastructure and innovation goals" + ], + "maxPenalty": "None — non-binding national policy with no statutory penalties", + "industryTags": ["general", "public-sector", "financial-services", "healthcare"], + "sourceUrl": "https://moitt.gov.pk/SiteImage/Misc/files/National%20AI%20Policy.pdf", + "lastVerified": "June 2026" + }, + { + "name": "Personal Data Protection Bill (draft)", + "type": "draft-bill", + "status": "proposed", + "effectiveDate": "Date TBD (draft, not yet enacted)", + "effectiveDateISO": null, + "dateConfidence": "unknown", + "scope": "Proposed framework to regulate processing of personal data in Pakistan, including a national data-protection commission, relevant to AI systems handling personal data.", + "obligations": [ + "Establish a lawful basis and consent requirements for personal-data processing (as proposed)", + "Honour data-subject rights including access, correction and erasure (as proposed)", + "Apply security safeguards and breach-handling duties (as proposed)", + "Observe data-localisation and cross-border transfer rules under discussion" + ], + "maxPenalty": "Not yet in force — penalties to be set on enactment (draft proposes administrative fines)", + "industryTags": ["general", "financial-services", "healthcare", "public-sector"], + "sourceUrl": "https://moitt.gov.pk/", + "lastVerified": "June 2026" + }, + { + "name": "Prevention of Electronic Crimes Act (PECA), 2016", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "2016 (amended 2025)", + "effectiveDateISO": "2016-01-01", + "dateConfidence": "approximate", + "scope": "Cyber offences in Pakistan, applicable to misuse of AI systems involving electronic crimes, fraud or harmful content.", + "obligations": [ + "Refrain from using AI systems to commit electronic fraud or unauthorised access", + "Avoid generating or distributing unlawful content prohibited under the Act", + "Cooperate with designated authorities investigating electronic offences", + "Apply safeguards where AI processes data that could enable cyber offences" + ], + "maxPenalty": "Imprisonment and/or fines as specified per offence under the Act (varies by offence)", + "industryTags": ["general", "public-sector", "financial-services"], + "sourceUrl": "https://moitt.gov.pk/", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "2016", + "description": "Prevention of Electronic Crimes Act (PECA) enacted, covering cyber offences." + }, + { + "date": "2023–2025", + "description": "Personal Data Protection Bill developed and revised; remains in draft." + }, + { + "date": "July 2025", + "description": "Federal cabinet approves the National AI Policy 2025, issued by MoITT." + }, + { + "date": "2025 onward", + "description": "AI Council stood up to oversee implementation of the National AI Policy." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "bahrain", + "name": "Bahrain", + "region": "asia", + "regulationCount": 2, + "hash": "sha256-217e9a662a9ebdd33f9bf2aa727c3c41f90a893dc00443e0b89b15e5c4c32568", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-217e9a662a9ebdd33f9bf2aa727c3c41f90a893dc00443e0b89b15e5c4c32568", + "regulationCount": 2 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/bahrain", + "flag": "🇧🇭", + "oneLiner": "Bahrain governs AI mainly through its data-protection law while rolling out a public-sector national AI policy.", + "executiveSummary": "Bahrain has no single binding AI statute in force, so AI is governed primarily through the Personal Data Protection Law (Law No. 30 of 2018), in force since August 2019 and supervised by the Personal Data Protection Authority. In May 2025 the Information & eGovernment Authority (iGA) launched a National Policy for the Use of Artificial Intelligence built on four pillars: legal compliance, responsible adoption, public education and international cooperation. A standalone draft AI law was approved by the Shura Council in 2024 but is not yet enacted. The framework aligns with Economic Vision 2030 and emphasises transparency, human oversight and privacy.", + "practicalTakeaway": "If your company builds or deploys AI that processes personal data in Bahrain, treat the PDPL as your binding compliance baseline today (lawful basis, data-subject rights, security, transfer rules) while aligning public-sector and high-risk deployments with the 2025 National AI Policy and watching the pending standalone AI law.", + "regulations": [ + { + "name": "Personal Data Protection Law (Law No. 30 of 2018)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "August 2019", + "effectiveDateISO": "2019-08-01", + "dateConfidence": "approximate", + "scope": "Processing of personal data by controllers and processors based in Bahrain or using means located in Bahrain, including AI systems that process personal data.", + "obligations": [ + "Establish a lawful basis (consent or other legal ground) before processing personal data", + "Honour data-subject rights of access, rectification, objection and to stop processing", + "Apply technical and organisational security safeguards to personal data", + "Register certain processing operations with the Personal Data Protection Authority", + "Restrict cross-border transfers to jurisdictions without adequate protection unless authorised", + "Appoint a data protection guardian or supervisor where required" + ], + "maxPenalty": "Imprisonment up to one year and/or fines up to BHD 20,000 for serious breaches under the PDPL", + "industryTags": ["general", "financial-services", "public-sector"], + "sourceUrl": "https://www.pdp.gov.bh/en/index.html", + "lastVerified": "June 2026" + }, + { + "name": "National Policy for the Use of Artificial Intelligence (iGA)", + "type": "national-strategy", + "status": "policy-only", + "effectiveDate": "May 2025", + "effectiveDateISO": "2025-05-01", + "dateConfidence": "approximate", + "scope": "Public-sector use of AI across government bodies; sets objectives, pillars and responsibilities for ethical AI adoption.", + "obligations": [ + "Ensure AI use complies with existing Bahraini laws including the PDPL", + "Adopt responsible AI practices with transparency and human oversight", + "Align AI initiatives with Economic Vision 2030 and the UN SDGs", + "Apply the GCC guiding manual on AI ethics referenced by the policy" + ], + "maxPenalty": "None — policy instrument with no standalone penalties", + "industryTags": ["public-sector", "general"], + "sourceUrl": "https://www.iga.gov.bh/", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "July 2018", + "description": "Personal Data Protection Law (Law No. 30 of 2018) enacted." + }, + { + "date": "August 2019", + "description": "PDPL enters into force." + }, + { + "date": "2024", + "description": "Shura Council approves a draft standalone AI regulation law (not yet enacted)." + }, + { + "date": "May 2025", + "description": "iGA launches the National Policy for the Use of Artificial Intelligence." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "sri-lanka", + "name": "Sri Lanka", + "region": "asia", + "regulationCount": 2, + "hash": "sha256-4b4a31a2d46fe1ecd452a4db4cd8173a0a24831884985157514ec3e41d89d3cf", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-4b4a31a2d46fe1ecd452a4db4cd8173a0a24831884985157514ec3e41d89d3cf", + "regulationCount": 2 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/sri-lanka", + "flag": "🇱🇰", + "oneLiner": "Sri Lanka anchors AI governance in South Asia's first data-protection act alongside a new national AI strategy.", + "executiveSummary": "Sri Lanka was the first South Asian country to enact standalone data-protection legislation: the Personal Data Protection Act No. 9 of 2022, phased into force with the Data Protection Authority established in August 2023 and full controller obligations enforceable from March 2025. The Act is the binding anchor for AI that processes personal data. In parallel, the Ministry of Digital Economy and the ICTA developed a National Strategy on Artificial Intelligence through a dedicated committee, opened for public consultation in 2024, and appointed an AI Advisory Committee to drive implementation alongside the Digital Economy Strategy 2030. There is no standalone binding AI law in force.", + "practicalTakeaway": "If your company processes personal data of people in Sri Lanka through AI, comply now with the Personal Data Protection Act No. 9 of 2022 (lawful basis, a Data Protection Management Programme, breach notification, transfer rules) and track the National AI Strategy and ICTA Advisory Committee for forthcoming sector guidance.", + "regulations": [ + { + "name": "Personal Data Protection Act, No. 9 of 2022", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "Phased; full controller obligations from March 2025", + "effectiveDateISO": "2025-03-01", + "dateConfidence": "approximate", + "scope": "Processing of personal data of data subjects in Sri Lanka by controllers and processors, including AI-driven processing, with extraterritorial reach to entities targeting Sri Lankan data subjects.", + "obligations": [ + "Process personal data only on a lawful basis under the Act", + "Respect data-subject rights of access, correction, erasure and withdrawal of consent", + "Implement a Data Protection Management Programme and appropriate security controls", + "Conduct data-protection impact assessments for high-risk processing", + "Notify the Data Protection Authority of personal data breaches", + "Comply with cross-border transfer conditions set by the Authority" + ], + "maxPenalty": "Administrative penalties up to LKR 10 million per instance imposed by the Data Protection Authority, with higher exposure for repeat non-compliance", + "industryTags": ["general", "financial-services", "healthcare"], + "sourceUrl": "https://www.dpa.gov.lk/", + "lastVerified": "June 2026" + }, + { + "name": "National Strategy on Artificial Intelligence", + "type": "national-strategy", + "status": "policy-only", + "effectiveDate": "2024 (consultation draft)", + "effectiveDateISO": "2024-01-01", + "dateConfidence": "approximate", + "scope": "Whole-of-economy strategy to position Sri Lanka as a regional AI hub through responsible AI development and adoption.", + "obligations": [ + "Promote responsible and ethical AI adoption across public and private sectors", + "Build national AI skills, infrastructure and data foundations", + "Implement governance via the ICTA AI Advisory Committee and sub-committees", + "Align AI initiatives with the Digital Economy Strategy 2030" + ], + "maxPenalty": "None — strategy instrument with no standalone penalties", + "industryTags": ["general", "public-sector"], + "sourceUrl": "https://mode.gov.lk/", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "March 2022", + "description": "Personal Data Protection Act No. 9 of 2022 enacted." + }, + { + "date": "August 2023", + "description": "Data Protection Authority of Sri Lanka established." + }, + { + "date": "2024", + "description": "National Strategy on AI opened for public consultation; ICTA AI Advisory Committee appointed." + }, + { + "date": "March 2025", + "description": "Full controller and processor obligations under the PDPA become enforceable." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "uruguay", + "name": "Uruguay", + "region": "south-america", + "regulationCount": 2, + "hash": "sha256-324db80457fb884d2f4df0a7eed24b368c65b5eb0c9df55b54518d0ba0c4cfd7", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-324db80457fb884d2f4df0a7eed24b368c65b5eb0c9df55b54518d0ba0c4cfd7", + "regulationCount": 2 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/uruguay", + "flag": "🇺🇾", + "oneLiner": "Uruguay pairs a mature data-protection regime with a national AI strategy and ethical guidance for the State.", + "executiveSummary": "Uruguay has one of Latin America's most mature data-protection regimes: Law No. 18.331 on the Protection of Personal Data (2008), enforced by the Personal Data Regulatory and Control Unit (URCDP) within AGESIC, which holds supervisory and sanctioning powers. This regime is the binding anchor for AI systems that process personal data. Law No. 20.212 (2023, art. 74) tasks AGESIC with designing the national data and AI strategy and sets core ethical principles. Building on that mandate, the National Artificial Intelligence Strategy 2024–2030 was approved in November 2024, developed with CAF and UNESCO support across governance, capability-building and monitoring pillars. There is no standalone binding horizontal AI law in force.", + "practicalTakeaway": "If your company deploys AI that processes personal data in Uruguay, comply with Law No. 18.331 under URCDP oversight (lawful basis, database registration, data-subject rights, transfer rules) and align public-sector or higher-risk AI with the ethical principles in the National AI Strategy 2024–2030.", + "regulations": [ + { + "name": "Law No. 18.331 on the Protection of Personal Data", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "2008", + "effectiveDateISO": "2008-01-01", + "dateConfidence": "approximate", + "scope": "Processing of personal data in any format by public and private entities, including AI systems that process personal data of individuals in Uruguay.", + "obligations": [ + "Process personal data lawfully under principles of legality, purpose limitation and data minimisation", + "Obtain a valid legal basis (including consent) before processing", + "Honour data-subject rights of access, rectification, deletion and objection", + "Register databases and processing activities with the URCDP", + "Apply appropriate security measures to personal data", + "Meet authorisation and adequacy conditions for international data transfers" + ], + "maxPenalty": "Administrative sanctions imposed by the URCDP ranging from warnings to fines and suspension of databases", + "industryTags": ["general", "financial-services", "public-sector"], + "sourceUrl": "https://www.gub.uy/unidad-reguladora-control-datos-personales", + "lastVerified": "June 2026" + }, + { + "name": "National Artificial Intelligence Strategy 2024–2030 (AGESIC)", + "type": "national-strategy", + "status": "policy-only", + "effectiveDate": "November 2024", + "effectiveDateISO": "2024-11-01", + "dateConfidence": "approximate", + "scope": "Strategic framework for AI in Uruguay, with emphasis on the State and public sector, organised around governance, sustainable-development capabilities and monitoring.", + "obligations": [ + "Apply ethical principles of equity, transparency and respect for human dignity to AI", + "Coordinate public-sector AI through the Strategic Committee for AI and Data", + "Strengthen national AI governance, capabilities and data foundations", + "Monitor and review AI deployments for legal and ethical compliance" + ], + "maxPenalty": "None — strategy instrument with no standalone penalties (binding force flows from Law 18.331 and Law 20.212)", + "industryTags": ["public-sector", "general"], + "sourceUrl": "https://www.gub.uy/agencia-gobierno-electronico-sociedad-informacion-conocimiento/", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "2008", + "description": "Law No. 18.331 on Personal Data Protection enacted; URCDP established as supervisory authority." + }, + { + "date": "2023", + "description": "Law No. 20.212 (art. 74) tasks AGESIC with the national data and AI strategy and sets ethical principles." + }, + { + "date": "November 2024", + "description": "National Artificial Intelligence Strategy 2024–2030 approved." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "morocco", + "name": "Morocco", + "region": "africa", + "regulationCount": 2, + "hash": "sha256-5d6b7ebbd98de4a81b83c729785309402ac17a4f7ae2670b3538f0755c769b7e", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-5d6b7ebbd98de4a81b83c729785309402ac17a4f7ae2670b3538f0755c769b7e", + "regulationCount": 2 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/morocco", + "flag": "🇲🇦", + "oneLiner": "Morocco governs AI through its CNDP-enforced data-protection law while rolling out the Maroc IA 2030 strategy.", + "executiveSummary": "Morocco's binding anchor for AI is Law No. 09-08 on the Protection of Individuals with Regard to the Processing of Personal Data (2009), enforced by the autonomous National Commission for the Control of Personal Data Protection (CNDP). The law was designed to align Morocco with European data-protection standards and applies to AI systems that process personal data. On the policy side, Morocco launched its national AI roadmap, branded 'Maroc IA 2030', under the Ministry of Digital Transition and Administrative Reform, translating the July 2025 National AI Conference into an operational framework within the Digital Morocco 2030 strategy. There is no standalone binding horizontal AI law in force yet.", + "practicalTakeaway": "If your company deploys AI that processes personal data in Morocco, comply with Law No. 09-08 under CNDP oversight (authorisation or declaration, data-subject rights, security and transfer rules) and align strategic deployments with the Maroc IA 2030 roadmap and the CNDP's emerging AI guidance.", + "regulations": [ + { + "name": "Law No. 09-08 on the Protection of Personal Data", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "2009", + "effectiveDateISO": "2009-01-01", + "dateConfidence": "approximate", + "scope": "Processing of personal data by public and private bodies in Morocco, including AI systems that process personal data.", + "obligations": [ + "Obtain CNDP authorisation or declaration for processing of personal data as required", + "Establish a lawful basis and inform data subjects about processing", + "Honour data-subject rights of access, rectification and objection", + "Apply confidentiality and security safeguards to personal data", + "Comply with conditions for cross-border transfers of personal data", + "Cooperate with CNDP oversight and complaint-handling" + ], + "maxPenalty": "Administrative fines plus criminal penalties of up to MAD 300,000 and imprisonment for serious violations under Law 09-08", + "industryTags": ["general", "financial-services", "public-sector"], + "sourceUrl": "https://www.cndp.ma/", + "lastVerified": "June 2026" + }, + { + "name": "Maroc IA 2030 National AI Roadmap", + "type": "national-strategy", + "status": "policy-only", + "effectiveDate": "January 2026", + "effectiveDateISO": "2026-01-01", + "dateConfidence": "approximate", + "scope": "National strategy to govern and scale AI within the Digital Morocco 2030 framework, emphasising public services, skills and digital sovereignty.", + "obligations": [ + "Modernise public services and improve digital interoperability through AI", + "Strengthen national AI skills and centres of excellence", + "Promote digital sovereignty and reduce dependence on external systems", + "Align AI initiatives with the Digital Morocco 2030 strategy" + ], + "maxPenalty": "None — strategy instrument with no standalone penalties (binding data rules flow from Law 09-08)", + "industryTags": ["public-sector", "general"], + "sourceUrl": "https://www.cndp.ma/", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "2009", + "description": "Law No. 09-08 on Personal Data Protection enacted; CNDP established." + }, + { + "date": "July 2025", + "description": "National Artificial Intelligence Conference held in Rabat." + }, + { + "date": "January 2026", + "description": "Ministry of Digital Transition launches the Maroc IA 2030 roadmap." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "jordan", + "name": "Jordan", + "region": "asia", + "regulationCount": 2, + "hash": "sha256-4c032bf6799d1beb78512f256e5fb36eaba24217d711dca943ba54bdc4e8c02b", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-4c032bf6799d1beb78512f256e5fb36eaba24217d711dca943ba54bdc4e8c02b", + "regulationCount": 2 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/jordan", + "flag": "🇯🇴", + "oneLiner": "Jordan combines a five-year national AI roadmap with a recently enacted personal data protection law.", + "executiveSummary": "Jordan governs AI through two complementary instruments. The binding anchor is the Personal Data Protection Law No. 24 of 2023, which became operative in March 2024 with a one-year grace period ending in March 2025; it regulates processing of ordinary and sensitive personal data, including AI-driven processing. On the strategy side, the Ministry of Digital Economy and Entrepreneurship (MoDEE) issued the Artificial Intelligence Strategy and Implementation Plan 2023–2027, a five-year roadmap of 68 projects to position Jordan as a regional AI leader across legislative, technological and entrepreneurial dimensions. Today's enforceable obligations sit in the data-protection law.", + "practicalTakeaway": "If your company deploys AI that processes personal data in Jordan, comply with the Personal Data Protection Law No. 24 of 2023 (lawful basis, consent for sensitive data, security, breach reporting, transfer rules) and align broader AI initiatives with MoDEE's AI Strategy 2023–2027.", + "regulations": [ + { + "name": "Personal Data Protection Law No. 24 of 2023", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "March 2024 (grace period ended March 2025)", + "effectiveDateISO": "2024-03-01", + "dateConfidence": "approximate", + "scope": "Processing of ordinary and sensitive personal data of individuals within Jordan by controllers and processors, including AI systems that process personal data.", + "obligations": [ + "Process personal data on a lawful basis with appropriate consent for sensitive data", + "Honour data-subject rights of access, correction, erasure and objection", + "Apply security safeguards proportionate to the sensitivity of the data", + "Comply with conditions and approvals for cross-border data transfers", + "Report personal data breaches as required", + "Comply with oversight by the competent data protection authority" + ], + "maxPenalty": "Graduated administrative fines under the PDPL, with higher exposure for sensitive-data and cross-border violations", + "industryTags": ["general", "financial-services", "public-sector"], + "sourceUrl": "https://www.modee.gov.jo/", + "lastVerified": "June 2026" + }, + { + "name": "Artificial Intelligence Strategy and Implementation Plan 2023–2027 (MoDEE)", + "type": "national-strategy", + "status": "policy-only", + "effectiveDate": "November 2022 (covers 2023–2027)", + "effectiveDateISO": "2022-11-01", + "dateConfidence": "approximate", + "scope": "National five-year roadmap of 68 projects to build Jordan's AI legislative, technological and entrepreneurial ecosystem across sectors.", + "obligations": [ + "Develop AI-enabling legislation, infrastructure and governance", + "Deliver 68 targeted AI projects across priority sectors", + "Build national AI skills and attract AI investment", + "Promote ethical and responsible AI adoption" + ], + "maxPenalty": "None — strategy instrument with no standalone penalties", + "industryTags": ["public-sector", "general"], + "sourceUrl": "https://www.modee.gov.jo/ebv4.0/root_storage/en/eb_list_page/40435648.pdf", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "November 2022", + "description": "MoDEE launches the AI Strategy and Implementation Roadmap (2023–2027)." + }, + { + "date": "2023", + "description": "Personal Data Protection Law No. 24 of 2023 enacted." + }, + { + "date": "March 2024", + "description": "PDPL becomes operative; one-year compliance grace period begins." + }, + { + "date": "March 2025", + "description": "PDPL compliance grace period ends; full obligations enforceable." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "oman", + "name": "Oman", + "region": "asia", + "regulationCount": 2, + "hash": "sha256-5d16a29456470033a7d9110f99aa6b79b0f27b84a7f60befb5880cf613a6cad8", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-5d16a29456470033a7d9110f99aa6b79b0f27b84a7f60befb5880cf613a6cad8", + "regulationCount": 2 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/oman", + "flag": "🇴🇲", + "oneLiner": "Oman anchors AI governance in its MTCIT-enforced data-protection law alongside a national AI program and policy.", + "executiveSummary": "Oman's binding anchor for AI is the Personal Data Protection Law issued by Royal Decree 6/2022, in force since February 2023 and supplemented by an Executive Regulation issued in February 2024; the Ministry of Transport, Communications and Information Technology (MTCIT) is the enforcing authority. The law sets consent-based controls for processing personal data, which extend to AI systems. On the policy side, Oman's Council of Ministers approved the National Program for Artificial Intelligence and Advanced Digital Technologies in September 2024 (running 2024–2026), and MTCIT has progressed a National AI Policy for the safe and ethical use of AI. These programs sit within Oman Vision 2040. There is no standalone binding horizontal AI law in force.", + "practicalTakeaway": "If your company deploys AI that processes personal data in Oman, comply with the Personal Data Protection Law (Royal Decree 6/2022) and its Executive Regulation under MTCIT oversight (consent, purpose limitation, security, sensitive-data permits, transfer rules) and align strategic AI work with the National AI Program and forthcoming National AI Policy.", + "regulations": [ + { + "name": "Personal Data Protection Law (Royal Decree 6/2022)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "February 2023 (Executive Regulation issued February 2024)", + "effectiveDateISO": "2023-02-01", + "dateConfidence": "approximate", + "scope": "Processing of personal data by controllers and processors in Oman, including AI systems that process personal data.", + "obligations": [ + "Obtain the data subject's consent before processing personal data", + "Process only for lawful, specified purposes with transparency to data subjects", + "Honour data-subject rights over their personal data", + "Apply security safeguards and controls approved by MTCIT", + "Obtain a permit or approval for processing sensitive personal data as required", + "Comply with conditions for cross-border transfers of personal data" + ], + "maxPenalty": "Fines under the PDPL up to OMR 500,000 for serious violations, with additional penalties for sensitive-data breaches", + "industryTags": ["general", "financial-services", "healthcare"], + "sourceUrl": "https://mtcit.gov.om/library-3/legislations-policies-8/laws-75/personal-data-protection-law-1034", + "lastVerified": "June 2026" + }, + { + "name": "National Program for Artificial Intelligence and Advanced Digital Technologies", + "type": "national-strategy", + "status": "policy-only", + "effectiveDate": "September 2024 (2024–2026)", + "effectiveDateISO": "2024-09-01", + "dateConfidence": "approximate", + "scope": "National program to adopt, localise and govern AI across economic and developmental sectors within Oman Vision 2040.", + "obligations": [ + "Promote adoption of AI across economic and developmental sectors", + "Localise AI capabilities through skills-building and public-private partnerships", + "Govern AI applications with a human-centred, safe and ethical approach", + "Deliver national initiatives such as an AI research centre and national data platform" + ], + "maxPenalty": "None — program instrument with no standalone penalties (binding data rules flow from the PDPL)", + "industryTags": ["public-sector", "general"], + "sourceUrl": "https://mtcit.gov.om/", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "February 2022", + "description": "Royal Decree 6/2022 promulgates the Personal Data Protection Law." + }, + { + "date": "February 2023", + "description": "PDPL enters into force, replacing the data-protection provisions of the Electronic Transactions Law." + }, + { + "date": "February 2024", + "description": "Executive Regulation of the PDPL issued." + }, + { + "date": "September 2024", + "description": "Council of Ministers approves the National Program for AI and Advanced Digital Technologies (2024–2026)." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "costa-rica", + "name": "Costa Rica", + "region": "north-america", + "regulationCount": 2, + "hash": "sha256-04c71ea9f8b249a8a24cbd8e9ae422e0f8fbdd6ee2a95332a050e58010f6d9ec", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-04c71ea9f8b249a8a24cbd8e9ae422e0f8fbdd6ee2a95332a050e58010f6d9ec", + "regulationCount": 2 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/costa-rica", + "flag": "🇨🇷", + "oneLiner": "Costa Rica governs AI through its data-protection law while a UNESCO-backed national AI strategy sets ethical policy direction.", + "executiveSummary": "Costa Rica has no dedicated, binding AI statute. The enforceable anchor is Law No. 8968 on the Protection of Persons Regarding the Processing of Their Personal Data, supervised by the Agency for the Protection of Inhabitants' Data (PRODHAB), which governs automated processing, profiling and consent. In 2024 the Ministry of Science, Innovation, Technology and Telecommunications (MICITT) launched the National AI Strategy (ENIA) 2024–2027, developed with UNESCO and aligned to UNESCO's Recommendation on the Ethics of AI, making Costa Rica the first Central American country with such a policy. The strategy is policy guidance, not law, and emphasises ethical, human-centric and responsible AI.", + "practicalTakeaway": "If your company builds or deploys AI that processes personal data of people in Costa Rica, comply with Law No. 8968 and PRODHAB requirements (consent, database registration, data-subject rights) now, and align governance with the ethical principles of the National AI Strategy 2024–2027 while watching for AI bills in the Legislative Assembly.", + "regulations": [ + { + "name": "Law No. 8968 on the Protection of Personal Data", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "September 2011", + "effectiveDateISO": "2011-09-01", + "dateConfidence": "approximate", + "scope": "Any public or private entity processing personal data of individuals in Costa Rica, including automated processing, profiling and AI-driven decisions.", + "obligations": [ + "Obtain informed, express consent before collecting or processing personal data", + "Register databases that store or process personal data with PRODHAB", + "Guarantee data-subject rights of access, rectification, deletion and objection", + "Apply security measures and notify of breaches affecting personal data", + "Limit processing to the declared purpose and retention period", + "Meet stricter conditions for sensitive data and cross-border transfers" + ], + "maxPenalty": "Administrative fines up to roughly 30 base salaries plus possible suspension of database operations for serious violations", + "industryTags": ["general", "financial-services", "healthcare", "public-sector"], + "sourceUrl": "https://www.prodhab.go.cr/", + "lastVerified": "June 2026" + }, + { + "name": "National Artificial Intelligence Strategy (ENIA) 2024–2027", + "type": "national-strategy", + "status": "policy-only", + "effectiveDate": "2024", + "effectiveDateISO": "2024-01-01", + "dateConfidence": "approximate", + "scope": "Whole-of-government policy framework guiding ethical and responsible AI development and adoption across public and private sectors.", + "obligations": [ + "Promote ethical, human-centric and transparent AI aligned with UNESCO's Recommendation on the Ethics of AI", + "Strengthen AI skills, talent and research capacity", + "Encourage interagency coordination between MICITT, PRODHAB and sectoral regulators", + "Foster responsible public-sector AI adoption and data governance" + ], + "maxPenalty": "None — non-binding policy guidance", + "industryTags": ["general", "public-sector"], + "sourceUrl": "https://www.micitt.go.cr/", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "2011", + "description": "Law No. 8968 on personal data protection enacted, creating PRODHAB." + }, + { + "date": "2013", + "description": "PRODHAB becomes operational as the data-protection supervisory authority." + }, + { + "date": "2024", + "description": "MICITT launches the National AI Strategy (ENIA) 2024–2027, developed with UNESCO." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "ecuador", + "name": "Ecuador", + "region": "south-america", + "regulationCount": 2, + "hash": "sha256-828e69d01e4006ee75687c810738d740ee1d8d33d3d46fdf6ed5a9072a4d16be", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-828e69d01e4006ee75687c810738d740ee1d8d33d3d46fdf6ed5a9072a4d16be", + "regulationCount": 2 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/ecuador", + "flag": "🇪🇨", + "oneLiner": "Ecuador regulates AI mainly through its GDPR-style data-protection law, with AI-specific bills still moving through the National Assembly.", + "executiveSummary": "Ecuador's binding anchor is the Organic Law on the Protection of Personal Data (LOPDP), in force since May 2021, with its sanctioning regime applicable from May 2023. The law is closely modelled on the EU GDPR and covers automated processing, profiling and automated decision-making, enforced by the Superintendency for the Protection of Personal Data. Penalties are turnover-based, reaching up to 1% of the previous year's turnover for serious violations. Ecuador has no enacted, dedicated AI statute, but AI-specific draft legislation has been introduced in the National Assembly and AI policy is emerging.", + "practicalTakeaway": "If your company processes personal data of people in Ecuador or runs AI-driven profiling there, achieve full LOPDP compliance now (lawful basis, data-subject rights, breach notification) given turnover-based fines up to 1%, and track the AI bills progressing through the National Assembly.", + "regulations": [ + { + "name": "Organic Law on the Protection of Personal Data (LOPDP)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "May 2021 (sanctions from May 2023)", + "effectiveDateISO": "2021-05-01", + "dateConfidence": "approximate", + "scope": "Controllers and processors handling personal data of individuals in Ecuador, including automated processing, profiling and automated decision-making by AI systems.", + "obligations": [ + "Establish a lawful basis (consent or other legal ground) for processing", + "Honour data-subject rights including access, rectification, deletion, portability and objection to automated decisions", + "Appoint a data protection officer where required and keep processing records", + "Notify the Superintendency and affected individuals of qualifying data breaches", + "Apply security, proportionality and purpose-limitation safeguards", + "Meet additional conditions for sensitive data and international transfers" + ], + "maxPenalty": "Serious violations: fine of 0.7% to 1% of the previous financial year's turnover; minor violations 0.1% to 0.7%", + "industryTags": ["general", "financial-services", "healthcare", "public-sector"], + "sourceUrl": "https://www.gob.ec/regulaciones/ley-organica-proteccion-datos-personales", + "lastVerified": "June 2026" + }, + { + "name": "Draft AI legislation before the National Assembly", + "type": "draft-bill", + "status": "proposed", + "effectiveDate": "Date TBD (not yet enacted)", + "effectiveDateISO": null, + "dateConfidence": "unknown", + "scope": "Proposed framework to regulate the development, deployment and ethical use of artificial intelligence in Ecuador.", + "obligations": [ + "Establish principles for ethical and responsible AI development and use", + "Define transparency and accountability duties for AI systems (as proposed)", + "Coordinate AI oversight with the existing data-protection regime", + "Promote AI literacy, innovation and rights protection" + ], + "maxPenalty": "Not yet defined; the bill is not enacted", + "industryTags": ["general", "public-sector"], + "sourceUrl": "https://www.asambleanacional.gob.ec/", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "May 2021", + "description": "LOPDP enters into force, Ecuador's first comprehensive data-protection law." + }, + { + "date": "May 2023", + "description": "LOPDP sanctioning regime becomes applicable, enabling turnover-based fines." + }, + { + "date": "November 2023", + "description": "Regulation to the LOPDP issued, developing the law's provisions." + }, + { + "date": "2024–2025", + "description": "Draft AI-regulation bills introduced and discussed in the National Assembly." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "tunisia", + "name": "Tunisia", + "region": "africa", + "regulationCount": 2, + "hash": "sha256-724393ffe5e5668c63bf13d0d6d0ecc6103baf2e24e46ae9b5d1795891153cfd", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-724393ffe5e5668c63bf13d0d6d0ecc6103baf2e24e46ae9b5d1795891153cfd", + "regulationCount": 2 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/tunisia", + "flag": "🇹🇳", + "oneLiner": "Tunisia governs AI through its long-standing data-protection law while developing successive national AI strategies.", + "executiveSummary": "Tunisia's binding anchor is Organic Law No. 2004-63 on the Protection of Personal Data, the first such law in the Maghreb, enforced by the National Authority for the Protection of Personal Data (INPDP). It regulates collection, use, storage and transfer of personal data, including automated processing relevant to AI. Tunisia has no dedicated binding AI statute; instead it has pursued a National AI Strategy and Roadmap process since 2021, with a successor strategy advancing and proposals for a National AI Council. A draft modernised data-protection law was also introduced in 2025.", + "practicalTakeaway": "If your company processes personal data of people in Tunisia or deploys AI there, comply with Organic Law 2004-63 and INPDP declaration/authorisation requirements now, and follow the evolving National AI Strategy and the 2025 draft data-protection reform for upcoming obligations.", + "regulations": [ + { + "name": "Organic Law No. 2004-63 on the Protection of Personal Data", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "July 2004", + "effectiveDateISO": "2004-07-01", + "dateConfidence": "approximate", + "scope": "Public and private bodies processing personal data of individuals in Tunisia, including automated processing used by AI systems.", + "obligations": [ + "Obtain prior consent for processing personal data unless a legal exemption applies", + "Declare or seek authorisation from the INPDP for processing operations", + "Respect data-subject rights of access, rectification and objection", + "Apply security and confidentiality safeguards to personal data", + "Restrict and authorise cross-border transfers of personal data", + "Apply heightened protection to sensitive data" + ], + "maxPenalty": "Criminal and administrative sanctions including fines and imprisonment for serious breaches under the Organic Law", + "industryTags": ["general", "financial-services", "healthcare", "public-sector"], + "sourceUrl": "https://www.inpdp.tn/", + "lastVerified": "June 2026" + }, + { + "name": "National AI Strategy and Roadmap", + "type": "national-strategy", + "status": "policy-only", + "effectiveDate": "2021 onwards", + "effectiveDateISO": "2021-01-01", + "dateConfidence": "approximate", + "scope": "National policy framework to develop AI skills, infrastructure, data governance and ethical AI across priority sectors.", + "obligations": [ + "Develop AI talent, research and high-performance computing and cloud infrastructure", + "Promote open data, pilot projects and sectoral AI adoption", + "Advance legal and governance review for AI, including ethics", + "Coordinate AI policy across ministries through a planned National AI Council" + ], + "maxPenalty": "None — non-binding policy guidance", + "industryTags": ["general", "public-sector", "healthcare"], + "sourceUrl": "https://www.inpdp.tn/", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "2004", + "description": "Organic Law No. 2004-63 on personal data protection enacted, first in the Maghreb." + }, + { + "date": "2008", + "description": "INPDP established as the supervisory authority for personal data." + }, + { + "date": "2021", + "description": "Tunisia launches its National AI Strategy and Roadmap process." + }, + { + "date": "2025", + "description": "Refreshed AI agenda and a draft modernised data-protection law advanced." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "kuwait", + "name": "Kuwait", + "region": "asia", + "regulationCount": 2, + "hash": "sha256-cabdaff17a913d03fe40c386b0cad7c2193202677ebc799f0cf1dc1e88e43128", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-cabdaff17a913d03fe40c386b0cad7c2193202677ebc799f0cf1dc1e88e43128", + "regulationCount": 2 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/kuwait", + "flag": "🇰🇼", + "oneLiner": "Kuwait regulates AI indirectly through CITRA's data-privacy regulation, with a national AI strategy and governance framework emerging.", + "executiveSummary": "Kuwait has no dedicated binding AI law. The enforceable anchor is the Data Privacy Protection Regulation issued by the Communication and Information Technology Regulatory Authority (CITRA) — originally Regulation No. 42 of 2021 and updated by Regulation No. 26 of 2024 — which sets data-protection obligations on telecom and IT service providers that collect, process or store personal data, including AI-driven processing. The Central Agency for Information Technology (CAIT) leads government digital transformation and AI adoption, and a forthcoming National AI Strategy is expected to add a policy framework, a high-level steering committee and AI safety provisions.", + "practicalTakeaway": "If your company processes personal data of people in Kuwait or provides AI-enabled telecom or IT services there, comply with CITRA's Data Privacy Protection Regulation (consent, security, transfer controls) now, and prepare for the emerging National AI Strategy and its governance and safety requirements.", + "regulations": [ + { + "name": "CITRA Data Privacy Protection Regulation (No. 42 of 2021, updated by No. 26 of 2024)", + "type": "sector-regulation", + "status": "in-force", + "effectiveDate": "2021 (updated 2024)", + "effectiveDateISO": "2021-01-01", + "dateConfidence": "approximate", + "scope": "Telecommunication and information-technology service providers and related sectors that collect, process or store personal data in Kuwait.", + "obligations": [ + "Obtain user consent and provide clear privacy notices before collecting personal data", + "Limit collection and processing to declared, legitimate purposes", + "Implement security controls to protect personal data during and after service provision", + "Restrict and document cross-border data transfers and data-localisation requirements", + "Honour user rights regarding their personal data", + "Maintain accountability and records for data-processing activities" + ], + "maxPenalty": "Enforcement and penalties administered by CITRA under its regulatory powers; specific fine amounts set by CITRA decisions", + "industryTags": ["general", "financial-services", "public-sector"], + "sourceUrl": "https://www.citra.gov.kw/", + "lastVerified": "June 2026" + }, + { + "name": "National AI Strategy (CAIT, emerging)", + "type": "national-strategy", + "status": "proposed", + "effectiveDate": "In development", + "effectiveDateISO": null, + "dateConfidence": "unknown", + "scope": "Proposed national framework to coordinate AI adoption, governance and safety across Kuwait's public and private sectors.", + "obligations": [ + "Establish a high-level steering committee spanning CAIT, CITRA, the National Cybersecurity Center and ministries", + "Define a shared-responsibility model between regulators and technology providers", + "Introduce AI safety frameworks, including safeguards for critical infrastructure", + "Align AI regulation, infrastructure and innovation across government" + ], + "maxPenalty": "Not yet defined; the strategy is in development", + "industryTags": ["general", "public-sector"], + "sourceUrl": "https://www.citra.gov.kw/", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "April 2021", + "description": "CITRA issues the Data Privacy Protection Regulation No. 42 of 2021." + }, + { + "date": "2024", + "description": "CITRA updates the regime with Data Privacy Protection Regulation No. 26 of 2024." + }, + { + "date": "2024–2026", + "description": "CAIT advances digital transformation and a draft National AI Strategy with safety provisions." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "ghana", + "name": "Ghana", + "region": "africa", + "regulationCount": 2, + "hash": "sha256-5c44ba67003a2027ef15a5253a3e9a8d7b42a9d0375732bfa6fac730b370d9aa", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-5c44ba67003a2027ef15a5253a3e9a8d7b42a9d0375732bfa6fac730b370d9aa", + "regulationCount": 2 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/ghana", + "flag": "🇬🇭", + "oneLiner": "Ghana governs AI through its Data Protection Act and a ten-year National AI Strategy that proposes a Responsible AI Authority.", + "executiveSummary": "Ghana's binding anchor is the Data Protection Act, 2012 (Act 843), enforced by the Data Protection Commission (DPC), which regulates the processing of personal data, including automated processing and profiling used by AI systems. Controllers must register with the DPC and uphold data-subject rights. The policy layer is the National Artificial Intelligence Strategy 2023–2033, led by the Ministry of Communications and Digitalisation, which aims to harness AI for inclusive growth and proposes a Responsible AI Authority — with the DPC initially incubating it.", + "practicalTakeaway": "If your company processes personal data of people in Ghana or deploys AI there, register with the Data Protection Commission and comply with Act 843 now, and follow the National AI Strategy 2023–2033 and the planned Responsible AI Authority for future AI-specific rules.", + "regulations": [ + { + "name": "Data Protection Act, 2012 (Act 843)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "2012", + "effectiveDateISO": "2012-01-01", + "dateConfidence": "approximate", + "scope": "Any person or organisation processing personal data in or from Ghana, including automated processing and profiling by AI systems.", + "obligations": [ + "Register as a data controller with the Data Protection Commission", + "Process personal data lawfully, fairly and for specified purposes", + "Obtain consent or another lawful basis before processing", + "Uphold data-subject rights of access, correction and objection", + "Apply security safeguards and accountability measures to personal data", + "Meet conditions for processing sensitive data and cross-border transfers" + ], + "maxPenalty": "Offences under Act 843 carry fines (penalty units) and, for serious breaches, imprisonment of up to several years", + "industryTags": ["general", "financial-services", "healthcare", "public-sector"], + "sourceUrl": "https://www.dataprotection.org.gh/", + "lastVerified": "June 2026" + }, + { + "name": "National Artificial Intelligence Strategy 2023–2033", + "type": "national-strategy", + "status": "policy-only", + "effectiveDate": "2023", + "effectiveDateISO": "2023-01-01", + "dateConfidence": "approximate", + "scope": "Ten-year national policy framework to harness AI for inclusive growth and improved quality of life across sectors.", + "obligations": [ + "Harmonise AI policy with the Data Protection Act, Cybersecurity Act and related laws", + "Establish a Responsible AI Authority, initially incubated within the Data Protection Commission", + "Pursue near-, medium- and long-term actions on data digitisation, infrastructure and capacity", + "Promote ethical, responsible and inclusive AI adoption in public services" + ], + "maxPenalty": "None — non-binding policy guidance", + "industryTags": ["general", "public-sector"], + "sourceUrl": "https://moc.gov.gh/", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "2012", + "description": "Data Protection Act (Act 843) enacted, establishing the Data Protection Commission." + }, + { + "date": "2014", + "description": "Data Protection Commission becomes operational and begins controller registration." + }, + { + "date": "2023", + "description": "Ghana publishes its National AI Strategy 2023–2033, led by the Ministry of Communications and Digitalisation." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + }, + { + "slug": "georgia", + "name": "Georgia", + "region": "europe", + "regulationCount": 1, + "hash": "sha256-d3a9e34167861bbdb118b09cbfde8090db0c05386b40ddfabbca5ac5629f8f55", + "history": { + "firstAssessed": "2026-06-25", + "lastChanged": "2026-06-25", + "lastChecked": "2026-06-25", + "assessmentCount": 1, + "hashHistory": [ + { + "date": "2026-06-25", + "hash": "sha256-d3a9e34167861bbdb118b09cbfde8090db0c05386b40ddfabbca5ac5629f8f55", + "regulationCount": 1 + } + ], + "lastChange": null + }, + "url": "/api/regulations/country/georgia", + "flag": "🇬🇪", + "oneLiner": "Georgia regulates AI through a new GDPR-aligned data-protection law enforced by the Personal Data Protection Service.", + "executiveSummary": "Georgia's binding anchor is the new Law on Personal Data Protection (No. 3144), adopted in June 2023 and in force from March 2024, with some provisions phased in through 2024–2025. It closely mirrors the EU GDPR and is enforced by the independent Personal Data Protection Service (PDPS), covering automated processing, profiling and automated decision-making relevant to AI. Georgia has no dedicated binding AI statute; AI governance is emerging within broader digital-development efforts and EU-alignment commitments.", + "practicalTakeaway": "If your company processes personal data of people in Georgia or runs AI-driven profiling there, comply with the 2024 Law on Personal Data Protection and PDPS requirements now, and monitor Georgia's EU-aligned AI governance as it develops.", + "regulations": [ + { + "name": "Law of Georgia on Personal Data Protection (No. 3144)", + "type": "binding-law", + "status": "in-force", + "effectiveDate": "March 2024 (phased provisions through 2025)", + "effectiveDateISO": "2024-03-01", + "dateConfidence": "approximate", + "scope": "Controllers and processors handling personal data of individuals in Georgia, including automated processing, profiling and automated decision-making by AI systems.", + "obligations": [ + "Establish a lawful basis (consent or other ground) for processing personal data", + "Honour data-subject rights of access, rectification, deletion and objection", + "Appoint a personal data protection officer where required", + "Notify the PDPS of qualifying personal-data breaches", + "Apply security measures and rules for audio/video recording and direct marketing", + "Meet conditions for processing special-category data and international transfers" + ], + "maxPenalty": "Administrative fines per violation up to GEL 10,000 or GEL 20,000 depending on the sanctioned entity's turnover", + "industryTags": ["general", "financial-services", "healthcare", "public-sector"], + "sourceUrl": "https://matsne.gov.ge/en/document/view/5827307", + "lastVerified": "June 2026" + } + ], + "timeline": [ + { + "date": "June 2023", + "description": "Parliament adopts the new Law on Personal Data Protection (No. 3144)." + }, + { + "date": "March 2024", + "description": "New data-protection law enters into force; PDPS empowered as supervisory authority." + }, + { + "date": "2024–2025", + "description": "Phased provisions take effect; AI governance discussed within EU-alignment and digital agenda." + } + ], + "meta": { + "name": "Global AI Regulations", + "scopeStatement": "Research-backed summaries of public AI regulations and governance frameworks worldwide, reflecting VerifyWise's good-faith reading of official sources as of the last update. Informational only.", + "disclaimer": "This data is provided for general informational purposes only and is not legal advice. It reflects VerifyWise's good-faith summaries of publicly available regulatory sources and may contain errors, omissions, or out-of-date information. Verify against the official primary sources before relying on it. To request a correction, contact hello@verifywise.ai.", + "sourceUrl": "https://verifywise.ai/global-ai-regulations", + "lastDataUpdate": "June 2026" + } + } + ] +} diff --git a/Servers/domain.layer/interfaces/i.notification.ts b/Servers/domain.layer/interfaces/i.notification.ts index 59ce51fbfa..0ac87ff9f0 100644 --- a/Servers/domain.layer/interfaces/i.notification.ts +++ b/Servers/domain.layer/interfaces/i.notification.ts @@ -58,6 +58,9 @@ export enum NotificationType { ASSIGNMENT_MEMBER = "assignment_member", ASSIGNMENT_ASSIGNEE = "assignment_assignee", ASSIGNMENT_ACTION_OWNER = "assignment_action_owner", + + // Regulations Tracker notifications + REGULATIONS_TRACKER = "regulations_tracker", } /** @@ -80,6 +83,7 @@ export enum NotificationEntityType { SHADOW_AI_TOOL = "shadow_ai_tool", AI_GATEWAY = "ai_gateway", AI_ACTION = "ai_action", + REGULATION_COUNTRY = "regulation_country", } /** diff --git a/Servers/domain.layer/interfaces/i.regulationsTracker.ts b/Servers/domain.layer/interfaces/i.regulationsTracker.ts new file mode 100644 index 0000000000..95aef4fed6 --- /dev/null +++ b/Servers/domain.layer/interfaces/i.regulationsTracker.ts @@ -0,0 +1,50 @@ +// Subset of the feed shapes we rely on; ignore other fields (additive-safe). + +export type RegulationChange = + | { field: "regulationCount"; from: number; to: number } + | { field: "regulation.status"; regulation: string; from: string; to: string } + | { field: "regulation.effectiveDate"; regulation: string; from: string; to: string } + | { field: "regulation"; change: "added" | "removed"; value: string }; + +export interface IFeedCountryHistory { + firstAssessed: string; + lastChanged: string; + lastChecked: string; + assessmentCount: number; + hashHistory: { date: string; hash: string; regulationCount: number }[]; + lastChange: { date: string; changes: RegulationChange[] } | null; +} + +// The manifest's per-country entry (what we store + hash on). +export interface IManifestCountry { + slug: string; + name: string; + region: string; + regulationCount: number; + hash: string; + history: IFeedCountryHistory | null; + url: string; +} + +export interface IManifest { + feedVersion: number; + generatedAt: string; + meta: Record; + counts: Record; + countries: IManifestCountry[]; +} + +// Row shape for the global catalog table. +export interface IRegulationCountry { + id?: number; + slug: string; + name: string; + region?: string | null; + regulation_count?: number | null; + data: IManifestCountry; + hash: string; + is_active: boolean; + removed_at?: Date | null; + last_changed_at?: Date | null; + last_fetched_at?: Date | null; +} diff --git a/Servers/domain.layer/models/regulationsTracker/regulationCountry.model.ts b/Servers/domain.layer/models/regulationsTracker/regulationCountry.model.ts new file mode 100644 index 0000000000..7d2f90a55c --- /dev/null +++ b/Servers/domain.layer/models/regulationsTracker/regulationCountry.model.ts @@ -0,0 +1,41 @@ +import { Column, DataType, Model, Table } from "sequelize-typescript"; +import { IManifestCountry, IRegulationCountry } from "../../interfaces/i.regulationsTracker"; + +@Table({ tableName: "regulation_countries", timestamps: false }) +export class RegulationCountryModel + extends Model + implements IRegulationCountry +{ + @Column({ type: DataType.INTEGER, autoIncrement: true, primaryKey: true }) + id?: number; + + @Column({ type: DataType.STRING(120), allowNull: false }) + slug!: string; + + @Column({ type: DataType.STRING(255), allowNull: false }) + name!: string; + + @Column({ type: DataType.STRING(50), allowNull: true }) + region?: string | null; + + @Column({ type: DataType.SMALLINT, allowNull: true }) + regulation_count?: number | null; + + @Column({ type: DataType.JSONB, allowNull: false }) + data!: IManifestCountry; + + @Column({ type: DataType.STRING(80), allowNull: false }) + hash!: string; + + @Column({ type: DataType.BOOLEAN, allowNull: false, defaultValue: true }) + is_active!: boolean; + + @Column({ type: DataType.DATE, allowNull: true }) + removed_at?: Date | null; + + @Column({ type: DataType.DATE, allowNull: true }) + last_changed_at?: Date | null; + + @Column({ type: DataType.DATE, allowNull: true }) + last_fetched_at?: Date | null; +} diff --git a/Servers/domain.layer/models/regulationsTracker/regulationTrackedCountry.model.ts b/Servers/domain.layer/models/regulationsTracker/regulationTrackedCountry.model.ts new file mode 100644 index 0000000000..4d00b2169c --- /dev/null +++ b/Servers/domain.layer/models/regulationsTracker/regulationTrackedCountry.model.ts @@ -0,0 +1,19 @@ +import { Column, DataType, Model, Table } from "sequelize-typescript"; + +@Table({ tableName: "regulation_tracked_countries", timestamps: false }) +export class RegulationTrackedCountryModel extends Model { + @Column({ type: DataType.INTEGER, autoIncrement: true, primaryKey: true }) + id?: number; + + @Column({ type: DataType.INTEGER, allowNull: false }) + organization_id!: number; + + @Column({ type: DataType.STRING(120), allowNull: false }) + country_slug!: string; + + @Column({ type: DataType.INTEGER, allowNull: true }) + tracked_by?: number; + + @Column({ type: DataType.DATE, allowNull: false }) + created_at?: Date; +} diff --git a/Servers/domain.layer/models/regulationsTracker/regulationTrackerMeta.model.ts b/Servers/domain.layer/models/regulationsTracker/regulationTrackerMeta.model.ts new file mode 100644 index 0000000000..31ec6f99ac --- /dev/null +++ b/Servers/domain.layer/models/regulationsTracker/regulationTrackerMeta.model.ts @@ -0,0 +1,16 @@ +import { Column, DataType, Model, Table } from "sequelize-typescript"; + +@Table({ tableName: "regulation_tracker_meta", timestamps: false }) +export class RegulationTrackerMetaModel extends Model { + @Column({ type: DataType.INTEGER, primaryKey: true }) + id!: number; + + @Column({ type: DataType.DATE, allowNull: true }) + seeded_at?: Date | null; + + @Column({ type: DataType.INTEGER, allowNull: true }) + last_good_count?: number | null; + + @Column({ type: DataType.STRING(10), allowNull: true }) + last_run_week?: string | null; +} diff --git a/Servers/domain.layer/models/regulationsTracker/regulationTrackerSettings.model.ts b/Servers/domain.layer/models/regulationsTracker/regulationTrackerSettings.model.ts new file mode 100644 index 0000000000..92a064e45c --- /dev/null +++ b/Servers/domain.layer/models/regulationsTracker/regulationTrackerSettings.model.ts @@ -0,0 +1,19 @@ +import { Column, DataType, Model, Table } from "sequelize-typescript"; + +@Table({ tableName: "regulation_tracker_settings", timestamps: false }) +export class RegulationTrackerSettingsModel extends Model { + @Column({ type: DataType.INTEGER, primaryKey: true }) + organization_id!: number; + + @Column({ type: DataType.JSONB, allowNull: false, defaultValue: [] }) + recipient_user_ids!: number[]; + + @Column({ type: DataType.JSONB, allowNull: false, defaultValue: [] }) + recipient_emails!: string[]; + + @Column({ type: DataType.INTEGER, allowNull: true }) + updated_by?: number; + + @Column({ type: DataType.DATE, allowNull: false }) + updated_at?: Date; +} diff --git a/Servers/jobs/producer.ts b/Servers/jobs/producer.ts index 2a6d4cc46a..220eb63c0e 100644 --- a/Servers/jobs/producer.ts +++ b/Servers/jobs/producer.ts @@ -14,6 +14,7 @@ import { scheduleAiGatewayCacheCleanup, scheduleMcpGatewayCleanup, scheduleAiTrustIndexSync, + scheduleRegulationsTrackerSync, } from "../services/automations/automationProducer"; export async function addAllJobs(): Promise { @@ -28,7 +29,11 @@ export async function addAllJobs(): Promise { await scheduleAiGatewayRiskDetection(); await scheduleAiGatewayCacheCleanup(); await scheduleMcpGatewayCleanup(); - await scheduleAiTrustIndexSync(); // MUST be last — earlier schedulers obliterate the queue + await scheduleAiTrustIndexSync(); + await scheduleRegulationsTrackerSync(); + // Registration order is no longer significant: no scheduler obliterates the + // shared queue any more (see automationProducer.ts). Each repeatable add is + // idempotent by repeat key, so jobs survive regardless of order. } if (require.main === module) { diff --git a/Servers/middleware/rateLimit.middleware.ts b/Servers/middleware/rateLimit.middleware.ts index d5ad565f55..ec448ddc9d 100644 --- a/Servers/middleware/rateLimit.middleware.ts +++ b/Servers/middleware/rateLimit.middleware.ts @@ -72,6 +72,18 @@ const RATE_LIMIT_CONFIGS: Record = { maxRequests: 10, message: "Too many AI detection scan requests from this IP, please try again after 60 minutes", }, + regulationsTrackerSync: { + windowMinutes: 5, + maxRequests: 5, + message: + "Too many regulations-tracker sync requests, please wait a few minutes before checking again", + }, + regulationsTrackerImpact: { + windowMinutes: 5, + maxRequests: 10, + message: + "Too many impact-analysis refresh requests, please wait a few minutes before trying again", + }, }; /** @@ -134,3 +146,20 @@ export const tokenRefreshLimiter = createRateLimiter(RATE_LIMIT_CONFIGS.tokenRef * Moderate limits as scans are resource-intensive */ export const aiDetectionScanLimiter = createRateLimiter(RATE_LIMIT_CONFIGS.aiDetectionScan); + +/** + * Rate limiter for the admin-triggered "check for updates now" regulations sync. + * Each run hits the external feed plus up to ~60 detail fetches, so cap manual + * triggers to a few per 5-minute window. + */ +export const regulationsTrackerSyncLimiter = createRateLimiter( + RATE_LIMIT_CONFIGS.regulationsTrackerSync, +); + +/** + * Rate limiter for the admin-triggered impact-analysis refresh. Each run can + * issue several LLM calls, so cap manual refreshes per 5-minute window. + */ +export const regulationsTrackerImpactLimiter = createRateLimiter( + RATE_LIMIT_CONFIGS.regulationsTrackerImpact, +); diff --git a/Servers/routes/regulationsTracker.route.ts b/Servers/routes/regulationsTracker.route.ts new file mode 100644 index 0000000000..ee81a845a8 --- /dev/null +++ b/Servers/routes/regulationsTracker.route.ts @@ -0,0 +1,48 @@ +import express from "express"; +import authenticateJWT from "../middleware/auth.middleware"; +import { + regulationsTrackerSyncLimiter, + regulationsTrackerImpactLimiter, +} from "../middleware/rateLimit.middleware"; +import { + getCountries, + getCountryDetail, + getTracked, + trackCountryCtrl, + trackBulkCtrl, + untrackCountryCtrl, + getSettingsCtrl, + updateSettingsCtrl, + getHorizon, + getDeadlines, + getFrameworks, + triggerSync, + getImpactAnalysis, + refreshImpactAnalysis, +} from "../controllers/regulationsTracker.ctrl"; + +const router = express.Router(); + +router.get("/countries", authenticateJWT, getCountries); +// MUST be registered before "/countries/:slug" — Express is greedy on path params, +// otherwise "/countries/france/impact" would route to getCountryDetail with slug="france/impact". +router.get("/countries/:slug/impact", authenticateJWT, getImpactAnalysis); +router.post( + "/countries/:slug/impact/refresh", + authenticateJWT, + regulationsTrackerImpactLimiter, + refreshImpactAnalysis, +); +router.get("/countries/:slug", authenticateJWT, getCountryDetail); +router.get("/tracked", authenticateJWT, getTracked); +router.post("/tracked/bulk", authenticateJWT, trackBulkCtrl); +router.post("/tracked", authenticateJWT, trackCountryCtrl); +router.delete("/tracked/:slug", authenticateJWT, untrackCountryCtrl); +router.get("/settings", authenticateJWT, getSettingsCtrl); +router.put("/settings", authenticateJWT, updateSettingsCtrl); +router.get("/horizon", authenticateJWT, getHorizon); +router.get("/deadlines", authenticateJWT, getDeadlines); +router.get("/frameworks", authenticateJWT, getFrameworks); +router.post("/sync", authenticateJWT, regulationsTrackerSyncLimiter, triggerSync); + +export default router; diff --git a/Servers/services/automations/actions/__tests__/syncRegulationsTracker.test.ts b/Servers/services/automations/actions/__tests__/syncRegulationsTracker.test.ts new file mode 100644 index 0000000000..053d6d8689 --- /dev/null +++ b/Servers/services/automations/actions/__tests__/syncRegulationsTracker.test.ts @@ -0,0 +1,642 @@ +import { sectionMjml, syncRegulationsTracker } from "../syncRegulationsTracker"; + +// --------------------------------------------------------------------------- +// Module mocks +// --------------------------------------------------------------------------- + +// jest.mock paths are resolved relative to the TEST FILE, not the source file. +// Test file is at: services/automations/actions/__tests__/ +// Source file is at: services/automations/actions/ +// utils/ is at: utils/ (4 levels up from __tests__) + +jest.mock("../../../../utils/regulationsTrackerFeed", () => ({ + fetchManifest: jest.fn(), + validateManifest: jest.fn(), + fetchCountryDetail: jest.fn().mockResolvedValue({ country: {}, meta: null }), + fetchHorizon: jest.fn().mockResolvedValue({ changes: [] }), + fetchDeadlines: jest.fn().mockResolvedValue({ deadlines: [], unscheduled: [] }), + fetchSnapshot: jest.fn().mockResolvedValue({ frameworks: [] }), +})); + +jest.mock("../../../../utils/regulationsTracker.utils", () => { + const actual = jest.requireActual("../../../../utils/regulationsTracker.utils"); + return { + ...actual, + getMetaQuery: jest.fn(), + upsertFeedTx: jest.fn(), + getAffectedOrgsBySlugs: jest.fn(), + getAllOrgAdmins: jest.fn().mockResolvedValue([]), + resolveEmailRecipients: jest.fn(), + resolveInAppUserIds: jest.fn(), + getStoredHashes: jest.fn().mockResolvedValue(new Map()), + setGlobalFeeds: jest.fn().mockResolvedValue(undefined), + recordRunStatus: jest.fn().mockResolvedValue(undefined), + getSettings: jest.fn().mockResolvedValue({ impact_enabled: true }), + setLastImpactRunAt: jest.fn().mockResolvedValue(undefined), + // keep the day-key helper and escapeHtml real + currentIsoDay: actual.currentIsoDay, + escapeHtml: actual.escapeHtml, + }; +}); + +jest.mock("../../../../utils/regulationImpact.utils", () => ({ + runImpactAnalysis: jest.fn().mockResolvedValue({ status: "ok", counts: {} }), +})); + +jest.mock("../../../../utils/llmKey.utils", () => ({ + getLLMKeysWithKeyQuery: jest.fn().mockResolvedValue([]), +})); + +jest.mock("../../../emailService", () => ({ + sendAutomationEmail: jest.fn(), +})); + +jest.mock("../../../../utils/notification.utils", () => ({ + createNotificationQuery: jest.fn(), +})); + +// compileMjmlToHtml returns a JSON string of the injected vars so we can +// assert on the detail text in the email body without needing a real MJML file. +jest.mock("../../../../tools/mjmlCompiler", () => ({ + compileMjmlToHtml: jest.fn((_template: string, vars: Record) => + JSON.stringify(vars), + ), +})); + +// --------------------------------------------------------------------------- +// Imports after mocks are registered +// --------------------------------------------------------------------------- + +import * as feedUtils from "../../../../utils/regulationsTrackerFeed"; +import * as trackerUtils from "../../../../utils/regulationsTracker.utils"; +import * as emailService from "../../../emailService"; +import * as notificationUtils from "../../../../utils/notification.utils"; +import * as impactUtils from "../../../../utils/regulationImpact.utils"; +import * as llmKeyUtils from "../../../../utils/llmKey.utils"; + +// --------------------------------------------------------------------------- +// Typed mock helpers +// --------------------------------------------------------------------------- + +const mockGetMeta = trackerUtils.getMetaQuery as jest.MockedFunction< + typeof trackerUtils.getMetaQuery +>; +const mockUpsert = trackerUtils.upsertFeedTx as jest.MockedFunction< + typeof trackerUtils.upsertFeedTx +>; +const mockGetAffected = trackerUtils.getAffectedOrgsBySlugs as jest.MockedFunction< + typeof trackerUtils.getAffectedOrgsBySlugs +>; +const mockResolveEmail = trackerUtils.resolveEmailRecipients as jest.MockedFunction< + typeof trackerUtils.resolveEmailRecipients +>; +const mockResolveInApp = trackerUtils.resolveInAppUserIds as jest.MockedFunction< + typeof trackerUtils.resolveInAppUserIds +>; +const mockValidate = feedUtils.validateManifest as jest.MockedFunction< + typeof feedUtils.validateManifest +>; +const mockSendEmail = emailService.sendAutomationEmail as jest.MockedFunction< + typeof emailService.sendAutomationEmail +>; +const mockCreateNotification = notificationUtils.createNotificationQuery as jest.MockedFunction< + typeof notificationUtils.createNotificationQuery +>; + +// --------------------------------------------------------------------------- +// Shared fixtures +// --------------------------------------------------------------------------- + +// Minimal feed injected via deps.feed — keeps fetchManifest uncalled. +const DUMMY_FEED = { version: 1, countries: [] }; + +// Build a ValidateResult for a valid feed. +function makeValidResult(countries: any[] = []) { + return { + ok: true as const, + countries, + presentSlugs: countries.map((c: any) => c.slug), + rawCount: countries.length, + }; +} + +// Current UTC day key (real util so the daily guard fires correctly). The key is +// stored in the legacy-named last_run_week column. OTHER_WEEK is any value that +// can never equal today's day string, used by tests that must NOT hit the skip. +const THIS_WEEK = trackerUtils.currentIsoDay(new Date()); +const OTHER_WEEK = "2000-01-01"; + +// --------------------------------------------------------------------------- +// sectionMjml — existing tests (preserved) +// --------------------------------------------------------------------------- + +describe("sectionMjml", () => { + it("returns empty string for no items", () => { + expect(sectionMjml("Changed", [])).toBe(""); + }); + it("escapes item names and renders bullet lines", () => { + const out = sectionMjml("Changed", [{ name: "", detail: "status a → b" }]); + expect(out).toContain("<EU>"); + expect(out).toContain("status a → b"); + }); +}); + +// --------------------------------------------------------------------------- +// syncRegulationsTracker — job-level tests +// --------------------------------------------------------------------------- + +describe("syncRegulationsTracker", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + // ------------------------------------------------------------------------- + // 1. Week-guard skip + // ------------------------------------------------------------------------- + it("skips and does not call validateManifest when the day key equals today (already ran)", async () => { + mockGetMeta.mockResolvedValue({ + seeded_at: "2026-01-01", + last_good_count: 10, + last_run_week: THIS_WEEK, + }); + + const result = await syncRegulationsTracker({ feed: DUMMY_FEED }); + + expect(result.skipped).toMatch(THIS_WEEK); + expect(result.orgsEmailed).toBe(0); + expect(result.orgsNotified).toBe(0); + expect(mockValidate).not.toHaveBeenCalled(); + expect(mockUpsert).not.toHaveBeenCalled(); + }); + + // ------------------------------------------------------------------------- + // 2. First-seed suppression + // ------------------------------------------------------------------------- + it("suppresses email and in-app notifications on first seed", async () => { + mockGetMeta.mockResolvedValue({ + seeded_at: null, + last_good_count: null, + last_run_week: OTHER_WEEK, + }); + + const country = { slug: "de", name: "Germany", hash: "abc", regulationCount: 1 }; + mockValidate.mockReturnValue(makeValidResult([country])); + mockUpsert.mockResolvedValue({ + changed: [], + newlyAdded: [], + newlyRemoved: [], + wasFirstSeed: true, + }); + + const result = await syncRegulationsTracker({ feed: DUMMY_FEED }); + + expect(result.orgsEmailed).toBe(0); + expect(result.orgsNotified).toBe(0); + expect(mockSendEmail).not.toHaveBeenCalled(); + expect(mockCreateNotification).not.toHaveBeenCalled(); + }); + + // ------------------------------------------------------------------------- + // 3. Changed-country path — email + in-app + detail uses joined lines + // ------------------------------------------------------------------------- + it("sends email and in-app notification with joined lines as detail for a changed country", async () => { + mockGetMeta.mockResolvedValue({ + seeded_at: "2026-01-01", + last_good_count: 5, + last_run_week: OTHER_WEEK, + }); + + const country = { slug: "fr", name: "France", hash: "xyz", regulationCount: 2 }; + mockValidate.mockReturnValue(makeValidResult([country])); + + // Two change lines — the digest detail must join them with ", ". + const changedCountry: trackerUtils.CountryChange = { + slug: "fr", + name: "France", + lines: ["AI Act: status draft → enacted", "AI Act: effective date 2026-01-01 → 2026-06-01"], + unstructured: false, + changeCount: 1, + changeDates: [], + }; + mockUpsert.mockResolvedValue({ + changed: [changedCountry], + newlyAdded: [], + newlyRemoved: [], + wasFirstSeed: false, + }); + + // One org (id=42) tracks France. + mockGetAffected.mockResolvedValue([ + { organization_id: 42, country_slug: "fr", name: "France" }, + ]); + + // In-app: one user (id=99). + mockResolveInApp.mockResolvedValue([99]); + mockCreateNotification.mockResolvedValue(undefined as any); + + // Email: one recipient. + mockResolveEmail.mockResolvedValue(["admin@acme.com"]); + mockSendEmail.mockResolvedValue(undefined as any); + + const result = await syncRegulationsTracker({ feed: DUMMY_FEED }); + + // Return counters. + expect(result.orgsEmailed).toBe(1); + expect(result.orgsNotified).toBe(1); + expect(result.changed).toBe(1); + expect(result.newlyRemoved).toBe(0); + + // In-app notification created for user 99 in org 42, deep-linked to the + // country page, with the change detail in the message. + expect(mockCreateNotification).toHaveBeenCalledTimes(1); + expect(mockCreateNotification).toHaveBeenCalledWith( + expect.objectContaining({ + user_id: 99, + action_url: "/regulations-tracker/fr", + entity_name: "France", + message: expect.stringContaining("status draft → enacted"), + }), + 42, + ); + + // Email sent to the right recipient. + expect(mockSendEmail).toHaveBeenCalledTimes(1); + expect(mockSendEmail).toHaveBeenCalledWith( + ["admin@acme.com"], + expect.any(String), + expect.any(String), + undefined, + ); + + // The HTML passed to sendAutomationEmail is the JSON of compileMjmlToHtml's + // vars (our mock serializes them). The changedSection must contain both the + // country name and the full joined lines string — proving the Critical fix. + const htmlArg = mockSendEmail.mock.calls[0][2] as string; + const vars = JSON.parse(htmlArg); + const expectedDetail = changedCountry.lines.join(", "); + expect(vars.changedSection).toContain("France"); + expect(vars.changedSection).toContain(expectedDetail); + }); + + // 4. Multi-change note (#3): when a country changed more than once since last + // check, the in-app message notes the count + dates. + it("notes multiple changes since last check in the in-app message", async () => { + mockGetMeta.mockResolvedValue({ + seeded_at: "2026-01-01", + last_good_count: 5, + last_run_week: OTHER_WEEK, + } as any); + const country = { slug: "fr", name: "France", hash: "xyz", regulationCount: 2 }; + mockValidate.mockReturnValue(makeValidResult([country])); + mockUpsert.mockResolvedValue({ + changed: [ + { + slug: "fr", + name: "France", + lines: ["AI Act: status draft → enacted"], + unstructured: false, + changeCount: 3, + changeDates: ["2026-06-10", "2026-05-02", "2026-04-01"], + }, + ], + newlyAdded: [], + newlyRemoved: [], + wasFirstSeed: false, + }); + mockGetAffected.mockResolvedValue([ + { organization_id: 42, country_slug: "fr", name: "France" }, + ] as any); + mockResolveInApp.mockResolvedValue([99]); + mockResolveEmail.mockResolvedValue([]); + mockCreateNotification.mockResolvedValue(undefined as any); + + await syncRegulationsTracker({ feed: DUMMY_FEED }); + + expect(mockCreateNotification).toHaveBeenCalledWith( + expect.objectContaining({ + message: expect.stringContaining("changed 3 times since last check"), + }), + 42, + ); + }); + + // 5. Impact analysis isolation: a throwing runImpactAnalysis must NOT break the sync. + it("completes successfully even when runImpactAnalysis throws", async () => { + const mockRunImpact = impactUtils.runImpactAnalysis as jest.MockedFunction< + typeof impactUtils.runImpactAnalysis + >; + const mockGetLLMKeys = llmKeyUtils.getLLMKeysWithKeyQuery as jest.MockedFunction< + typeof llmKeyUtils.getLLMKeysWithKeyQuery + >; + const mockRecordRunStatus = trackerUtils.recordRunStatus as jest.MockedFunction< + typeof trackerUtils.recordRunStatus + >; + + // Impact analysis always throws for this test. + mockRunImpact.mockRejectedValue(new Error("LLM exploded")); + // Org has a key → impact will be attempted. + mockGetLLMKeys.mockResolvedValue([{ key: "k", name: "OpenAI", url: null, model: "m" } as any]); + + mockGetMeta.mockResolvedValue({ + seeded_at: "2026-01-01", + last_good_count: 5, + last_run_week: OTHER_WEEK, + } as any); + const country = { slug: "de", name: "Germany", hash: "abc", regulationCount: 1 }; + mockValidate.mockReturnValue(makeValidResult([country])); + mockUpsert.mockResolvedValue({ + changed: [ + { + slug: "de", + name: "Germany", + lines: ["AI Act: status draft → enacted"], + unstructured: false, + changeCount: 1, + changeDates: [], + }, + ], + newlyAdded: [], + newlyRemoved: [], + wasFirstSeed: false, + }); + mockGetAffected.mockResolvedValue([ + { organization_id: 42, country_slug: "de", name: "Germany" }, + ] as any); + mockResolveInApp.mockResolvedValue([99]); + mockResolveEmail.mockResolvedValue([]); + mockCreateNotification.mockResolvedValue(undefined as any); + + // Should not throw. + const result = await syncRegulationsTracker({ feed: DUMMY_FEED }); + + // Sync completed and status was recorded as ok. + expect(result.orgsNotified).toBe(1); + expect(mockRecordRunStatus).toHaveBeenCalledWith(expect.stringContaining("ok")); + // Notifications were still created despite impact throwing. + expect(mockCreateNotification).toHaveBeenCalledTimes(1); + expect(mockCreateNotification).toHaveBeenCalledWith( + expect.objectContaining({ user_id: 99 }), + 42, + ); + }); + + // Unstructured-change suppression: when the feed moved a country's hash but + // carried no structured field-level diff, impact analysis must be skipped + // entirely (no LLM call, no impact panel), while the plain change + // notification still fires so the org knows the country changed. + it("skips impact analysis for an unstructured change but still notifies", async () => { + const mockRunImpact = impactUtils.runImpactAnalysis as jest.MockedFunction< + typeof impactUtils.runImpactAnalysis + >; + const mockGetLLMKeys = llmKeyUtils.getLLMKeysWithKeyQuery as jest.MockedFunction< + typeof llmKeyUtils.getLLMKeysWithKeyQuery + >; + + // Org HAS a key and impact is enabled — so the ONLY reason to skip the LLM + // is the change being unstructured. + mockGetLLMKeys.mockResolvedValue([{ key: "k", name: "OpenAI", url: null, model: "m" } as any]); + + mockGetMeta.mockResolvedValue({ + seeded_at: "2026-01-01", + last_good_count: 5, + last_run_week: OTHER_WEEK, + } as any); + const country = { slug: "de", name: "Germany", hash: "abc", regulationCount: 1 }; + mockValidate.mockReturnValue(makeValidResult([country])); + mockUpsert.mockResolvedValue({ + changed: [ + { + slug: "de", + name: "Germany", + // No structured lines — the hash moved but the feed gave no field diff. + lines: [], + unstructured: true, + changeCount: 1, + changeDates: [], + }, + ], + newlyAdded: [], + newlyRemoved: [], + wasFirstSeed: false, + }); + mockGetAffected.mockResolvedValue([ + { organization_id: 42, country_slug: "de", name: "Germany" }, + ] as any); + mockResolveInApp.mockResolvedValue([99]); + // Email recipient present too, to exercise the email backfill gate. + mockResolveEmail.mockResolvedValue(["admin@acme.com"]); + mockSendEmail.mockResolvedValue(undefined as any); + mockCreateNotification.mockResolvedValue(undefined as any); + + const result = await syncRegulationsTracker({ feed: DUMMY_FEED }); + + // The LLM was never invoked for an unstructured change — neither the in-app + // pass nor the email backfill should have called it. + expect(mockRunImpact).not.toHaveBeenCalled(); + + // The org is still told the country changed (plain notification, no + // "Impact:" suffix, and no "Configure an LLM key" nudge since a key would + // not have produced a panel here anyway). + expect(result.orgsNotified).toBe(1); + expect(mockCreateNotification).toHaveBeenCalledTimes(1); + const notifArg = mockCreateNotification.mock.calls[0][0] as { message: string }; + expect(notifArg.message).not.toContain("Impact:"); + expect(notifArg.message).not.toContain("Configure an LLM key"); + + // Email still goes out, but with no impact section. + expect(mockSendEmail).toHaveBeenCalledTimes(1); + const htmlArg = mockSendEmail.mock.calls[0][2] as string; + const vars = JSON.parse(htmlArg); + expect(vars.impactSection ?? "").toBe(""); + }); + + // BUG 5: Cap counting — cache hits must NOT increment impactAnalysesRun so they + // don't starve countries that need real LLM analysis. + it("does not count cache hits against the per-run impact cap", async () => { + const mockRunImpact = impactUtils.runImpactAnalysis as jest.MockedFunction< + typeof impactUtils.runImpactAnalysis + >; + const mockGetLLMKeys = llmKeyUtils.getLLMKeysWithKeyQuery as jest.MockedFunction< + typeof llmKeyUtils.getLLMKeysWithKeyQuery + >; + + // Org has a key → impact will be attempted. + mockGetLLMKeys.mockResolvedValue([{ key: "k", name: "OpenAI", url: null, model: "m" } as any]); + + mockGetMeta.mockResolvedValue({ + seeded_at: "2026-01-01", + last_good_count: 5, + last_run_week: OTHER_WEEK, + } as any); + + // Two changed countries — fr (cached) and de (real LLM run) + const countries = [ + { slug: "fr", name: "France", hash: "xyz", regulationCount: 2 }, + { slug: "de", name: "Germany", hash: "abc", regulationCount: 1 }, + ]; + mockValidate.mockReturnValue(makeValidResult(countries)); + mockUpsert.mockResolvedValue({ + changed: [ + { + slug: "fr", + name: "France", + lines: ["status draft → enacted"], + unstructured: false, + changeCount: 1, + changeDates: [], + }, + { + slug: "de", + name: "Germany", + lines: ["status draft → enacted"], + unstructured: false, + changeCount: 1, + changeDates: [], + }, + ], + newlyAdded: [], + newlyRemoved: [], + wasFirstSeed: false, + }); + mockGetAffected.mockResolvedValue([ + { organization_id: 42, country_slug: "fr", name: "France" }, + { organization_id: 42, country_slug: "de", name: "Germany" }, + ]); + mockResolveInApp.mockResolvedValue([99]); + mockResolveEmail.mockResolvedValue([]); + mockCreateNotification.mockResolvedValue(undefined as any); + + // France is a cache hit, Germany is a real LLM run + mockRunImpact + .mockResolvedValueOnce({ status: "ok", counts: {}, cached: true } as any) // France (cache) + .mockResolvedValueOnce({ status: "ok", counts: {}, cached: false } as any); // Germany (real) + + await syncRegulationsTracker({ feed: DUMMY_FEED }); + + // runImpactAnalysis was called for both countries + expect(mockRunImpact).toHaveBeenCalledTimes(2); + // Both notifications sent (cap not hit; only 1 real run counted) + expect(mockCreateNotification).toHaveBeenCalledTimes(2); + }); + + // BUG 5b: no_key and skipped_no_candidates must NOT increment impactAnalysesRun. + // A sync covering orgs with no LLM key or no matching candidates must not exhaust + // the per-run cap and starve later orgs that need real analysis. + it("does not count no_key or skipped_no_candidates passes against the per-run impact cap", async () => { + const mockRunImpact = impactUtils.runImpactAnalysis as jest.MockedFunction< + typeof impactUtils.runImpactAnalysis + >; + const mockGetLLMKeys = llmKeyUtils.getLLMKeysWithKeyQuery as jest.MockedFunction< + typeof llmKeyUtils.getLLMKeysWithKeyQuery + >; + + mockGetLLMKeys.mockResolvedValue([{ key: "k", name: "OpenAI", url: null, model: "m" } as any]); + + mockGetMeta.mockResolvedValue({ + seeded_at: "2026-01-01", + last_good_count: 5, + last_run_week: OTHER_WEEK, + } as any); + + // Three changed countries: no_key, skipped_no_candidates, then a real LLM run. + const countries = [ + { slug: "aa", name: "Alpha", hash: "h1", regulationCount: 1 }, + { slug: "bb", name: "Beta", hash: "h2", regulationCount: 1 }, + { slug: "cc", name: "Gamma", hash: "h3", regulationCount: 1 }, + ]; + mockValidate.mockReturnValue(makeValidResult(countries)); + mockUpsert.mockResolvedValue({ + changed: [ + { + slug: "aa", + name: "Alpha", + lines: ["a"], + unstructured: false, + changeCount: 1, + changeDates: [], + }, + { + slug: "bb", + name: "Beta", + lines: ["b"], + unstructured: false, + changeCount: 1, + changeDates: [], + }, + { + slug: "cc", + name: "Gamma", + lines: ["c"], + unstructured: false, + changeCount: 1, + changeDates: [], + }, + ], + newlyAdded: [], + newlyRemoved: [], + wasFirstSeed: false, + }); + mockGetAffected.mockResolvedValue([ + { organization_id: 42, country_slug: "aa", name: "Alpha" }, + { organization_id: 42, country_slug: "bb", name: "Beta" }, + { organization_id: 42, country_slug: "cc", name: "Gamma" }, + ]); + mockResolveInApp.mockResolvedValue([99]); + mockResolveEmail.mockResolvedValue([]); + mockCreateNotification.mockResolvedValue(undefined as any); + + // First two return non-LLM statuses (cached: false but no LLM call made) + mockRunImpact + .mockResolvedValueOnce({ status: "no_key", result: null, counts: {}, cached: false } as any) + .mockResolvedValueOnce({ + status: "skipped_no_candidates", + result: null, + counts: {}, + cached: false, + } as any) + .mockResolvedValueOnce({ status: "ok", counts: {}, cached: false } as any); // real LLM run + + await syncRegulationsTracker({ feed: DUMMY_FEED }); + + // All three countries were processed — cap was NOT exhausted by the non-LLM passes + expect(mockRunImpact).toHaveBeenCalledTimes(3); + // Notifications sent for all three tracked orgs + expect(mockCreateNotification).toHaveBeenCalledTimes(3); + }); + + // 6. New-country awareness (#4): brand-new countries notify each org's admins. + it("notifies org admins when a new country is added", async () => { + mockGetMeta.mockResolvedValue({ + seeded_at: "2026-01-01", + last_good_count: 5, + last_run_week: OTHER_WEEK, + } as any); + const country = { slug: "newland", name: "Newland", hash: "n1", regulationCount: 1 }; + mockValidate.mockReturnValue(makeValidResult([country])); + mockUpsert.mockResolvedValue({ + changed: [], + newlyAdded: ["newland"], + newlyRemoved: [], + wasFirstSeed: false, + }); + mockGetAffected.mockResolvedValue([]); + (trackerUtils.getAllOrgAdmins as jest.Mock).mockResolvedValue([ + { organization_id: 7, user_id: 1 }, + { organization_id: 7, user_id: 2 }, + ]); + mockCreateNotification.mockResolvedValue(undefined as any); + + await syncRegulationsTracker({ feed: DUMMY_FEED }); + + // Both admins of org 7 get a deep-linked "new jurisdiction" notification. + expect(mockCreateNotification).toHaveBeenCalledWith( + expect.objectContaining({ + user_id: 1, + action_url: "/regulations-tracker/newland", + title: expect.stringContaining("New jurisdiction"), + }), + 7, + ); + expect(mockCreateNotification).toHaveBeenCalledWith(expect.objectContaining({ user_id: 2 }), 7); + }); +}); diff --git a/Servers/services/automations/actions/syncRegulationsTracker.ts b/Servers/services/automations/actions/syncRegulationsTracker.ts new file mode 100644 index 0000000000..b50343fba7 --- /dev/null +++ b/Servers/services/automations/actions/syncRegulationsTracker.ts @@ -0,0 +1,723 @@ +import { promises as fs } from "fs"; +import path from "path"; +import { + fetchManifest, + validateManifest, + fetchCountryDetail, + fetchHorizon, + fetchDeadlines, + fetchSnapshot, +} from "../../../utils/regulationsTrackerFeed"; +import { + getMetaQuery, + getStoredHashes, + upsertFeedTx, + setGlobalFeeds, + recordRunStatus, + getAffectedOrgsBySlugs, + getAllOrgAdmins, + resolveEmailRecipients, + resolveInAppUserIds, + currentIsoDay, + escapeHtml, + getSettings, + setLastImpactRunAt, + acquireSyncLock, + CountryChange, +} from "../../../utils/regulationsTracker.utils"; +import { runImpactAnalysis, ImpactResult } from "../../../utils/regulationImpact.utils"; +import { getLLMKeysWithKeyQuery } from "../../../utils/llmKey.utils"; +import { createNotificationQuery } from "../../../utils/notification.utils"; +import { + NotificationType, + NotificationEntityType, +} from "../../../domain.layer/interfaces/i.notification"; +import { sendAutomationEmail } from "../../emailService"; +import { compileMjmlToHtml } from "../../../tools/mjmlCompiler"; +import logger from "../../../utils/logger/fileLogger"; + +const IMPACT_MAX_ANALYSES_PER_RUN = 200; + +const FRONTEND = process.env.FRONTEND_URL ?? "http://localhost:5173"; +const MODULE_URL = FRONTEND + "/regulations-tracker/browse"; +const TRACKED_URL = FRONTEND + "/regulations-tracker/tracked"; +const SETTINGS_URL = FRONTEND + "/regulations-tracker/settings"; + +export interface DigestItem { + name: string; + detail?: string; +} + +// Per-run tally of impact-analysis outcomes, for the feed-quality / value +// telemetry logged at the end of a sync. See the call sites in run(). +export interface ImpactOutcomeTally { + ok: number; + skipped_no_candidates: number; + error: number; + no_key: number; + cached: number; +} + +/** + * Classify one runImpactAnalysis result into the tally. A cached `ok` is + * recorded as `cached` (reused analysis), not as a fresh `ok`, so the two + * signals stay distinct. Unknown statuses fall into `error` so nothing is + * silently dropped. + */ +export function tallyImpact( + tally: ImpactOutcomeTally, + impact: { status: string; cached: boolean }, +): void { + if (impact.cached) { + tally.cached += 1; + return; + } + switch (impact.status) { + case "ok": + tally.ok += 1; + break; + case "skipped_no_candidates": + tally.skipped_no_candidates += 1; + break; + case "no_key": + tally.no_key += 1; + break; + default: + tally.error += 1; + } +} + +export function sectionMjml(title: string, items: DigestItem[]): string { + if (!items.length) return ""; + const header = `${escapeHtml(title)}`; + const lines = items + .map((it) => { + const label = it.detail ? `${it.name} — ${it.detail}` : it.name; + return `• ${escapeHtml(label)}`; + }) + .join(""); + return header + lines; +} + +// One country's impact analysis, prepared for the email digest. `result` is the +// full ImpactResult (per-entity name + reason), not just counts — the email has +// the room the cramped in-app notification does not. +export interface ImpactDigestItem { + countryName: string; + result: ImpactResult; +} + +// Renders the "How these changes affect your organization" block: per country, +// each affected entity group (systems / controls / policies / vendors / +// assessments) with the specific item names and the LLM's one-line reason. Only +// groups with at least one affected entity are shown. +export function impactSectionMjml(items: ImpactDigestItem[]): string { + const GROUPS: { key: keyof Omit; label: string }[] = [ + { key: "systems", label: "AI systems" }, + { key: "controls", label: "Controls to review" }, + { key: "policies", label: "Policies that may be outdated" }, + { key: "vendors", label: "Vendors impacted" }, + { key: "assessments", label: "Assessments to update" }, + ]; + + const blocks = items + .map(({ countryName, result }) => { + const groupMjml = GROUPS.map(({ key, label }) => { + const entities = result[key]; + if (!entities.length) return ""; + const groupHeader = `${escapeHtml( + label, + )} (${entities.length})`; + const rows = entities + .map( + (e) => + `${escapeHtml( + e.name, + )} — ${escapeHtml(e.why)}`, + ) + .join(""); + return groupHeader + rows; + }).join(""); + + if (!groupMjml) return ""; // nothing affected for this country + const countryHeader = `${escapeHtml( + countryName, + )}`; + return countryHeader + groupMjml; + }) + .filter(Boolean) + .join(""); + + if (!blocks) return ""; + + return ( + `` + + `How these changes affect your organization` + + `Based on your AI systems, controls, policies, vendors and assessments. Generated automatically — review before acting.` + + blocks + ); +} + +async function renderDigest( + changed: DigestItem[], + removed: DigestItem[], + impact: ImpactDigestItem[] = [], +): Promise { + const tmplPath = path.join(__dirname, "../../../templates/regulations-tracker-digest.mjml"); + const template = await fs.readFile(tmplPath, "utf8"); + return compileMjmlToHtml(template, { + changedSection: sectionMjml("Changed", changed), + removedSection: sectionMjml("No longer in the feed", removed), + impactSection: impactSectionMjml(impact), + moduleUrl: MODULE_URL, + trackedUrl: TRACKED_URL, + settingsUrl: SETTINGS_URL, + }); +} + +// In-process guard so an admin "check for updates now" can't run concurrently +// with the scheduled daily job (or with another admin trigger). The two would +// otherwise both hit the external feed (~60 detail fetches each) and race on the +// global-feed / run-status writes that sit outside upsertFeedTx's row lock. +// In-process fast-path guard: cheaply short-circuits a second concurrent call +// within the SAME process without a DB round-trip. The authoritative, +// cross-process guard is the Postgres advisory lock below (acquireSyncLock). +let syncInProgress = false; + +const SKIPPED_RESULT = (reason: string) => ({ + fetched: 0, + changed: 0, + newlyAdded: 0, + newlyRemoved: 0, + orgsEmailed: 0, + orgsNotified: 0, + skipped: reason, +}); + +export async function syncRegulationsTracker(deps?: { feed?: unknown }): Promise<{ + fetched: number; + changed: number; + newlyAdded: number; + newlyRemoved: number; + orgsEmailed: number; + orgsNotified: number; + skipped?: string; +}> { + if (syncInProgress) { + logger.info("[regulations-tracker] sync already in progress (same process); skipping"); + return SKIPPED_RESULT("already running"); + } + // Cross-process lock: prevents the daily cron and an admin trigger (or two + // worker pods) from both hitting the external feed and racing on the global + // writes that sit outside upsertFeedTx's row lock. + const release = await acquireSyncLock(); + if (!release) { + logger.info("[regulations-tracker] sync already in progress (another process); skipping"); + return SKIPPED_RESULT("already running"); + } + syncInProgress = true; + try { + return await runSync(deps); + } finally { + syncInProgress = false; + await release(); + } +} + +async function runSync(deps?: { feed?: unknown }): Promise<{ + fetched: number; + changed: number; + newlyAdded: number; + newlyRemoved: number; + orgsEmailed: number; + orgsNotified: number; + skipped?: string; +}> { + const meta = await getMetaQuery(); + // Daily idempotency: the cron fires every morning, but we only fetch + diff + // once per UTC day. The day key is stored in the legacy last_run_week column. + const today = currentIsoDay(new Date()); + if (meta.last_run_week === today) + return { + fetched: 0, + changed: 0, + newlyAdded: 0, + newlyRemoved: 0, + orgsEmailed: 0, + orgsNotified: 0, + skipped: `already ran ${today}`, + }; + + let raw: unknown; + try { + raw = deps?.feed ?? (await fetchManifest()); + } catch (e) { + logger.error(`[regulations-tracker] feed fetch failed: ${(e as Error).message}`); + await recordRunStatus("fetch failed").catch(() => undefined); + return { + fetched: 0, + changed: 0, + newlyAdded: 0, + newlyRemoved: 0, + orgsEmailed: 0, + orgsNotified: 0, + skipped: "fetch failed", + }; + } + + const validated = validateManifest(raw, meta.last_good_count ?? null); + if (!validated.ok) { + logger.error(`[regulations-tracker] feed rejected: ${validated.reason}`); + await recordRunStatus(`rejected: ${validated.reason}`).catch(() => undefined); + return { + fetched: 0, + changed: 0, + newlyAdded: 0, + newlyRemoved: 0, + orgsEmailed: 0, + orgsNotified: 0, + skipped: validated.reason, + }; + } + + // Fetch full per-country detail (regulations/timeline/meta) for countries that + // are new or whose hash moved, so the catalog stores complete content (the + // detail page renders from our DB without a per-request external call). Stored + // as data = { ...country, meta }, matching the live-fetch shape in the detail + // controller. A per-country fetch failure is non-fatal — that country falls + // back to its manifest summary for this run. + const norm = (s: string) => s.trim().toLowerCase(); + const storedHashes = await getStoredHashes(validated.countries.map((c) => c.slug)); + const staleForDetail = validated.countries.filter( + (c) => storedHashes.get(norm(c.slug)) !== c.hash, + ); + const detailBySlug = new Map(); + for (const c of staleForDetail) { + try { + const d = (await fetchCountryDetail(norm(c.slug))) as { + country?: Record; + meta?: Record; + }; + if (d.country) detailBySlug.set(norm(c.slug), { ...d.country, meta: d.meta ?? null }); + } catch (e) { + logger.warn( + `[regulations-tracker] detail fetch failed for ${c.slug}: ${(e as Error).message}; storing summary`, + ); + } + } + + const { changed, newlyAdded, newlyRemoved, wasFirstSeed } = await upsertFeedTx( + validated.countries, + validated.presentSlugs, + // Persist the VALID country count as last_good_count, not rawCount. The + // 50%-drop guard compares the next run's valid count against this watermark, + // so storing the (larger) raw count would inflate the baseline and could + // wrongly reject a later, legitimately-smaller-but-valid feed. + validated.countries.length, + detailBySlug, + ); + + // Refresh the three global, non-tenant feeds (changelog / deadlines / + // frameworks) cached on the meta singleton. Best-effort: a failure here must + // not abort the country sync or notifications. + try { + const [horizon, deadlines, snapshot] = await Promise.all([ + fetchHorizon().catch(() => undefined), + fetchDeadlines().catch(() => undefined), + fetchSnapshot().catch(() => undefined), + ]); + await setGlobalFeeds({ + horizon, + deadlines, + frameworks: (snapshot as { frameworks?: unknown[] } | undefined)?.frameworks, + }); + } catch (e) { + logger.warn(`[regulations-tracker] global feed refresh failed: ${(e as Error).message}`); + } + + if (wasFirstSeed) { + logger.info( + `[regulations-tracker] first seed (${validated.countries.length}); notifications suppressed`, + ); + await recordRunStatus(`ok: first seed (${validated.countries.length})`).catch(() => undefined); + return { + fetched: validated.countries.length, + changed: 0, + newlyAdded: 0, + newlyRemoved: 0, + orgsEmailed: 0, + orgsNotified: 0, + }; + } + + const changeBySlug = new Map(changed.map((c) => [c.slug, c])); + const changedSlugs = Array.from(new Set([...changed.map((c) => c.slug), ...newlyRemoved])); + + // Feed-quality signal: a "changed" country is only actionable for impact + // analysis when the feed gives us a structured per-field diff (status / + // effective-date / regulation added-or-removed). When `unstructured` is true + // the hash moved but the feed carried no field-level changes, so the LLM is + // told "(no structured diff available)" and its verdicts degrade to generic. + // Tracking the split over time tells us whether the enriched email is + // delivering change-specific value or just "something changed" noise. + const structuredChanges = changed.filter((c) => !c.unstructured).length; + const unstructuredChanges = changed.length - structuredChanges; + if (changed.length) { + logger.info( + `[regulations-tracker] feed diff quality: ${structuredChanges}/${changed.length} changed countries had a structured field-level diff, ${unstructuredChanges} were hash-only (no structured diff)`, + ); + if (unstructuredChanges) { + logger.warn( + `[regulations-tracker] ${unstructuredChanges} changed countr${ + unstructuredChanges === 1 ? "y" : "ies" + } had no structured diff this run: ${changed + .filter((c) => c.unstructured) + .map((c) => c.slug) + .join(", ")} — impact analysis for these will be generic`, + ); + } + } + + // Impact-run outcome tally across every org/country pass this run. Lets us + // measure, in production, how often the LLM pass actually produced a verdict + // ("ok") versus had nothing to judge ("skipped_no_candidates"), failed + // ("error"), or was gated out ("no_key"). Cache hits are counted separately + // since they reflect reused, not freshly-generated, analysis. + const impactOutcomes = { + ok: 0, + skipped_no_candidates: 0, + error: 0, + no_key: 0, + cached: 0, + }; + + let orgsEmailed = 0; + let orgsNotified = 0; + let orgsNotifiedNew = 0; + + // The catalogue is already committed by upsertFeedTx (and last_run_week is set, + // so we won't re-import this week — that's correct, the data landed). But if + // notification/email dispatch throws, we must NOT leave last_run_status showing + // the previous "ok": record the failure and rethrow so the job is marked failed + // and the Settings page reflects that notifications didn't go out. + try { + if (changedSlugs.length) { + const affected = await getAffectedOrgsBySlugs(changedSlugs); + // Per-org buckets carry each affected country's slug + name + (for changed + // countries) the human change lines, so we can both build the email digest + // and emit one deep-linked in-app notification per country. + interface OrgCountry { + slug: string; + name: string; + removed: boolean; + lines: string[]; + changeCount: number; + changeDates: string[]; + // True when the feed moved the country's hash but carried no structured + // field-level diff. Impact analysis is skipped for these: with no diff + // to judge, the LLM would only produce a generic verdict, so we don't + // run it, don't show an impact block, and don't burn LLM capacity. + unstructured: boolean; + } + const byOrg = new Map(); + for (const row of affected) { + const list = byOrg.get(row.organization_id) ?? []; + const name = row.name ?? row.country_slug; + const removed = newlyRemoved.includes(row.country_slug); + const ch = changeBySlug.get(row.country_slug); + list.push({ + slug: row.country_slug, + name, + removed, + lines: ch?.lines ?? [], + changeCount: ch?.changeCount ?? 1, + changeDates: ch?.changeDates ?? [], + unstructured: ch?.unstructured ?? false, + }); + byOrg.set(row.organization_id, list); + } + + let impactAnalysesRun = 0; + let impactCapLogged = false; + // Count orgs whose dispatch failed, so a partial failure is visible in the + // run status instead of either aborting the whole job (losing every + // not-yet-notified org) or silently passing. + let orgsFailed = 0; + + for (const [orgId, countries] of byOrg) { + // Isolate each org's dispatch: a failure for one org (DB blip, email + // provider error) must not abort the loop and starve every subsequent + // org of its notification. We log + count the failure and move on, so + // the day-key guard doesn't then make a retry skip the whole run. + try { + let orgHasKey = false; + let impactEnabled = true; + let impactRan = false; // becomes true if at least one impact pass executes this run + try { + orgHasKey = (await getLLMKeysWithKeyQuery(orgId)).length > 0; + const orgSettings = await getSettings(orgId); + impactEnabled = orgSettings.impact_enabled !== false; // default ON + } catch { + orgHasKey = false; + } + + const changedItems: DigestItem[] = countries + .filter((c) => !c.removed) + .map((c) => ({ + name: c.name, + detail: c.lines.length ? c.lines.join(", ") : undefined, + })); + const removedItems: DigestItem[] = countries + .filter((c) => c.removed) + .map((c) => ({ name: c.name })); + + // Per-country impact results captured during analysis below, for the + // email digest (the verbose, roomy surface). Keyed by slug so a country + // analyzed for in-app notifications is reused for the email too. + const impactBySlug = new Map(); + + // In-app: always to admins ∪ configured recipients. One deep-linked + // notification per affected country so the message names what changed and + // links straight to that country's page. + const userIds = await resolveInAppUserIds(orgId); + if (userIds.length) { + for (const c of countries) { + const title = c.removed + ? `${c.name} removed from the regulations feed` + : `AI regulations updated: ${c.name}`; + // When a country changed more than once since our last check, the feed + // only carries the latest change's detail — note the count + dates so + // the user knows to review the full history at the source. + const multiNote = + !c.removed && c.changeCount > 1 + ? ` (changed ${c.changeCount} times since last check${ + c.changeDates.length ? `: ${c.changeDates.join(", ")}` : "" + }; showing the latest)` + : ""; + const baseMessage = c.removed + ? `${c.name} is no longer in the regulations feed.` + : (c.lines.length + ? c.lines.join("; ") + : "Regulations were updated — open to see the details.") + multiNote; + let impactSuffix = ""; + if (!c.removed) { + if (orgHasKey && impactEnabled && !c.unstructured) { + if (impactAnalysesRun >= IMPACT_MAX_ANALYSES_PER_RUN) { + if (!impactCapLogged) { + logger.warn( + `[regulations-tracker] per-run impact cap (${IMPACT_MAX_ANALYSES_PER_RUN}) reached; skipping remaining LLM analyses for this sync`, + ); + impactCapLogged = true; + } + } else { + impactRan = true; + try { + const impact = await runImpactAnalysis(orgId, c.slug); + tallyImpact(impactOutcomes, impact); + // BUG 5: Only count passes that actually called the LLM. Cache + // hits, no_key, and skipped statuses do not consume LLM capacity + // and must not burn the per-run cap. + if ( + !impact.cached && + impact.status !== "no_key" && + impact.status !== "skipped_no_candidates" + ) + impactAnalysesRun += 1; + if (impact.status === "ok") { + if (impact.result) impactBySlug.set(c.slug, impact.result); + const parts: string[] = []; + if (impact.counts.system) + parts.push(`${impact.counts.system} AI system(s) affected`); + if (impact.counts.control) + parts.push(`${impact.counts.control} control(s) to review`); + if (impact.counts.policy) + parts.push(`${impact.counts.policy} policy(ies) may be outdated`); + if (impact.counts.vendor) + parts.push(`${impact.counts.vendor} vendor(s) impacted`); + if (impact.counts.assessment) + parts.push(`${impact.counts.assessment} assessment(s) to update`); + if (parts.length) impactSuffix = `\n\nImpact: ${parts.join(", ")}.`; + } + } catch (err) { + logger.error( + `[regulations-tracker] impact analysis failed for org ${orgId} / ${c.slug}: ${(err as Error).message}`, + ); + } + } // end cap-else + } else if (!orgHasKey && !c.unstructured) { + // Keyless org with a structured change → nudge to configure a key, + // since a key would have produced a real impact panel here. A + // key-having org that toggled impact OFF (impactEnabled === false) + // gets NEITHER panel NOR nudge — they chose. An unstructured change + // gets no nudge either: even with a key there'd be nothing to show. + impactSuffix = + "\n\nConfigure an LLM key to see which of your AI systems, controls and vendors this affects."; + } + } + const message = `${baseMessage}${impactSuffix}`; + for (const uid of userIds) { + await createNotificationQuery( + { + user_id: uid, + type: NotificationType.REGULATIONS_TRACKER, + title, + message, + entity_type: NotificationEntityType.REGULATION_COUNTRY, + entity_name: c.name, + action_url: `/regulations-tracker/${c.slug}`, + }, + orgId, + ); + } + } + orgsNotified++; + } + // Email: configured recipients only, no fallback. + const emails = await resolveEmailRecipients(orgId); + if (emails.length) { + // Email-only orgs (no in-app recipients) never entered the loop above, + // so impactBySlug may be empty even though a key is configured. Backfill + // impact for changed countries we haven't analyzed yet, honoring the + // same key/enabled gate and per-run LLM cap. + if (orgHasKey && impactEnabled) { + for (const c of countries) { + // Skip removed, already-analyzed, and unstructured changes — the + // last has no diff to judge, so we don't run the LLM or show a + // panel (mirrors the in-app gate above). + if (c.removed || c.unstructured || impactBySlug.has(c.slug)) continue; + if (impactAnalysesRun >= IMPACT_MAX_ANALYSES_PER_RUN) { + if (!impactCapLogged) { + logger.warn( + `[regulations-tracker] per-run impact cap (${IMPACT_MAX_ANALYSES_PER_RUN}) reached; skipping remaining LLM analyses for this sync`, + ); + impactCapLogged = true; + } + break; + } + impactRan = true; + try { + const impact = await runImpactAnalysis(orgId, c.slug); + tallyImpact(impactOutcomes, impact); + if ( + !impact.cached && + impact.status !== "no_key" && + impact.status !== "skipped_no_candidates" + ) + impactAnalysesRun += 1; + if (impact.status === "ok" && impact.result) + impactBySlug.set(c.slug, impact.result); + } catch (err) { + logger.error( + `[regulations-tracker] impact analysis failed for org ${orgId} / ${c.slug}: ${(err as Error).message}`, + ); + } + } + } + + // Build the per-country impact section for the email (verbose surface). + const impactItems: ImpactDigestItem[] = countries + .filter((c) => !c.removed && impactBySlug.has(c.slug)) + .map((c) => ({ countryName: c.name, result: impactBySlug.get(c.slug)! })); + + const html = await renderDigest(changedItems, removedItems, impactItems); + await sendAutomationEmail(emails, "Global AI regulations — update", html, undefined); + orgsEmailed++; + } + if (impactRan) { + try { + await setLastImpactRunAt(orgId); + } catch { + /* best-effort */ + } + } + } catch (orgErr) { + orgsFailed += 1; + logger.error( + `[regulations-tracker] dispatch failed for org ${orgId}; continuing with remaining orgs: ${(orgErr as Error).message}`, + ); + } + } + if (orgsFailed) { + // Surface partial failure on the run status. The catalogue is committed + // and the day key is set, so a retry would skip — but at least the + // Settings page and logs reflect that some orgs were not notified. + await recordRunStatus( + `partial: ${orgsFailed} org(s) failed to notify (${orgsNotified} notified, ${orgsEmailed} emailed)`, + ).catch(() => undefined); + } + } + + // New countries appeared in the feed. They aren't tracked by anyone yet, so + // notify each org's admins (in-app only) so they can decide whether to track. + if (newlyAdded.length) { + // Resolve display names from the validated feed (slug -> name). + const feedName = new Map( + validated.countries.map((c) => [c.slug.trim().toLowerCase(), c.name]), + ); + const addedNames = newlyAdded.map((s) => feedName.get(s) ?? s); + const admins = await getAllOrgAdmins(); + const byOrgAdmins = new Map(); + for (const a of admins) { + const arr = byOrgAdmins.get(a.organization_id) ?? []; + arr.push(a.user_id); + byOrgAdmins.set(a.organization_id, arr); + } + const title = + newlyAdded.length === 1 + ? `New jurisdiction added: ${addedNames[0]}` + : `${newlyAdded.length} new jurisdictions added`; + const message = + newlyAdded.length === 1 + ? `${addedNames[0]} was added to the regulations catalogue. Track it to monitor its AI regulations.` + : `Added: ${addedNames.join(", ")}. Track the ones relevant to your organization.`; + const actionUrl = + newlyAdded.length === 1 + ? `/regulations-tracker/${newlyAdded[0]}` + : "/regulations-tracker/browse"; + for (const [orgId, userIds] of byOrgAdmins) { + for (const uid of userIds) { + await createNotificationQuery( + { + user_id: uid, + type: NotificationType.REGULATIONS_TRACKER, + title, + message, + entity_type: NotificationEntityType.REGULATION_COUNTRY, + action_url: actionUrl, + }, + orgId, + ); + } + orgsNotifiedNew++; + } + } + } catch (e) { + // Notifications/emails failed after the catalogue was committed. Surface the + // failure on the run status and rethrow so BullMQ marks the job failed. + logger.error(`[regulations-tracker] notification dispatch failed: ${(e as Error).message}`); + await recordRunStatus(`error: notifications failed: ${(e as Error).message}`).catch( + () => undefined, + ); + throw e; + } + + logger.info( + `[regulations-tracker] done: fetched=${validated.countries.length} added=${newlyAdded.length} changed=${changed.length} removed=${newlyRemoved.length} emailed=${orgsEmailed} notified=${orgsNotified} newCountryOrgs=${orgsNotifiedNew}`, + ); + if (changed.length) { + logger.info( + `[regulations-tracker] value telemetry: feedDiff structured=${structuredChanges} unstructured=${unstructuredChanges} | impact ok=${impactOutcomes.ok} skippedNoCandidates=${impactOutcomes.skipped_no_candidates} error=${impactOutcomes.error} noKey=${impactOutcomes.no_key} cached=${impactOutcomes.cached}`, + ); + } + await recordRunStatus(`ok: ${changed.length} changed, ${newlyRemoved.length} removed`).catch( + () => undefined, + ); + return { + fetched: validated.countries.length, + changed: changed.length, + newlyAdded: newlyAdded.length, + newlyRemoved: newlyRemoved.length, + orgsEmailed, + orgsNotified, + }; +} diff --git a/Servers/services/automations/automationProducer.ts b/Servers/services/automations/automationProducer.ts index 91829ab40e..12e9b02242 100644 --- a/Servers/services/automations/automationProducer.ts +++ b/Servers/services/automations/automationProducer.ts @@ -16,7 +16,12 @@ export async function enqueueAutomationAction( } export async function scheduleVendorReviewDateNotification() { - await automationQueue.obliterate({ force: true }); + // NOTE: previously called automationQueue.obliterate({ force: true }) here, + // which wiped the ENTIRE shared queue (every repeatable schedule, not just + // this one). That made job survival depend on registration order and silently + // dropped sibling jobs (e.g. the regulations-tracker daily sync) whenever a + // scheduler that obliterates ran after them. queue.add with a repeat pattern + // is idempotent by repeat key, so no obliterate is needed. logger.info("Adding Vendor Review Date Notification jobs to the queue..."); // Vendor Review Date Notification Every day at 12 am await automationQueue.add( @@ -49,7 +54,9 @@ export async function schedulePolicyDueSoonNotification() { } export async function scheduleReportNotification() { - await automationQueue.obliterate({ force: true }); + // NOTE: obliterate({ force: true }) removed here too — see the comment in + // scheduleVendorReviewDateNotification. It wiped the whole queue and made + // job survival order-dependent. The repeatable add below is idempotent. logger.info("Adding Report Notification jobs to the queue..."); // Report Notification Every day at 12 am await automationQueue.add( @@ -224,3 +231,21 @@ export async function scheduleAiTrustIndexSync() { }, ); } + +export async function scheduleRegulationsTrackerSync() { + logger.info("Adding Regulations Tracker daily sync job to the queue..."); + // Daily 06:00 UTC so a regulation change is picked up (and tracked-country + // customers alerted) the day it lands rather than waiting for the next Monday. + // The handler self-guards via the day key in last_run_week (one fetch+diff per + // UTC day), and the hash-diff makes a no-change run cheap (1 manifest fetch, 0 + // detail fetches). Repeatable add is idempotent by repeat key. + await automationQueue.add( + "regulations_tracker_sync", + {}, + { + repeat: { pattern: "0 6 * * *", tz: "UTC" }, // every day 06:00 UTC + removeOnComplete: true, + removeOnFail: false, + }, + ); +} diff --git a/Servers/services/automations/automationWorker.ts b/Servers/services/automations/automationWorker.ts index 5e4505f02c..ad9ff1f3d4 100644 --- a/Servers/services/automations/automationWorker.ts +++ b/Servers/services/automations/automationWorker.ts @@ -23,6 +23,7 @@ import { import { runAgentDiscoverySync } from "../agentDiscovery/agentDiscoverySync.service"; import { processScheduledAiDetectionScans } from "../aiDetection/scheduledScanProcessor"; import { syncAiTrustIndex } from "./actions/syncAiTrustIndex"; +import { syncRegulationsTracker } from "./actions/syncRegulationsTracker"; // AI Gateway budget/risk jobs — call AIGateway HTTP endpoints via internal API const AI_GATEWAY_URL = process.env.AI_GATEWAY_URL || "http://127.0.0.1:8100"; const AI_GATEWAY_KEY = process.env.AI_GATEWAY_INTERNAL_KEY || ""; @@ -520,6 +521,8 @@ export const createAutomationWorker = () => { } } else if (name === "ai_trust_index_sync") { await syncAiTrustIndex(); + } else if (name === "regulations_tracker_sync") { + await syncRegulationsTracker(); } else if (name === "mcp_audit_cleanup") { try { const [auditResult, approvalResult] = await Promise.all([ diff --git a/Servers/swagger.yaml b/Servers/swagger.yaml index 937d6c6815..5b24a12393 100644 --- a/Servers/swagger.yaml +++ b/Servers/swagger.yaml @@ -1,11 +1,12 @@ openapi: 3.0.0 info: - title: "VerifyWise API" - description: "AI Governance Platform API" + title: 'VerifyWise API' + description: 'AI Governance Platform API' version: 2.0.0 servers: - - url: /api - description: "Main API server" + - + url: /api + description: 'Main API server' components: securitySchemes: bearerAuth: @@ -17,7 +18,7 @@ components: name: id in: path required: true - description: "The numeric project ID" + description: 'The numeric project ID' schema: type: integer example: 1 @@ -25,7 +26,7 @@ components: name: id in: path required: true - description: "Numeric ID of the policy" + description: 'Numeric ID of the policy' schema: type: integer example: 42 @@ -39,10 +40,10 @@ components: type: string enum: - Prohibited - - "High risk" - - "Limited risk" - - "Minimal risk" - description: "EU AI Act risk classification level" + - 'High risk' + - 'Limited risk' + - 'Minimal risk' + description: 'EU AI Act risk classification level' HighRiskRole: type: string enum: @@ -50,20 +51,20 @@ components: - Provider - Distributor - Importer - - "Product manufacturer" - - "Authorized representative" - description: "Role of the organization under the EU AI Act for high-risk systems" + - 'Product manufacturer' + - 'Authorized representative' + description: 'Role of the organization under the EU AI Act for high-risk systems' ProjectStatus: type: string enum: - - "Not started" - - "In progress" - - "Under review" + - 'Not started' + - 'In progress' + - 'Under review' - Completed - Closed - - "On hold" + - 'On hold' - Rejected - description: "Current lifecycle status of the project" + description: 'Current lifecycle status of the project' CreateProjectRequest: type: object required: @@ -74,36 +75,36 @@ components: properties: project_title: type: string - example: "Customer Support Chatbot" + example: 'Customer Support Chatbot' owner: type: integer example: 1 start_date: type: string format: date-time - example: "2026-01-15T00:00:00.000Z" + example: '2026-01-15T00:00:00.000Z' geography: type: integer default: 1 example: 1 ai_risk_classification: - $ref: "#/components/schemas/AiRiskClassification" + $ref: '#/components/schemas/AiRiskClassification' type_of_high_risk_role: - $ref: "#/components/schemas/HighRiskRole" + $ref: '#/components/schemas/HighRiskRole' goal: type: string nullable: true - example: "Automate tier-1 support queries" + example: 'Automate tier-1 support queries' target_industry: type: string nullable: true - example: "Financial Services" + example: 'Financial Services' description: type: string nullable: true - example: "An LLM-powered chatbot for handling customer inquiries" + example: 'An LLM-powered chatbot for handling customer inquiries' status: - $ref: "#/components/schemas/ProjectStatus" + $ref: '#/components/schemas/ProjectStatus' is_organizational: type: boolean default: false @@ -142,9 +143,9 @@ components: geography: type: integer ai_risk_classification: - $ref: "#/components/schemas/AiRiskClassification" + $ref: '#/components/schemas/AiRiskClassification' type_of_high_risk_role: - $ref: "#/components/schemas/HighRiskRole" + $ref: '#/components/schemas/HighRiskRole' goal: type: string nullable: true @@ -155,7 +156,7 @@ components: type: string nullable: true status: - $ref: "#/components/schemas/ProjectStatus" + $ref: '#/components/schemas/ProjectStatus' last_updated: type: string format: date-time @@ -178,7 +179,7 @@ components: type: integer name: type: string - example: "EU AI Act" + example: 'EU AI Act' Project: type: object properties: @@ -190,7 +191,7 @@ components: example: UC-7 project_title: type: string - example: "Customer Support Chatbot" + example: 'Customer Support Chatbot' owner: type: integer example: 1 @@ -201,9 +202,9 @@ components: type: integer example: 1 ai_risk_classification: - $ref: "#/components/schemas/AiRiskClassification" + $ref: '#/components/schemas/AiRiskClassification' type_of_high_risk_role: - $ref: "#/components/schemas/HighRiskRole" + $ref: '#/components/schemas/HighRiskRole' goal: type: string nullable: true @@ -214,7 +215,7 @@ components: type: string nullable: true status: - $ref: "#/components/schemas/ProjectStatus" + $ref: '#/components/schemas/ProjectStatus' last_updated: type: string format: date-time @@ -240,15 +241,17 @@ components: framework: type: array items: - $ref: "#/components/schemas/ProjectFramework" + $ref: '#/components/schemas/ProjectFramework' members: type: array items: type: integer ProjectListItem: allOf: - - $ref: "#/components/schemas/Project" - - type: object + - + $ref: '#/components/schemas/Project' + - + type: object properties: has_pending_approval: type: boolean @@ -265,12 +268,14 @@ components: example: jira-assets ProjectDetail: allOf: - - $ref: "#/components/schemas/Project" - - type: object + - + $ref: '#/components/schemas/Project' + - + type: object properties: owner_name: type: string - example: "John Doe" + example: 'John Doe' has_pending_approval: type: boolean approval_status: @@ -282,8 +287,10 @@ components: - null ProjectWithMembers: allOf: - - $ref: "#/components/schemas/Project" - - type: object + - + $ref: '#/components/schemas/Project' + - + type: object properties: members: type: array @@ -325,7 +332,7 @@ components: example: High count: type: string - example: "3" + example: '3' VendorRiskCount: type: object properties: @@ -334,33 +341,41 @@ components: example: Critical count: type: string - example: "2" + example: '2' ComplianceProgress: type: object properties: allsubControls: oneOf: - - type: string - - type: integer - example: "45" + - + type: string + - + type: integer + example: '45' allDonesubControls: oneOf: - - type: string - - type: integer - example: "12" + - + type: string + - + type: integer + example: '12' AssessmentProgress: type: object properties: totalQuestions: oneOf: - - type: string - - type: integer - example: "80" + - + type: string + - + type: integer + example: '80' answeredQuestions: oneOf: - - type: string - - type: integer - example: "35" + - + type: string + - + type: integer + example: '35' ControlCategory: type: object properties: @@ -395,21 +410,13 @@ components: type: array items: type: object - properties: - { - id: { type: integer }, - control_id: { type: integer }, - title: { type: string }, - description: { type: string }, - status: { type: string, enum: [Draft, "In progress", Done] }, - organization_id: { type: integer }, - } + properties: {id: {type: integer}, control_id: {type: integer}, title: {type: string}, description: {type: string}, status: {type: string, enum: [Draft, 'In progress', Done]}, organization_id: {type: integer}} ErrorResponse: type: object properties: message: type: string - example: "Not Found" + example: 'Not Found' data: example: {} ServerError: @@ -417,13 +424,13 @@ components: properties: message: type: string - example: "Internal Server Error" + example: 'Internal Server Error' error: type: string - example: "Unexpected error occurred" + example: 'Unexpected error occurred' UserSafe: type: object - description: "User object with password_hash excluded" + description: 'User object with password_hash excluded' properties: id: type: integer @@ -440,7 +447,7 @@ components: example: john@example.com role_id: type: integer - description: "1=Admin, 2=Reviewer, 3=Editor, 4=Auditor, 5=SuperAdmin" + description: '1=Admin, 2=Reviewer, 3=Editor, 4=Auditor, 5=SuperAdmin' example: 1 created_at: type: string @@ -475,16 +482,16 @@ components: type: string description: 'HTTP status text (e.g. "OK", "Created", "Accepted")' data: - description: "Response payload (varies by endpoint)" + description: 'Response payload (varies by endpoint)' ErrorEnvelope: type: object properties: message: type: string - description: "HTTP status text or error category" + description: 'HTTP status text or error category' data: type: string - description: "Error detail message" + description: 'Error detail message' ProgressResponse: type: object properties: @@ -523,117 +530,117 @@ components: properties: id: type: integer - description: "Auto-generated primary key" + description: 'Auto-generated primary key' example: 42 order_no: type: integer nullable: true - description: "Display order number" + description: 'Display order number' vendor_name: type: string - description: "Name of the vendor" - example: "Acme Corp" + description: 'Name of the vendor' + example: 'Acme Corp' vendor_provides: type: string - description: "What the vendor provides" - example: "Cloud hosting services" + description: 'What the vendor provides' + example: 'Cloud hosting services' assignee: type: integer - description: "User ID of the assigned owner" + description: 'User ID of the assigned owner' example: 5 website: type: string - description: "Vendor website URL" - example: "https://acme.example.com" + description: 'Vendor website URL' + example: 'https://acme.example.com' vendor_contact_person: type: string - description: "Name of the vendor contact" - example: "Jane Doe" + description: 'Name of the vendor contact' + example: 'Jane Doe' review_result: type: string nullable: true - description: "Free-text review result summary" + description: 'Free-text review result summary' review_status: type: string nullable: true enum: - - "Not started" - - "In review" + - 'Not started' + - 'In review' - Reviewed - - "Requires follow-up" - default: "Not started" - description: "Current review lifecycle status" + - 'Requires follow-up' + default: 'Not started' + description: 'Current review lifecycle status' reviewer: type: integer nullable: true - description: "User ID of the reviewer" + description: 'User ID of the reviewer' review_date: type: string format: date-time nullable: true - description: "Date the review was performed (ISO 8601)" + description: 'Date the review was performed (ISO 8601)' is_demo: type: boolean default: false - description: "Whether this is a demo vendor (read-only after creation)" + description: 'Whether this is a demo vendor (read-only after creation)' projects: type: array items: type: integer - description: "Array of associated project IDs" + description: 'Array of associated project IDs' data_sensitivity: type: string nullable: true enum: - None - - "Internal only" - - "Personally identifiable information (PII)" - - "Financial data" - - "Health data (e.g. HIPAA)" - - "Model weights or AI assets" - - "Other sensitive data" - description: "Scorecard - type of data the vendor accesses" + - 'Internal only' + - 'Personally identifiable information (PII)' + - 'Financial data' + - 'Health data (e.g. HIPAA)' + - 'Model weights or AI assets' + - 'Other sensitive data' + description: 'Scorecard - type of data the vendor accesses' business_criticality: type: string nullable: true enum: - - "Low (vendor supports non-core functions)" - - "Medium (affects operations but is replaceable)" - - "High (critical to core services or products)" - description: "Scorecard - how critical the vendor is to operations" + - 'Low (vendor supports non-core functions)' + - 'Medium (affects operations but is replaceable)' + - 'High (critical to core services or products)' + description: 'Scorecard - how critical the vendor is to operations' past_issues: type: string nullable: true enum: - None - - "Minor incident (e.g. small delay, minor bug)" - - "Major incident (e.g. data breach, legal issue)" - description: "Scorecard - history of past incidents" + - 'Minor incident (e.g. small delay, minor bug)' + - 'Major incident (e.g. data breach, legal issue)' + description: 'Scorecard - history of past incidents' regulatory_exposure: type: string nullable: true enum: - None - - "GDPR (EU)" - - "HIPAA (US)" - - "SOC 2" - - "ISO 27001" - - "EU AI act" - - "CCPA (california)" + - 'GDPR (EU)' + - 'HIPAA (US)' + - 'SOC 2' + - 'ISO 27001' + - 'EU AI act' + - 'CCPA (california)' - Other - description: "Scorecard - applicable regulatory framework" + description: 'Scorecard - applicable regulatory framework' risk_score: type: integer nullable: true - description: "Computed risk score for the vendor" + description: 'Computed risk score for the vendor' created_at: type: string format: date-time - description: "Creation timestamp (ISO 8601)" + description: 'Creation timestamp (ISO 8601)' updated_at: type: string format: date-time - description: "Last update timestamp (ISO 8601)" + description: 'Last update timestamp (ISO 8601)' required: - vendor_name - vendor_provides @@ -642,13 +649,15 @@ components: - vendor_contact_person VendorWithReviewerName: allOf: - - $ref: "#/components/schemas/Vendor" - - type: object + - + $ref: '#/components/schemas/Vendor' + - + type: object properties: reviewer_name: type: string - description: "Full name of the reviewer (joined from users table)" - example: "John Smith" + description: 'Full name of the reviewer (joined from users table)' + example: 'John Smith' VendorInput: type: object required: @@ -660,90 +669,90 @@ components: properties: vendor_name: type: string - description: "Name of the vendor (required, non-empty)" + description: 'Name of the vendor (required, non-empty)' vendor_provides: type: string - description: "What the vendor provides (required, non-empty)" + description: 'What the vendor provides (required, non-empty)' assignee: type: integer minimum: 1 - description: "User ID of the assigned owner (required, >= 1)" + description: 'User ID of the assigned owner (required, >= 1)' website: type: string - description: "Vendor website URL (required, non-empty)" + description: 'Vendor website URL (required, non-empty)' vendor_contact_person: type: string - description: "Name of the vendor contact (required, non-empty)" + description: 'Name of the vendor contact (required, non-empty)' review_result: type: string - description: "Free-text review result summary" + description: 'Free-text review result summary' review_status: type: string enum: - - "Not started" - - "In review" + - 'Not started' + - 'In review' - Reviewed - - "Requires follow-up" - description: "Current review lifecycle status" + - 'Requires follow-up' + description: 'Current review lifecycle status' reviewer: type: integer minimum: 1 - description: "User ID of the reviewer" + description: 'User ID of the reviewer' review_date: type: string format: date-time - description: "Date of the review (ISO 8601)" + description: 'Date of the review (ISO 8601)' order_no: type: integer - description: "Display order number" + description: 'Display order number' is_demo: type: boolean default: false - description: "Mark as demo vendor" + description: 'Mark as demo vendor' projects: type: array items: type: integer - description: "Array of project IDs to associate" + description: 'Array of project IDs to associate' data_sensitivity: type: string enum: - None - - "Internal only" - - "Personally identifiable information (PII)" - - "Financial data" - - "Health data (e.g. HIPAA)" - - "Model weights or AI assets" - - "Other sensitive data" + - 'Internal only' + - 'Personally identifiable information (PII)' + - 'Financial data' + - 'Health data (e.g. HIPAA)' + - 'Model weights or AI assets' + - 'Other sensitive data' business_criticality: type: string enum: - - "Low (vendor supports non-core functions)" - - "Medium (affects operations but is replaceable)" - - "High (critical to core services or products)" + - 'Low (vendor supports non-core functions)' + - 'Medium (affects operations but is replaceable)' + - 'High (critical to core services or products)' past_issues: type: string enum: - None - - "Minor incident (e.g. small delay, minor bug)" - - "Major incident (e.g. data breach, legal issue)" + - 'Minor incident (e.g. small delay, minor bug)' + - 'Major incident (e.g. data breach, legal issue)' regulatory_exposure: type: string enum: - None - - "GDPR (EU)" - - "HIPAA (US)" - - "SOC 2" - - "ISO 27001" - - "EU AI act" - - "CCPA (california)" + - 'GDPR (EU)' + - 'HIPAA (US)' + - 'SOC 2' + - 'ISO 27001' + - 'EU AI act' + - 'CCPA (california)' - Other risk_score: type: integer - description: "Computed risk score" + description: 'Computed risk score' VendorUpdate: type: object - description: "All fields are optional. Only provided fields are updated. Review and scorecard fields can be set to null to clear them." + description: 'All fields are optional. Only provided fields are updated. Review and scorecard fields can be set to null to clear them.' properties: vendor_name: type: string @@ -763,10 +772,10 @@ components: type: string nullable: true enum: - - "Not started" - - "In review" + - 'Not started' + - 'In review' - Reviewed - - "Requires follow-up" + - 'Requires follow-up' reviewer: type: integer nullable: true @@ -785,37 +794,37 @@ components: nullable: true enum: - None - - "Internal only" - - "Personally identifiable information (PII)" - - "Financial data" - - "Health data (e.g. HIPAA)" - - "Model weights or AI assets" - - "Other sensitive data" + - 'Internal only' + - 'Personally identifiable information (PII)' + - 'Financial data' + - 'Health data (e.g. HIPAA)' + - 'Model weights or AI assets' + - 'Other sensitive data' business_criticality: type: string nullable: true enum: - - "Low (vendor supports non-core functions)" - - "Medium (affects operations but is replaceable)" - - "High (critical to core services or products)" + - 'Low (vendor supports non-core functions)' + - 'Medium (affects operations but is replaceable)' + - 'High (critical to core services or products)' past_issues: type: string nullable: true enum: - None - - "Minor incident (e.g. small delay, minor bug)" - - "Major incident (e.g. data breach, legal issue)" + - 'Minor incident (e.g. small delay, minor bug)' + - 'Major incident (e.g. data breach, legal issue)' regulatory_exposure: type: string nullable: true enum: - None - - "GDPR (EU)" - - "HIPAA (US)" - - "SOC 2" - - "ISO 27001" - - "EU AI act" - - "CCPA (california)" + - 'GDPR (EU)' + - 'HIPAA (US)' + - 'SOC 2' + - 'ISO 27001' + - 'EU AI act' + - 'CCPA (california)' - Other risk_score: type: integer @@ -828,30 +837,30 @@ components: - Pending - Blocked - Rejected - description: "Current approval/review status of the model" + description: 'Current approval/review status of the model' Filedata: type: object - description: "Metadata for an uploaded security-assessment file" + description: 'Metadata for an uploaded security-assessment file' properties: id: type: integer - description: "File record ID" + description: 'File record ID' filename: type: string - description: "Original file name" + description: 'Original file name' size: type: integer - description: "File size in bytes" + description: 'File size in bytes' mimetype: type: string - description: "MIME type (e.g. application/pdf)" + description: 'MIME type (e.g. application/pdf)' upload_date: type: string format: date-time - description: "ISO 8601 upload timestamp" + description: 'ISO 8601 upload timestamp' uploaded_by: type: integer - description: "User ID who uploaded the file" + description: 'User ID who uploaded the file' required: - id - filename @@ -861,33 +870,33 @@ components: - uploaded_by ModelInventoryResponse: type: object - description: "Model inventory record as returned by the API" + description: 'Model inventory record as returned by the API' properties: id: type: integer - description: "Auto-generated primary key" + description: 'Auto-generated primary key' example: 42 provider_model: type: string nullable: true - description: "Legacy combined provider+model field (backward compatibility)" - example: "OpenAI / GPT-4" + description: 'Legacy combined provider+model field (backward compatibility)' + example: 'OpenAI / GPT-4' provider: type: string - description: "Model provider name" + description: 'Model provider name' example: OpenAI model: type: string - description: "Model name" + description: 'Model name' example: GPT-4 version: type: string - description: "Model version identifier" + description: 'Model version identifier' example: turbo-2024-04-09 approver: type: integer nullable: true - description: "User ID of the assigned approver" + description: 'User ID of the assigned approver' example: 7 capabilities: type: array @@ -895,61 +904,61 @@ components: type: string description: "List of capability strings. Stored as comma-separated text in the DB, returned as an array.\n" example: - - "Text Generation" - - "Code Generation" + - 'Text Generation' + - 'Code Generation' - Reasoning security_assessment: type: boolean - description: "Whether a security assessment has been completed" + description: 'Whether a security assessment has been completed' example: false status: - $ref: "#/components/schemas/ModelInventoryStatus" + $ref: '#/components/schemas/ModelInventoryStatus' status_date: type: string format: date-time - description: "ISO 8601 timestamp of the last status change" - example: "2026-04-15T10:30:00.000Z" + description: 'ISO 8601 timestamp of the last status change' + example: '2026-04-15T10:30:00.000Z' reference_link: type: string nullable: true - description: "URL to external model documentation or model card" - example: "https://platform.openai.com/docs/models/gpt-4" + description: 'URL to external model documentation or model card' + example: 'https://platform.openai.com/docs/models/gpt-4' biases: type: string - description: "Known biases of the model" - example: "May reflect biases present in training data" + description: 'Known biases of the model' + example: 'May reflect biases present in training data' limitations: type: string - description: "Known limitations of the model" - example: "Knowledge cutoff, potential hallucinations" + description: 'Known limitations of the model' + example: 'Knowledge cutoff, potential hallucinations' hosting_provider: type: string - description: "Where the model is hosted" - example: "Azure OpenAI" + description: 'Where the model is hosted' + example: 'Azure OpenAI' security_assessment_data: type: array items: - $ref: "#/components/schemas/Filedata" - description: "Uploaded security assessment file metadata" + $ref: '#/components/schemas/Filedata' + description: 'Uploaded security assessment file metadata' is_demo: type: boolean - description: "Whether this is a demo/sample record" + description: 'Whether this is a demo/sample record' example: false created_at: type: string format: date-time - description: "ISO 8601 creation timestamp" - example: "2026-04-10T08:00:00.000Z" + description: 'ISO 8601 creation timestamp' + example: '2026-04-10T08:00:00.000Z' updated_at: type: string format: date-time - description: "ISO 8601 last-updated timestamp" - example: "2026-04-15T10:30:00.000Z" + description: 'ISO 8601 last-updated timestamp' + example: '2026-04-15T10:30:00.000Z' projects: type: array items: type: integer - description: "IDs of projects this model is associated with" + description: 'IDs of projects this model is associated with' example: - 1 - 3 @@ -957,12 +966,12 @@ components: type: array items: type: integer - description: "IDs of frameworks this model is associated with" + description: 'IDs of frameworks this model is associated with' example: - 5 ModelInventoryCreateRequest: type: object - description: "Request body for creating a new model inventory record" + description: 'Request body for creating a new model inventory record' required: - provider - model @@ -977,79 +986,81 @@ components: properties: provider_model: type: string - description: "Legacy combined provider+model field (optional, backward compatibility)" - example: "OpenAI / GPT-4" + description: 'Legacy combined provider+model field (optional, backward compatibility)' + example: 'OpenAI / GPT-4' provider: type: string - description: "Model provider name" + description: 'Model provider name' example: OpenAI model: type: string - description: "Model name" + description: 'Model name' example: GPT-4 version: type: string - description: "Model version identifier" + description: 'Model version identifier' example: turbo-2024-04-09 approver: type: integer nullable: true - description: "User ID of the assigned approver" + description: 'User ID of the assigned approver' example: 7 capabilities: oneOf: - - type: string - description: "Comma-separated capabilities" - - type: array + - + type: string + description: 'Comma-separated capabilities' + - + type: array items: type: string - description: "Array of capability strings" + description: 'Array of capability strings' description: "Accepted as either a comma-separated string or an array of strings. Arrays are joined with \", \" before storage.\n" example: - - "Text Generation" - - "Code Generation" + - 'Text Generation' + - 'Code Generation' security_assessment: type: boolean - description: "Whether a security assessment has been completed" + description: 'Whether a security assessment has been completed' default: false status: - $ref: "#/components/schemas/ModelInventoryStatus" + $ref: '#/components/schemas/ModelInventoryStatus' status_date: type: string format: date-time - description: "ISO 8601 timestamp for the status" - example: "2026-04-15T10:30:00.000Z" + description: 'ISO 8601 timestamp for the status' + example: '2026-04-15T10:30:00.000Z' reference_link: type: string - description: "URL to external model documentation or model card" - example: "https://platform.openai.com/docs/models/gpt-4" + description: 'URL to external model documentation or model card' + example: 'https://platform.openai.com/docs/models/gpt-4' biases: type: string - description: "Known biases of the model" - example: "May reflect biases present in training data" + description: 'Known biases of the model' + example: 'May reflect biases present in training data' limitations: type: string - description: "Known limitations of the model" - example: "Knowledge cutoff, potential hallucinations" + description: 'Known limitations of the model' + example: 'Knowledge cutoff, potential hallucinations' hosting_provider: type: string - description: "Where the model is hosted" - example: "Azure OpenAI" + description: 'Where the model is hosted' + example: 'Azure OpenAI' security_assessment_data: type: array items: - $ref: "#/components/schemas/Filedata" - description: "Uploaded security assessment file metadata" + $ref: '#/components/schemas/Filedata' + description: 'Uploaded security assessment file metadata' default: [] is_demo: type: boolean - description: "Whether this is a demo/sample record" + description: 'Whether this is a demo/sample record' default: false projects: type: array items: type: integer - description: "Project IDs to associate with this model" + description: 'Project IDs to associate with this model' default: [] example: - 1 @@ -1058,7 +1069,7 @@ components: type: array items: type: integer - description: "Framework IDs to associate with this model" + description: 'Framework IDs to associate with this model' default: [] example: - 5 @@ -1068,56 +1079,58 @@ components: properties: provider_model: type: string - description: "Legacy combined provider+model field" + description: 'Legacy combined provider+model field' provider: type: string - description: "Model provider name" + description: 'Model provider name' model: type: string - description: "Model name" + description: 'Model name' version: type: string - description: "Model version identifier" + description: 'Model version identifier' approver: type: integer nullable: true - description: "User ID of the assigned approver" + description: 'User ID of the assigned approver' capabilities: oneOf: - - type: string - - type: array + - + type: string + - + type: array items: type: string - description: "Capabilities (string or array)" + description: 'Capabilities (string or array)' security_assessment: type: boolean - description: "Whether a security assessment has been completed" + description: 'Whether a security assessment has been completed' status: - $ref: "#/components/schemas/ModelInventoryStatus" + $ref: '#/components/schemas/ModelInventoryStatus' status_date: type: string format: date-time - description: "ISO 8601 timestamp for the status" + description: 'ISO 8601 timestamp for the status' reference_link: type: string - description: "URL to external model documentation" + description: 'URL to external model documentation' biases: type: string - description: "Known biases of the model" + description: 'Known biases of the model' limitations: type: string - description: "Known limitations of the model" + description: 'Known limitations of the model' hosting_provider: type: string - description: "Where the model is hosted" + description: 'Where the model is hosted' security_assessment_data: type: array items: - $ref: "#/components/schemas/Filedata" - description: "Uploaded security assessment file metadata" + $ref: '#/components/schemas/Filedata' + description: 'Uploaded security assessment file metadata' is_demo: type: boolean - description: "Whether this is a demo/sample record" + description: 'Whether this is a demo/sample record' projects: type: array items: @@ -1138,70 +1151,70 @@ components: default: false PolicyWithReviewers: type: object - description: "A policy record with computed assigned reviewer IDs" + description: 'A policy record with computed assigned reviewer IDs' properties: id: type: integer - description: "Auto-generated primary key" + description: 'Auto-generated primary key' example: 42 organization_id: type: integer - description: "Tenant organization ID" + description: 'Tenant organization ID' example: 1 title: type: string - description: "Policy title" - example: "AI Ethics Policy" + description: 'Policy title' + example: 'AI Ethics Policy' content_html: type: string - description: "Full policy content as HTML" - example: "

AI Ethics Policy

...

" + description: 'Full policy content as HTML' + example: '

AI Ethics Policy

...

' status: type: string - description: "Policy lifecycle status" + description: 'Policy lifecycle status' example: draft tags: type: array items: type: string enum: - - "AI ethics" + - 'AI ethics' - Fairness - Transparency - Explainability - - "Bias mitigation" + - 'Bias mitigation' - Privacy - - "Data governance" - - "Model risk" + - 'Data governance' + - 'Model risk' - Accountability - Security - LLM - - "Human oversight" - - "EU AI Act" - - "ISO 42001" - - "NIST RMF" - - "Red teaming" + - 'Human oversight' + - 'EU AI Act' + - 'ISO 42001' + - 'NIST RMF' + - 'Red teaming' - Audit - Monitoring - - "Vendor management" - description: "Categorization tags" + - 'Vendor management' + description: 'Categorization tags' next_review_date: type: string format: date-time nullable: true - description: "Scheduled next review date" + description: 'Scheduled next review date' author_id: type: integer - description: "User ID of the policy creator" + description: 'User ID of the policy creator' example: 1 last_updated_by: type: integer - description: "User ID of the last editor" + description: 'User ID of the last editor' example: 1 last_updated_at: type: string format: date-time - description: "Timestamp of last update" + description: 'Timestamp of last update' review_status: type: string nullable: true @@ -1210,33 +1223,33 @@ components: - approved - changes_requested - null - description: "Current review workflow status" + description: 'Current review workflow status' review_comment: type: string nullable: true - description: "Most recent review comment" + description: 'Most recent review comment' reviewed_by: type: integer nullable: true - description: "User ID of the last reviewer" + description: 'User ID of the last reviewer' reviewed_at: type: string format: date-time nullable: true - description: "Timestamp of last review action" + description: 'Timestamp of last review action' is_demo: type: boolean - description: "Whether this is a demo/seed policy" + description: 'Whether this is a demo/seed policy' default: false created_at: type: string format: date-time - description: "Row creation timestamp (set by database)" + description: 'Row creation timestamp (set by database)' assigned_reviewer_ids: type: array items: type: integer - description: "User IDs of assigned reviewers (aggregated from mapping table)" + description: 'User IDs of assigned reviewers (aggregated from mapping table)' example: - 2 - 5 @@ -1249,70 +1262,70 @@ components: properties: title: type: string - description: "Policy title" - example: "Data Governance Policy" + description: 'Policy title' + example: 'Data Governance Policy' content_html: type: string - description: "Full policy content as HTML" - example: "

Data Governance

This policy outlines...

" + description: 'Full policy content as HTML' + example: '

Data Governance

This policy outlines...

' status: type: string - description: "Initial policy status" + description: 'Initial policy status' example: draft tags: type: array items: type: string - description: "Categorization tags (must be from the allowed tag list)" + description: 'Categorization tags (must be from the allowed tag list)' example: - - "Data governance" + - 'Data governance' - Privacy next_review_date: type: string format: date-time nullable: true - description: "Scheduled next review date" - example: "2026-07-01T00:00:00.000Z" + description: 'Scheduled next review date' + example: '2026-07-01T00:00:00.000Z' assigned_reviewer_ids: type: array items: type: integer - description: "User IDs to assign as reviewers" + description: 'User IDs to assign as reviewers' example: - 2 - 5 is_demo: type: boolean - description: "Mark as a demo policy" + description: 'Mark as a demo policy' default: false PolicyUpdateRequest: type: object - description: "All fields are optional. Only provided fields are updated." + description: 'All fields are optional. Only provided fields are updated.' properties: title: type: string - description: "Updated policy title" + description: 'Updated policy title' content_html: type: string - description: "Updated policy content as HTML" + description: 'Updated policy content as HTML' status: type: string - description: "Updated policy status" + description: 'Updated policy status' tags: type: array items: type: string - description: "Replacement tag list (must be from the allowed tag list)" + description: 'Replacement tag list (must be from the allowed tag list)' next_review_date: type: string format: date-time nullable: true - description: "Updated next review date" + description: 'Updated next review date' assigned_reviewer_ids: type: array items: type: integer - description: "Replacement reviewer list (fully replaces existing reviewers)" + description: 'Replacement reviewer list (fully replaces existing reviewers)' ProjectRiskInput: type: object required: @@ -1322,28 +1335,28 @@ components: properties: risk_name: type: string - description: "Name/title of the risk." + description: 'Name/title of the risk.' risk_owner: type: integer - description: "User ID of the risk owner (must be >= 1)." + description: 'User ID of the risk owner (must be >= 1).' ai_lifecycle_phase: type: string enum: - - "Problem definition & planning" - - "Data collection & processing" - - "Model development & training" - - "Model validation & testing" - - "Deployment & integration" - - "Monitoring & maintenance" - - "Decommissioning & retirement" + - 'Problem definition & planning' + - 'Data collection & processing' + - 'Model development & training' + - 'Model validation & testing' + - 'Deployment & integration' + - 'Monitoring & maintenance' + - 'Decommissioning & retirement' risk_description: type: string - description: "Detailed description of the risk." + description: 'Detailed description of the risk.' risk_category: type: array items: type: string - description: "Array of category labels." + description: 'Array of category labels.' impact: type: string assessment_mapping: @@ -1357,7 +1370,7 @@ components: - Unlikely - Possible - Likely - - "Almost Certain" + - 'Almost Certain' severity: type: string enum: @@ -1369,32 +1382,32 @@ components: risk_level_autocalculated: type: string enum: - - "No risk" - - "Very low risk" - - "Low risk" - - "Medium risk" - - "High risk" - - "Very high risk" + - 'No risk' + - 'Very low risk' + - 'Low risk' + - 'Medium risk' + - 'High risk' + - 'Very high risk' review_notes: type: string mitigation_status: type: string enum: - - "Not Started" - - "In Progress" + - 'Not Started' + - 'In Progress' - Completed - - "On Hold" + - 'On Hold' - Deferred - Canceled - - "Requires review" + - 'Requires review' current_risk_level: type: string enum: - - "Very Low risk" - - "Low risk" - - "Medium risk" - - "High risk" - - "Very high risk" + - 'Very Low risk' + - 'Low risk' + - 'Medium risk' + - 'High risk' + - 'Very high risk' deadline: type: string format: date-time @@ -1411,7 +1424,7 @@ components: - Unlikely - Possible - Likely - - "Almost Certain" + - 'Almost Certain' risk_severity: type: string enum: @@ -1424,7 +1437,7 @@ components: type: string risk_approval: type: integer - description: "User ID of the approver." + description: 'User ID of the approver.' approval_status: type: string date_of_assessment: @@ -1437,12 +1450,12 @@ components: type: array items: type: integer - description: "Array of project IDs to link this risk to." + description: 'Array of project IDs to link this risk to.' frameworks: type: array items: type: integer - description: "Array of framework IDs to link this risk to." + description: 'Array of framework IDs to link this risk to.' event_frequency_min: type: number nullable: true @@ -1491,7 +1504,7 @@ components: control_effectiveness: type: number nullable: true - description: "Percentage 0-100." + description: 'Percentage 0-100.' mitigation_cost_annual: type: number nullable: true @@ -1505,8 +1518,10 @@ components: maxLength: 3 ProjectRiskResponse: allOf: - - $ref: "#/components/schemas/ProjectRiskInput" - - type: object + - + $ref: '#/components/schemas/ProjectRiskInput' + - + type: object properties: id: type: integer @@ -1574,11 +1589,11 @@ components: properties: vendor_id: type: integer - description: "ID of the vendor this risk belongs to." + description: 'ID of the vendor this risk belongs to.' order_no: type: integer nullable: true - description: "Optional ordering number." + description: 'Optional ordering number.' risk_description: type: string impact_description: @@ -1590,7 +1605,7 @@ components: - Unlikely - Possible - Likely - - "Almost certain" + - 'Almost certain' risk_severity: type: string enum: @@ -1603,10 +1618,10 @@ components: type: string action_owner: type: integer - description: "User ID of the action owner." + description: 'User ID of the action owner.' risk_level: type: string - description: "Free-text risk level." + description: 'Free-text risk level.' is_demo: type: boolean default: false @@ -1634,7 +1649,7 @@ components: - Unlikely - Possible - Likely - - "Almost certain" + - 'Almost certain' risk_severity: type: string enum: @@ -1658,7 +1673,7 @@ components: type: string format: date-time VendorRiskAllProjectsResponse: - description: "Extended vendor risk with joined vendor/project info." + description: 'Extended vendor risk with joined vendor/project info.' type: object properties: risk_id: @@ -1719,9 +1734,9 @@ components: type: string enum: - Performance - - "Bias & Fairness" + - 'Bias & Fairness' - Security - - "Data Quality" + - 'Data Quality' - Compliance risk_level: type: string @@ -1734,17 +1749,17 @@ components: type: string enum: - Open - - "In Progress" + - 'In Progress' - Resolved - Accepted default: Open owner: type: string - description: "Name or identifier of the risk owner." + description: 'Name or identifier of the risk owner.' target_date: type: string format: date - description: "Next review / target date." + description: 'Next review / target date.' description: type: string mitigation_plan: @@ -1762,7 +1777,7 @@ components: model_id: type: integer nullable: true - description: "ID of the model inventory entry this risk is linked to." + description: 'ID of the model inventory entry this risk is linked to.' is_demo: type: boolean default: false @@ -1777,9 +1792,9 @@ components: type: string enum: - Performance - - "Bias & Fairness" + - 'Bias & Fairness' - Security - - "Data Quality" + - 'Data Quality' - Compliance risk_level: type: string @@ -1792,7 +1807,7 @@ components: type: string enum: - Open - - "In Progress" + - 'In Progress' - Resolved - Accepted owner: @@ -1830,7 +1845,7 @@ components: type: string example: success data: - description: "Response payload (type varies per endpoint)" + description: 'Response payload (type varies per endpoint)' ValidationErrorResponse: type: object properties: @@ -1839,7 +1854,7 @@ components: example: error message: type: string - example: "Dataset creation validation failed" + example: 'Dataset creation validation failed' errors: type: array items: @@ -1866,7 +1881,7 @@ components: evidence_files: type: array items: - $ref: "#/components/schemas/FileResponse" + $ref: '#/components/schemas/FileResponse' expiry_date: type: string format: date-time @@ -1887,14 +1902,18 @@ components: properties: id: oneOf: - - type: string - - type: integer + - + type: string + - + type: integer filename: type: string size: oneOf: - - type: number - - type: string + - + type: number + - + type: string mimetype: type: string uploaded_by: @@ -1923,9 +1942,9 @@ components: properties: id: oneOf: - - { type: string } - - { type: integer } - description: "Array of file references (by ID) to link" + - {type: string} + - {type: integer} + description: 'Array of file references (by ID) to link' expiry_date: type: string format: date-time @@ -1934,7 +1953,7 @@ components: type: array items: type: integer - description: "Model inventory IDs to map this evidence to" + description: 'Model inventory IDs to map this evidence to' EvidenceUpdateRequest: type: object properties: @@ -1954,16 +1973,18 @@ components: properties: id: oneOf: - - { type: string } - - { type: integer } - description: "New files to link" + - {type: string} + - {type: integer} + description: 'New files to link' deleteFiles: type: array items: oneOf: - - type: string - - type: integer - description: "File IDs to unlink" + - + type: string + - + type: integer + description: 'File IDs to unlink' expiry_date: type: string format: date-time @@ -2041,7 +2062,7 @@ components: documentation_data: type: array items: - $ref: "#/components/schemas/DocumentationFile" + $ref: '#/components/schemas/DocumentationFile' is_demo: type: boolean created_at: @@ -2054,15 +2075,15 @@ components: type: array items: type: integer - description: "Related model inventory IDs" + description: 'Related model inventory IDs' projects: type: array items: type: integer - description: "Related project IDs" + description: 'Related project IDs' DocumentationFile: type: object - description: "File metadata stored in JSONB documentation_data column" + description: 'File metadata stored in JSONB documentation_data column' properties: id: type: string @@ -2138,7 +2159,7 @@ components: status_date: type: string format: date-time - description: "Defaults to current time if omitted" + description: 'Defaults to current time if omitted' known_biases: type: string bias_mitigation: @@ -2150,7 +2171,7 @@ components: documentation_data: type: array items: - $ref: "#/components/schemas/DocumentationFile" + $ref: '#/components/schemas/DocumentationFile' is_demo: type: boolean default: false @@ -2158,15 +2179,15 @@ components: type: array items: type: integer - description: "Model inventory IDs to associate" + description: 'Model inventory IDs to associate' projects: type: array items: type: integer - description: "Project IDs to associate" + description: 'Project IDs to associate' DatasetUpdateRequest: type: object - description: "All fields optional. Only provided fields are updated." + description: 'All fields optional. Only provided fields are updated.' properties: name: type: string @@ -2231,25 +2252,25 @@ components: documentation_data: type: array items: - $ref: "#/components/schemas/DocumentationFile" + $ref: '#/components/schemas/DocumentationFile' is_demo: type: boolean models: type: array items: type: integer - description: "Replace model associations (provide full list)" + description: 'Replace model associations (provide full list)' projects: type: array items: type: integer - description: "Replace project associations (provide full list)" + description: 'Replace project associations (provide full list)' deleteModels: type: boolean - description: "If true, remove all model associations (even if models array is empty)" + description: 'If true, remove all model associations (even if models array is empty)' deleteProjects: type: boolean - description: "If true, remove all project associations (even if projects array is empty)" + description: 'If true, remove all project associations (even if projects array is empty)' DatasetChangeHistoryEntry: type: object properties: @@ -2259,7 +2280,7 @@ components: type: integer change_type: type: string - description: "e.g. created, deleted, field_change" + description: 'e.g. created, deleted, field_change' field_name: type: string nullable: true @@ -2282,10 +2303,10 @@ components: type: integer key: type: string - description: "Machine-readable trigger identifier" + description: 'Machine-readable trigger identifier' label: type: string - description: "Human-readable name" + description: 'Human-readable name' event_name: type: string description: @@ -2298,17 +2319,17 @@ components: type: integer key: type: string - description: "Machine-readable action identifier" + description: 'Machine-readable action identifier' label: type: string - description: "Human-readable name" + description: 'Human-readable name' description: type: string nullable: true default_params: type: object nullable: true - description: "Default parameters for this action type" + description: 'Default parameters for this action type' Automation: type: object properties: @@ -2320,7 +2341,7 @@ components: type: integer params: type: object - description: "Trigger-specific parameters (JSON)" + description: 'Trigger-specific parameters (JSON)' is_active: type: boolean created_by: @@ -2334,13 +2355,15 @@ components: format: date-time AutomationWithActions: allOf: - - $ref: "#/components/schemas/Automation" - - type: object + - + $ref: '#/components/schemas/Automation' + - + type: object properties: actions: type: array items: - $ref: "#/components/schemas/TenantAutomationAction" + $ref: '#/components/schemas/TenantAutomationAction' TenantAutomationAction: type: object properties: @@ -2353,10 +2376,10 @@ components: params: type: object nullable: true - description: "Action-specific parameters (JSON)" + description: 'Action-specific parameters (JSON)' order: type: integer - description: "Execution order (1-based)" + description: 'Execution order (1-based)' AutomationCreateRequest: type: object required: @@ -2366,14 +2389,14 @@ components: properties: triggerId: type: integer - description: "ID of the automation trigger" + description: 'ID of the automation trigger' name: type: string - description: "Automation name" + description: 'Automation name' params: type: string - description: "JSON-encoded trigger parameters (parsed server-side)" - default: "{}" + description: 'JSON-encoded trigger parameters (parsed server-side)' + default: '{}' actions: type: array minItems: 1 @@ -2384,11 +2407,11 @@ components: properties: action_type_id: type: integer - description: "ID of the action type" + description: 'ID of the action type' params: type: object nullable: true - description: "Action-specific parameters" + description: 'Action-specific parameters' AutomationUpdateRequest: type: object properties: @@ -2398,8 +2421,8 @@ components: type: integer params: type: string - description: "JSON-encoded trigger parameters" - default: "{}" + description: 'JSON-encoded trigger parameters' + default: '{}' is_active: type: boolean actions: @@ -2414,7 +2437,7 @@ components: params: type: object nullable: true - description: "If provided, replaces all existing actions" + description: 'If provided, replaces all existing actions' AutomationHistoryResponse: type: object properties: @@ -2440,17 +2463,17 @@ components: type: array items: type: object - description: "Per-action execution results" + description: 'Per-action execution results' total: type: integer - description: "Total number of log entries" + description: 'Total number of log entries' limit: type: integer offset: type: integer AutomationStats: type: object - description: "Aggregated execution statistics for an automation" + description: 'Aggregated execution statistics for an automation' properties: total_executions: type: integer @@ -2467,7 +2490,7 @@ components: statusCode: type: integer data: - description: "Response payload (varies by endpoint)" + description: 'Response payload (varies by endpoint)' Pagination: type: object properties: @@ -2552,25 +2575,25 @@ components: properties: repository_url: type: string - description: "GitHub repository URL" - example: "https://github.com/owner/repo" + description: 'GitHub repository URL' + example: 'https://github.com/owner/repo' scan_mode: - $ref: "#/components/schemas/ScanMode" + $ref: '#/components/schemas/ScanMode' base_commit_sha: type: string - pattern: "^[0-9a-fA-F]{7,40}$" - description: "Required for incremental scans" + pattern: '^[0-9a-fA-F]{7,40}$' + description: 'Required for incremental scans' head_commit_sha: type: string - pattern: "^[0-9a-fA-F]{7,40}$" - description: "Required for incremental scans" + pattern: '^[0-9a-fA-F]{7,40}$' + description: 'Required for incremental scans' ScanStatusResponse: type: object properties: id: type: integer status: - $ref: "#/components/schemas/ScanStatus" + $ref: '#/components/schemas/ScanStatus' progress: type: number format: float @@ -2610,7 +2633,7 @@ components: repository_name: type: string status: - $ref: "#/components/schemas/ScanStatus" + $ref: '#/components/schemas/ScanStatus' findings_count: type: integer files_scanned: @@ -2630,7 +2653,7 @@ components: type: string nullable: true triggered_by: - $ref: "#/components/schemas/TriggeredByUser" + $ref: '#/components/schemas/TriggeredByUser' risk_score: type: number nullable: true @@ -2645,7 +2668,7 @@ components: format: date-time nullable: true scan_mode: - $ref: "#/components/schemas/ScanMode" + $ref: '#/components/schemas/ScanMode' base_commit_sha: type: string nullable: true @@ -2662,7 +2685,7 @@ components: type: string format: date-time summary: - $ref: "#/components/schemas/ScanSummary" + $ref: '#/components/schemas/ScanSummary' ScanSummary: type: object properties: @@ -2693,7 +2716,7 @@ components: repository_name: type: string status: - $ref: "#/components/schemas/ScanStatus" + $ref: '#/components/schemas/ScanStatus' findings_count: type: integer files_scanned: @@ -2710,7 +2733,7 @@ components: type: integer nullable: true triggered_by: - $ref: "#/components/schemas/TriggeredByUser" + $ref: '#/components/schemas/TriggeredByUser' risk_score: type: number nullable: true @@ -2718,7 +2741,7 @@ components: type: string nullable: true scan_mode: - $ref: "#/components/schemas/ScanMode" + $ref: '#/components/schemas/ScanMode' baseline_scan_id: type: integer nullable: true @@ -2734,9 +2757,9 @@ components: scans: type: array items: - $ref: "#/components/schemas/ScanListItem" + $ref: '#/components/schemas/ScanListItem' pagination: - $ref: "#/components/schemas/Pagination" + $ref: '#/components/schemas/Pagination' CancelScanResponse: type: object properties: @@ -2769,7 +2792,7 @@ components: id: type: integer finding_type: - $ref: "#/components/schemas/FindingType" + $ref: '#/components/schemas/FindingType' category: type: string name: @@ -2777,9 +2800,9 @@ components: provider: type: string confidence: - $ref: "#/components/schemas/ConfidenceLevel" + $ref: '#/components/schemas/ConfidenceLevel' risk_level: - $ref: "#/components/schemas/RiskLevel" + $ref: '#/components/schemas/RiskLevel' description: type: string nullable: true @@ -2791,9 +2814,9 @@ components: file_paths: type: array items: - $ref: "#/components/schemas/FilePath" + $ref: '#/components/schemas/FilePath' governance_status: - $ref: "#/components/schemas/GovernanceStatus" + $ref: '#/components/schemas/GovernanceStatus' governance_updated_at: type: string format: date-time @@ -2808,7 +2831,7 @@ components: type: string nullable: true license_risk: - $ref: "#/components/schemas/LicenseRiskLevel" + $ref: '#/components/schemas/LicenseRiskLevel' license_source: type: string enum: @@ -2828,16 +2851,16 @@ components: type: object nullable: true finding_status: - $ref: "#/components/schemas/FindingStatus" + $ref: '#/components/schemas/FindingStatus' FindingsResponse: type: object properties: findings: type: array items: - $ref: "#/components/schemas/Finding" + $ref: '#/components/schemas/Finding' pagination: - $ref: "#/components/schemas/Pagination" + $ref: '#/components/schemas/Pagination' UpdateGovernanceStatusRequest: type: object properties: @@ -2848,7 +2871,7 @@ components: - approved - flagged nullable: true - description: "Set to null to clear governance status" + description: 'Set to null to clear governance status' UpdateGovernanceStatusResponse: type: object properties: @@ -2901,7 +2924,7 @@ components: type: number minimum: 0 maximum: 1 - description: "All values must sum to 1.0" + description: 'All values must sum to 1.0' VulnerabilityTypesEnabled: type: object properties: @@ -2937,11 +2960,11 @@ components: type: integer nullable: true dimension_weights: - $ref: "#/components/schemas/DimensionWeights" + $ref: '#/components/schemas/DimensionWeights' vulnerability_scan_enabled: type: boolean vulnerability_types_enabled: - $ref: "#/components/schemas/VulnerabilityTypesEnabled" + $ref: '#/components/schemas/VulnerabilityTypesEnabled' updated_by: type: integer nullable: true @@ -2958,11 +2981,11 @@ components: type: integer nullable: true dimension_weights: - $ref: "#/components/schemas/DimensionWeights" + $ref: '#/components/schemas/DimensionWeights' vulnerability_scan_enabled: type: boolean vulnerability_types_enabled: - $ref: "#/components/schemas/VulnerabilityTypesEnabled" + $ref: '#/components/schemas/VulnerabilityTypesEnabled' DependencyGraphResponse: type: object properties: @@ -3095,8 +3118,8 @@ components: properties: repository_url: type: string - description: "GitHub repository URL" - example: "https://github.com/owner/repo" + description: 'GitHub repository URL' + example: 'https://github.com/owner/repo' display_name: type: string nullable: true @@ -3115,17 +3138,17 @@ components: - daily - weekly - monthly - description: "Required when schedule_enabled is true" + description: 'Required when schedule_enabled is true' schedule_day_of_week: type: integer minimum: 0 maximum: 6 - description: "Required for weekly schedule (0=Sunday)" + description: 'Required for weekly schedule (0=Sunday)' schedule_day_of_month: type: integer minimum: 1 maximum: 31 - description: "Required for monthly schedule" + description: 'Required for monthly schedule' schedule_hour: type: integer minimum: 0 @@ -3189,12 +3212,12 @@ components: repositories: type: array items: - $ref: "#/components/schemas/Repository" + $ref: '#/components/schemas/Repository' pagination: - $ref: "#/components/schemas/Pagination" + $ref: '#/components/schemas/Pagination' AITrustCentreOverview: type: object - description: "Full overview with all sections" + description: 'Full overview with all sections' properties: intro: type: object @@ -3354,7 +3377,7 @@ components: type: string AITrustCentrePublicPage: type: object - description: "Conditionally includes sections based on visibility settings" + description: 'Conditionally includes sections based on visibility settings' properties: info: type: object @@ -3461,18 +3484,18 @@ components: type: string enum: - Malfunction - - "Unexpected behavior" - - "Model drift" + - 'Unexpected behavior' + - 'Model drift' - Misuse - - "Data corruption" - - "Security breach" - - "Performance degradation" + - 'Data corruption' + - 'Security breach' + - 'Performance degradation' IncidentSeverity: type: string enum: - Minor - Serious - - "Very serious" + - 'Very serious' IncidentStatus: type: string enum: @@ -3486,7 +3509,7 @@ components: - Approved - Rejected - Pending - - "Not required" + - 'Not required' Incident: type: object properties: @@ -3498,11 +3521,11 @@ components: ai_project: type: string type: - $ref: "#/components/schemas/IncidentType" + $ref: '#/components/schemas/IncidentType' severity: - $ref: "#/components/schemas/IncidentSeverity" + $ref: '#/components/schemas/IncidentSeverity' status: - $ref: "#/components/schemas/IncidentStatus" + $ref: '#/components/schemas/IncidentStatus' occurred_date: type: string format: date-time @@ -3537,7 +3560,7 @@ components: archived: type: boolean approval_status: - $ref: "#/components/schemas/IncidentApprovalStatus" + $ref: '#/components/schemas/IncidentApprovalStatus' approved_by: type: string nullable: true @@ -3572,11 +3595,11 @@ components: ai_project: type: string type: - $ref: "#/components/schemas/IncidentType" + $ref: '#/components/schemas/IncidentType' severity: - $ref: "#/components/schemas/IncidentSeverity" + $ref: '#/components/schemas/IncidentSeverity' status: - $ref: "#/components/schemas/IncidentStatus" + $ref: '#/components/schemas/IncidentStatus' occurred_date: type: string format: date-time @@ -3586,16 +3609,18 @@ components: reporter: type: string approval_status: - $ref: "#/components/schemas/IncidentApprovalStatus" + $ref: '#/components/schemas/IncidentApprovalStatus' approved_by: type: string categories_of_harm: oneOf: - - type: array + - + type: array items: type: string - - type: string - description: "Comma-separated string (will be split)" + - + type: string + description: 'Comma-separated string (will be split)' affected_persons_groups: type: string description: @@ -3621,16 +3646,16 @@ components: type: string UpdateIncidentRequest: type: object - description: "All fields are optional; only provided fields are updated." + description: 'All fields are optional; only provided fields are updated.' properties: ai_project: type: string type: - $ref: "#/components/schemas/IncidentType" + $ref: '#/components/schemas/IncidentType' severity: - $ref: "#/components/schemas/IncidentSeverity" + $ref: '#/components/schemas/IncidentSeverity' status: - $ref: "#/components/schemas/IncidentStatus" + $ref: '#/components/schemas/IncidentStatus' occurred_date: type: string format: date-time @@ -3640,15 +3665,17 @@ components: reporter: type: string approval_status: - $ref: "#/components/schemas/IncidentApprovalStatus" + $ref: '#/components/schemas/IncidentApprovalStatus' approved_by: type: string categories_of_harm: oneOf: - - type: array + - + type: array items: type: string - - type: string + - + type: string affected_persons_groups: type: string description: @@ -3702,7 +3729,7 @@ components: subClauses: type: array items: - $ref: "#/components/schemas/ISO27001SubClause" + $ref: '#/components/schemas/ISO27001SubClause' ISO27001SubClause: type: object properties: @@ -3717,9 +3744,9 @@ components: status: type: string enum: - - "Not started" + - 'Not started' - Draft - - "In review" + - 'In review' - Done owner: type: integer @@ -3770,7 +3797,7 @@ components: controls: type: array items: - $ref: "#/components/schemas/ISO27001AnnexControl" + $ref: '#/components/schemas/ISO27001AnnexControl' ISO27001AnnexControl: type: object properties: @@ -3785,9 +3812,9 @@ components: status: type: string enum: - - "Not started" + - 'Not started' - Draft - - "In review" + - 'In review' - Done owner: type: integer @@ -3818,16 +3845,16 @@ components: properties: user_id: type: string - description: "User ID for file upload attribution" + description: 'User ID for file upload attribution' project_id: type: string - description: "Project ID for file upload association" + description: 'Project ID for file upload association' status: type: string enum: - - "Not started" + - 'Not started' - Draft - - "In review" + - 'In review' - Done owner: type: integer @@ -3842,37 +3869,37 @@ components: type: string tags: type: string - description: "JSON-encoded array of tag strings" + description: 'JSON-encoded array of tag strings' delete: type: string - description: "JSON-encoded array of file IDs to unlink" + description: 'JSON-encoded array of file IDs to unlink' risksDelete: type: string - description: "JSON-encoded array of risk IDs to remove" + description: 'JSON-encoded array of risk IDs to remove' risksMitigated: type: string - description: "JSON-encoded array of risk IDs to add" + description: 'JSON-encoded array of risk IDs to add' files: type: array items: type: string format: binary - description: "Evidence files to upload" + description: 'Evidence files to upload' ISO27001SaveAnnexRequest: type: object properties: user_id: type: string - description: "User ID for file upload attribution" + description: 'User ID for file upload attribution' project_id: type: string - description: "Project ID for file upload association" + description: 'Project ID for file upload association' status: type: string enum: - - "Not started" + - 'Not started' - Draft - - "In review" + - 'In review' - Done owner: type: integer @@ -3887,22 +3914,22 @@ components: type: string tags: type: string - description: "JSON-encoded array of tag strings" + description: 'JSON-encoded array of tag strings' delete: type: string - description: "JSON-encoded array of file IDs to unlink" + description: 'JSON-encoded array of file IDs to unlink' risksDelete: type: string - description: "JSON-encoded array of risk IDs to remove" + description: 'JSON-encoded array of risk IDs to remove' risksMitigated: type: string - description: "JSON-encoded array of risk IDs to add" + description: 'JSON-encoded array of risk IDs to add' files: type: array items: type: string format: binary - description: "Evidence files to upload" + description: 'Evidence files to upload' ISO42001Clause: type: object properties: @@ -3924,7 +3951,7 @@ components: subClauses: type: array items: - $ref: "#/components/schemas/ISO42001SubClause" + $ref: '#/components/schemas/ISO42001SubClause' ISO42001SubClause: type: object properties: @@ -3939,9 +3966,9 @@ components: status: type: string enum: - - "Not started" + - 'Not started' - Draft - - "In review" + - 'In review' - Done owner: type: integer @@ -3988,7 +4015,7 @@ components: categories: type: array items: - $ref: "#/components/schemas/ISO42001AnnexCategory" + $ref: '#/components/schemas/ISO42001AnnexCategory' ISO42001AnnexCategory: type: object properties: @@ -4003,9 +4030,9 @@ components: status: type: string enum: - - "Not started" + - 'Not started' - Draft - - "In review" + - 'In review' - Done owner: type: integer @@ -4041,9 +4068,9 @@ components: status: type: string enum: - - "Not started" + - 'Not started' - Draft - - "In review" + - 'In review' - Done owner: type: integer @@ -4058,16 +4085,16 @@ components: type: string tags: type: string - description: "JSON-encoded array of tag strings" + description: 'JSON-encoded array of tag strings' delete: type: string - description: "JSON-encoded array of file IDs to unlink" + description: 'JSON-encoded array of file IDs to unlink' risksDelete: type: string - description: "JSON-encoded array of risk IDs to remove" + description: 'JSON-encoded array of risk IDs to remove' risksMitigated: type: string - description: "JSON-encoded array of risk IDs to add" + description: 'JSON-encoded array of risk IDs to add' files: type: array items: @@ -4083,9 +4110,9 @@ components: status: type: string enum: - - "Not started" + - 'Not started' - Draft - - "In review" + - 'In review' - Done owner: type: integer @@ -4100,16 +4127,16 @@ components: type: string tags: type: string - description: "JSON-encoded array of tag strings" + description: 'JSON-encoded array of tag strings' delete: type: string - description: "JSON-encoded array of file IDs to unlink" + description: 'JSON-encoded array of file IDs to unlink' risksDelete: type: string - description: "JSON-encoded array of risk IDs to remove" + description: 'JSON-encoded array of risk IDs to remove' risksMitigated: type: string - description: "JSON-encoded array of risk IDs to add" + description: 'JSON-encoded array of risk IDs to add' files: type: array items: @@ -4152,9 +4179,9 @@ components: status: type: string enum: - - "Not started" + - 'Not started' - Draft - - "In review" + - 'In review' - Done owner: type: integer @@ -4185,16 +4212,16 @@ components: properties: user_id: type: string - description: "User ID for file upload attribution" + description: 'User ID for file upload attribution' project_id: type: string - description: "Project ID for file upload association" + description: 'Project ID for file upload association' status: type: string enum: - - "Not started" + - 'Not started' - Draft - - "In review" + - 'In review' - Done owner: type: integer @@ -4209,22 +4236,22 @@ components: type: string tags: type: string - description: "JSON-encoded array of tag strings" + description: 'JSON-encoded array of tag strings' delete: type: string - description: "JSON-encoded array of file IDs to unlink" + description: 'JSON-encoded array of file IDs to unlink' risksDelete: type: string - description: "JSON-encoded array of risk IDs to remove" + description: 'JSON-encoded array of risk IDs to remove' risksMitigated: type: string - description: "JSON-encoded array of risk IDs to add" + description: 'JSON-encoded array of risk IDs to add' files: type: array items: type: string format: binary - description: "Evidence files to upload" + description: 'Evidence files to upload' NISTSubcategoryStatusUpdate: type: object required: @@ -4233,11 +4260,11 @@ components: status: type: string enum: - - "Not started" + - 'Not started' - Draft - - "In review" + - 'In review' - Done - description: "New status value" + description: 'New status value' NISTProgress: type: object properties: @@ -4268,17 +4295,17 @@ components: type: object properties: riskManagement: - $ref: "#/components/schemas/ModuleScore" + $ref: '#/components/schemas/ModuleScore' vendorManagement: - $ref: "#/components/schemas/ModuleScore" + $ref: '#/components/schemas/ModuleScore' projectGovernance: - $ref: "#/components/schemas/ModuleScore" + $ref: '#/components/schemas/ModuleScore' modelLifecycle: - $ref: "#/components/schemas/ModuleScore" + $ref: '#/components/schemas/ModuleScore' policyDocumentation: - $ref: "#/components/schemas/ModuleScore" + $ref: '#/components/schemas/ModuleScore' metadata: - $ref: "#/components/schemas/ComplianceMetadata" + $ref: '#/components/schemas/ComplianceMetadata' ModuleScore: type: object properties: @@ -4288,18 +4315,18 @@ components: maximum: 100 weight: type: number - description: "Module weight (sums to 1.0 across all modules)" + description: 'Module weight (sums to 1.0 across all modules)' components: type: array items: - $ref: "#/components/schemas/ComponentScore" + $ref: '#/components/schemas/ComponentScore' totalDataPoints: type: integer qualityScore: type: number minimum: 0 maximum: 1 - description: "Data completeness indicator" + description: 'Data completeness indicator' ComponentScore: type: object properties: @@ -4313,7 +4340,7 @@ components: type: integer details: type: object - description: "Module-specific details" + description: 'Module-specific details' ComplianceMetadata: type: object properties: @@ -4350,44 +4377,29 @@ components: type: string ComplianceDetails: allOf: - - $ref: "#/components/schemas/ComplianceScore" - - type: object + - + $ref: '#/components/schemas/ComplianceScore' + - + type: object properties: insights: type: object properties: strongestModule: type: object - properties: { name: { type: string }, score: { type: number } } + properties: {name: {type: string}, score: {type: number}} weakestModule: type: object - properties: { name: { type: string }, score: { type: number } } + properties: {name: {type: string}, score: {type: number}} improvementPriority: type: array - items: - { - type: object, - properties: - { - module: { type: string }, - score: { type: number }, - weight: { type: number }, - impact: { type: number, description: "(100 - score) * weight" }, - }, - } + items: {type: object, properties: {module: {type: string}, score: {type: number}, weight: {type: number}, impact: {type: number, description: '(100 - score) * weight'}}} overallTrend: type: string enum: [up, down, stable] dataQuality: type: object - properties: - { - riskManagement: { type: number }, - vendorManagement: { type: number }, - projectGovernance: { type: number }, - modelLifecycle: { type: number }, - policyDocumentation: { type: number }, - } + properties: {riskManagement: {type: number}, vendorManagement: {type: number}, projectGovernance: {type: number}, modelLifecycle: {type: number}, policyDocumentation: {type: number}} AssignmentResponse: type: object properties: @@ -4403,7 +4415,7 @@ components: data: type: array items: - $ref: "#/components/schemas/Risk" + $ref: '#/components/schemas/Risk' Risk: type: object properties: @@ -4425,7 +4437,7 @@ components: code: type: integer data: - description: "Response payload" + description: 'Response payload' ShareLinkSettings: type: object properties: @@ -4458,9 +4470,9 @@ components: resource_id: type: integer minimum: 0 - description: "Use 0 for sharing the entire table/list view" + description: 'Use 0 for sharing the entire table/list view' settings: - $ref: "#/components/schemas/ShareLinkSettings" + $ref: '#/components/schemas/ShareLinkSettings' expires_at: type: string format: date-time @@ -4469,7 +4481,7 @@ components: type: object properties: settings: - $ref: "#/components/schemas/ShareLinkSettings" + $ref: '#/components/schemas/ShareLinkSettings' is_enabled: type: boolean expires_at: @@ -4488,7 +4500,7 @@ components: resource_id: type: integer settings: - $ref: "#/components/schemas/ShareLinkSettings" + $ref: '#/components/schemas/ShareLinkSettings' is_enabled: type: boolean expires_at: @@ -4506,11 +4518,13 @@ components: format: uri ShareLinkListItem: allOf: - - $ref: "#/components/schemas/ShareLinkResponse" - - properties: + - + $ref: '#/components/schemas/ShareLinkResponse' + - + properties: is_valid: type: boolean - description: "Whether the link is currently active and not expired" + description: 'Whether the link is currently active and not expired' ShareLinkMetadata: type: object properties: @@ -4523,7 +4537,7 @@ components: resource_id: type: integer settings: - $ref: "#/components/schemas/ShareLinkSettings" + $ref: '#/components/schemas/ShareLinkSettings' is_enabled: type: boolean expires_at: @@ -4547,12 +4561,14 @@ components: resource_type: type: string settings: - $ref: "#/components/schemas/ShareLinkSettings" + $ref: '#/components/schemas/ShareLinkSettings' data: - description: "Resource data (object for single record, array for table view)" + description: 'Resource data (object for single record, array for table view)' oneOf: - - type: object - - type: array + - + type: object + - + type: array items: type: object permissions: @@ -4610,19 +4626,19 @@ components: properties: results: type: object - description: "Results grouped by entity type (projects, vendors, models, etc.)" + description: 'Results grouped by entity type (projects, vendors, models, etc.)' additionalProperties: type: array items: type: object - description: "Entity-specific result object" + description: 'Entity-specific result object' totalCount: type: integer query: type: string message: type: string - description: "Present when query is too short" + description: 'Present when query is too short' ReportGenerateRequest: type: object required: @@ -4639,7 +4655,8 @@ components: type: integer reportType: oneOf: - - type: string + - + type: string enum: - projectRisks - vendorRisks @@ -4654,7 +4671,8 @@ components: - policyManager - incidentManagement - all - - type: array + - + type: array items: type: string reportName: @@ -4670,7 +4688,7 @@ components: default: false llmKeyId: type: integer - description: "LLM key ID for AI-enhanced reports" + description: 'LLM key ID for AI-enhanced reports' GeneratedReport: type: object properties: @@ -4694,16 +4712,16 @@ components: properties: projects: type: integer - description: "Total project count" + description: 'Total project count' trainings: type: integer - description: "Total training records" + description: 'Total training records' models: type: integer - description: "Total model count" + description: 'Total model count' reports: type: integer - description: "Total report count" + description: 'Total report count' task_radar: type: object properties: @@ -4717,7 +4735,7 @@ components: type: array items: type: object - description: "Project summary objects" + description: 'Project summary objects' ConformityStep: type: object properties: @@ -4731,10 +4749,10 @@ components: status: type: string enum: - - "Not started" - - "In progress" + - 'Not started' + - 'In progress' - Completed - - "Not needed" + - 'Not needed' owner: type: string nullable: true @@ -4770,7 +4788,7 @@ components: conformitySteps: type: array items: - $ref: "#/components/schemas/ConformityStep" + $ref: '#/components/schemas/ConformityStep' completedStepsCount: type: integer totalStepsCount: @@ -4883,10 +4901,10 @@ components: status: type: string enum: - - "Not started" - - "In progress" + - 'Not started' + - 'In progress' - Completed - - "Not needed" + - 'Not needed' owner: type: string dueDate: @@ -4988,7 +5006,7 @@ components: format: date-time TierFeatures: type: object - description: "Tier details with associated feature flags" + description: 'Tier details with associated feature flags' properties: id: type: integer @@ -5013,11 +5031,11 @@ components: properties: name: type: string - description: "Descriptive name for the token" + description: 'Descriptive name for the token' expires_in_days: type: integer minimum: 1 - description: "Number of days until token expires" + description: 'Number of days until token expires' ApiToken: type: object properties: @@ -5037,11 +5055,13 @@ components: format: date-time ApiTokenCreated: allOf: - - $ref: "#/components/schemas/ApiToken" - - properties: + - + $ref: '#/components/schemas/ApiToken' + - + properties: token: type: string - description: "Full JWT token value (only shown on creation)" + description: 'Full JWT token value (only shown on creation)' LLMKeyCreateRequest: type: object required: @@ -5055,23 +5075,23 @@ components: - OpenAI - OpenRouter - Custom - description: "LLM provider name" + description: 'LLM provider name' key: type: string - description: "API key value" + description: 'API key value' model: type: string - description: "Default model to use with this key" + description: 'Default model to use with this key' url: type: string format: uri - description: "Required for Custom provider; auto-populated for others" + description: 'Required for Custom provider; auto-populated for others' custom_headers: type: object additionalProperties: type: string nullable: true - description: "Optional custom headers (string key-value pairs)" + description: 'Optional custom headers (string key-value pairs)' LLMKeyUpdateRequest: type: object properties: @@ -5103,7 +5123,7 @@ components: type: string key: type: string - description: "Masked key value" + description: 'Masked key value' url: type: string model: @@ -5144,7 +5164,7 @@ components: minimum: 1 date_format: type: string - description: "Preferred date format (e.g., YYYY-MM-DD, DD/MM/YYYY)" + description: 'Preferred date format (e.g., YYYY-MM-DD, DD/MM/YYYY)' UserPreferenceUpdateRequest: type: object properties: @@ -5201,9 +5221,9 @@ components: files: type: array items: - $ref: "#/components/schemas/FileListItem" + $ref: '#/components/schemas/FileListItem' pagination: - $ref: "#/components/schemas/Pagination" + $ref: '#/components/schemas/Pagination' FileListItem: type: object properties: @@ -5245,11 +5265,11 @@ components: type: string snippet: type: string - description: "Highlighted text snippet matching the query" + description: 'Highlighted text snippet matching the query' rank: type: number pagination: - $ref: "#/components/schemas/Pagination" + $ref: '#/components/schemas/Pagination' FileMetadata: type: object properties: @@ -5284,7 +5304,7 @@ components: items: type: string maxItems: 50 - description: "Each tag max 100 chars, alphanumeric/spaces/hyphens/underscores only" + description: 'Each tag max 100 chars, alphanumeric/spaces/hyphens/underscores only' review_status: type: string enum: @@ -5357,13 +5377,13 @@ components: parent_id: type: integer nullable: true - description: "Parent folder ID (null for root)" + description: 'Parent folder ID (null for root)' color: type: string - description: "Hex color code" + description: 'Hex color code' icon: type: string - description: "Icon identifier" + description: 'Icon identifier' UpdateFolderRequest: type: object properties: @@ -5375,7 +5395,7 @@ components: parent_id: type: integer nullable: true - description: "New parent (null to move to root). Cannot create circular refs." + description: 'New parent (null to move to root). Cannot create circular refs.' color: type: string icon: @@ -5453,7 +5473,7 @@ components: type: integer key: type: string - description: "Full API key (shown only on creation)" + description: 'Full API key (shown only on creation)' key_prefix: type: string label: @@ -5520,7 +5540,7 @@ components: domain: type: string status: - $ref: "#/components/schemas/ShadowAiToolStatus" + $ref: '#/components/schemas/ShadowAiToolStatus' first_seen: type: string format: date-time @@ -5976,12 +5996,12 @@ components: - Low - Medium - High - description: "Task priority level." + description: 'Task priority level.' TaskStatus: type: string enum: - Open - - "In Progress" + - 'In Progress' - Completed - Overdue - Deleted @@ -5999,7 +6019,7 @@ components: - iso27001_annexcontrol - eu_control - eu_subcontrol - description: "Type of entity that can be linked to a task." + description: 'Type of entity that can be linked to a task.' CreateTaskRequest: type: object required: @@ -6007,7 +6027,7 @@ components: properties: title: type: string - example: "Review vendor risk assessment" + example: 'Review vendor risk assessment' description: type: string nullable: true @@ -6015,11 +6035,11 @@ components: type: string format: date-time nullable: true - example: "2026-05-15T00:00:00.000Z" + example: '2026-05-15T00:00:00.000Z' priority: - $ref: "#/components/schemas/TaskPriority" + $ref: '#/components/schemas/TaskPriority' status: - $ref: "#/components/schemas/TaskStatus" + $ref: '#/components/schemas/TaskStatus' categories: type: array items: @@ -6031,10 +6051,12 @@ components: type: array items: oneOf: - - type: integer - - type: object + - + type: integer + - + type: object properties: - user_id: { type: integer } + user_id: {type: integer} required: - user_id example: @@ -6049,7 +6071,7 @@ components: entity_id: type: integer entity_type: - $ref: "#/components/schemas/EntityType" + $ref: '#/components/schemas/EntityType' entity_name: type: string UpdateTaskRequest: @@ -6065,9 +6087,9 @@ components: format: date-time nullable: true priority: - $ref: "#/components/schemas/TaskPriority" + $ref: '#/components/schemas/TaskPriority' status: - $ref: "#/components/schemas/TaskStatus" + $ref: '#/components/schemas/TaskStatus' categories: type: array items: @@ -6084,7 +6106,7 @@ components: entity_id: type: integer entity_type: - $ref: "#/components/schemas/EntityType" + $ref: '#/components/schemas/EntityType' entity_name: type: string AddEntityLinkRequest: @@ -6097,10 +6119,10 @@ components: type: integer example: 42 entity_type: - $ref: "#/components/schemas/EntityType" + $ref: '#/components/schemas/EntityType' entity_name: type: string - example: "Acme Corp" + example: 'Acme Corp' Task: type: object properties: @@ -6109,7 +6131,7 @@ components: example: 1 title: type: string - example: "Review vendor risk assessment" + example: 'Review vendor risk assessment' description: type: string nullable: true @@ -6122,9 +6144,9 @@ components: format: date-time nullable: true priority: - $ref: "#/components/schemas/TaskPriority" + $ref: '#/components/schemas/TaskPriority' status: - $ref: "#/components/schemas/TaskStatus" + $ref: '#/components/schemas/TaskStatus' categories: type: array items: @@ -6139,8 +6161,10 @@ components: format: date-time TaskWithAssignees: allOf: - - $ref: "#/components/schemas/Task" - - type: object + - + $ref: '#/components/schemas/Task' + - + type: object properties: assignees: type: array @@ -6148,8 +6172,10 @@ components: type: integer TaskWithRelations: allOf: - - $ref: "#/components/schemas/Task" - - type: object + - + $ref: '#/components/schemas/Task' + - + type: object properties: assignees: type: array @@ -6158,11 +6184,13 @@ components: entity_links: type: array items: - $ref: "#/components/schemas/EntityLinkSummary" + $ref: '#/components/schemas/EntityLinkSummary' TaskDetail: allOf: - - $ref: "#/components/schemas/Task" - - type: object + - + $ref: '#/components/schemas/Task' + - + type: object properties: assignees: type: array @@ -6170,18 +6198,18 @@ components: type: integer creator_name: type: string - example: "Jane Smith" + example: 'Jane Smith' assignee_names: type: array items: type: string example: - - "John Doe" - - "Alice Johnson" + - 'John Doe' + - 'Alice Johnson' entity_links: type: array items: - $ref: "#/components/schemas/EntityLinkSummary" + $ref: '#/components/schemas/EntityLinkSummary' EntityLinkSummary: type: object properties: @@ -6190,7 +6218,7 @@ components: entity_id: type: integer entity_type: - $ref: "#/components/schemas/EntityType" + $ref: '#/components/schemas/EntityType' entity_name: type: string TaskEntityLink: @@ -6203,7 +6231,7 @@ components: entity_id: type: integer entity_type: - $ref: "#/components/schemas/EntityType" + $ref: '#/components/schemas/EntityType' entity_name: type: string nullable: true @@ -6222,41 +6250,47 @@ components: - message EnvelopeEmpty: allOf: - - $ref: "#/components/schemas/EnvelopeBase" - - type: object + - + $ref: '#/components/schemas/EnvelopeBase' + - + type: object properties: message: - example: "No Content" + example: 'No Content' data: type: array items: {} example: [] EnvelopeNotFound: allOf: - - $ref: "#/components/schemas/EnvelopeBase" - - type: object + - + $ref: '#/components/schemas/EnvelopeBase' + - + type: object properties: message: - example: "Not Found" + example: 'Not Found' data: nullable: true example: null EnvelopeBadRequest: allOf: - - $ref: "#/components/schemas/EnvelopeBase" - - type: object + - + $ref: '#/components/schemas/EnvelopeBase' + - + type: object properties: message: - example: "Bad Request" + example: 'Bad Request' data: type: string - example: "Invalid training ID" + example: 'Invalid training ID' EnvelopeError: type: object properties: message: type: string - example: "Internal Server Error" + example: 'Internal Server Error' error: type: string required: @@ -6270,10 +6304,10 @@ components: example: 42 training_name: type: string - example: "AI Governance Fundamentals" + example: 'AI Governance Fundamentals' duration: type: string - example: "2 hours" + example: '2 hours' provider: type: string example: Internal @@ -6284,7 +6318,7 @@ components: type: string enum: - Planned - - "In Progress" + - 'In Progress' - Completed example: Planned numberOfPeople: @@ -6292,7 +6326,7 @@ components: example: 25 description: type: string - example: "Introductory course on EU AI Act compliance" + example: 'Introductory course on EU AI Act compliance' progressPercentage: type: integer enum: @@ -6328,10 +6362,10 @@ components: properties: training_name: type: string - example: "AI Governance Fundamentals" + example: 'AI Governance Fundamentals' duration: type: string - example: "2 hours" + example: '2 hours' provider: type: string example: Internal @@ -6342,7 +6376,7 @@ components: type: string enum: - Planned - - "In Progress" + - 'In Progress' - Completed example: Planned numberOfPeople: @@ -6350,7 +6384,7 @@ components: example: 25 description: type: string - example: "Introductory course on EU AI Act compliance" + example: 'Introductory course on EU AI Act compliance' is_demo: type: boolean default: false @@ -6359,10 +6393,10 @@ components: properties: training_name: type: string - example: "Advanced AI Risk Management" + example: 'Advanced AI Risk Management' duration: type: string - example: "3 days" + example: '3 days' provider: type: string example: External @@ -6373,35 +6407,39 @@ components: type: string enum: - Planned - - "In Progress" + - 'In Progress' - Completed - example: "In Progress" + example: 'In Progress' numberOfPeople: type: integer example: 30 description: type: string - example: "Updated scope to cover ISO 42001" + example: 'Updated scope to cover ISO 42001' TrainingListResponse: allOf: - - $ref: "#/components/schemas/EnvelopeBase" - - type: object + - + $ref: '#/components/schemas/EnvelopeBase' + - + type: object properties: message: example: OK data: type: array items: - $ref: "#/components/schemas/Training" + $ref: '#/components/schemas/Training' TrainingSingleResponse: allOf: - - $ref: "#/components/schemas/EnvelopeBase" - - type: object + - + $ref: '#/components/schemas/EnvelopeBase' + - + type: object properties: message: example: OK data: - $ref: "#/components/schemas/Training" + $ref: '#/components/schemas/Training' ChangeHistoryEntry: type: object properties: @@ -6424,7 +6462,7 @@ components: new_value: type: string nullable: true - example: "In Progress" + example: 'In Progress' change_type: type: string enum: @@ -6457,7 +6495,7 @@ components: data: type: array items: - $ref: "#/components/schemas/ChangeHistoryEntry" + $ref: '#/components/schemas/ChangeHistoryEntry' hasMore: type: boolean example: false @@ -6466,13 +6504,15 @@ components: example: 5 ChangeHistoryResponse: allOf: - - $ref: "#/components/schemas/EnvelopeBase" - - type: object + - + $ref: '#/components/schemas/EnvelopeBase' + - + type: object properties: message: example: OK data: - $ref: "#/components/schemas/ChangeHistoryData" + $ref: '#/components/schemas/ChangeHistoryData' CreateOrganizationRequest: type: object required: @@ -6484,12 +6524,12 @@ components: properties: name: type: string - example: "Acme Corp" + example: 'Acme Corp' logo: type: string format: uri nullable: true - example: "https://example.com/logo.png" + example: 'https://example.com/logo.png' userEmail: type: string format: email @@ -6509,12 +6549,12 @@ components: properties: name: type: string - example: "Acme Corporation" + example: 'Acme Corporation' logo: type: string format: uri nullable: true - example: "https://example.com/new-logo.png" + example: 'https://example.com/new-logo.png' Organization: type: object properties: @@ -6523,11 +6563,11 @@ components: example: 1 name: type: string - example: "Acme Corp" + example: 'Acme Corp' logo: type: string nullable: true - example: "https://example.com/logo.png" + example: 'https://example.com/logo.png' created_at: type: string format: date-time @@ -6563,7 +6603,7 @@ components: type: object properties: user: - $ref: "#/components/schemas/SafeUser" + $ref: '#/components/schemas/SafeUser' organization: type: object properties: @@ -6572,7 +6612,7 @@ components: example: 1 name: type: string - example: "Acme Corp" + example: 'Acme Corp' token: type: string example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... @@ -6675,16 +6715,17 @@ components: type: integer example: 7 type: - $ref: "#/components/schemas/NotificationType" + $ref: '#/components/schemas/NotificationType' title: type: string - example: "Task assigned to you" + example: 'Task assigned to you' message: type: string - example: "You have been assigned to task 'Review vendor contract'." + example: 'You have been assigned to task ''Review vendor contract''.' entity_type: allOf: - - $ref: "#/components/schemas/NotificationEntityType" + - + $ref: '#/components/schemas/NotificationEntityType' nullable: true entity_id: type: integer @@ -6693,7 +6734,7 @@ components: entity_name: type: string nullable: true - example: "Review vendor contract" + example: 'Review vendor contract' action_url: type: string nullable: true @@ -6708,7 +6749,7 @@ components: created_at: type: string format: date-time - example: "2026-04-20T10:00:00.000Z" + example: '2026-04-20T10:00:00.000Z' created_by: type: integer nullable: true @@ -6734,7 +6775,7 @@ components: type: array maxItems: 10 items: - $ref: "#/components/schemas/NotificationJSON" + $ref: '#/components/schemas/NotificationJSON' RealtimeNotification: type: object required: @@ -6746,7 +6787,7 @@ components: enum: - notification data: - $ref: "#/components/schemas/NotificationJSON" + $ref: '#/components/schemas/NotificationJSON' ApiEnvelope: type: object properties: @@ -6754,7 +6795,7 @@ components: type: integer example: 200 data: - description: "Response payload (varies per endpoint)" + description: 'Response payload (varies per endpoint)' FriaErrorEnvelope: type: object properties: @@ -6763,7 +6804,7 @@ components: example: 400 data: type: string - example: "Invalid project ID" + example: 'Invalid project ID' DeletedResponse: type: object properties: @@ -6807,7 +6848,7 @@ components: version: type: integer status: - $ref: "#/components/schemas/FriaStatus" + $ref: '#/components/schemas/FriaStatus' assessment_owner: type: string nullable: true @@ -6896,7 +6937,7 @@ components: risk_score: type: integer risk_level: - $ref: "#/components/schemas/FriaRiskLevel" + $ref: '#/components/schemas/FriaRiskLevel' rights_flagged: type: integer created_by: @@ -6927,7 +6968,7 @@ components: type: object properties: status: - $ref: "#/components/schemas/FriaStatus" + $ref: '#/components/schemas/FriaStatus' assessment_owner: type: string assessment_date: @@ -6988,19 +7029,19 @@ components: type: object properties: assessment: - $ref: "#/components/schemas/FriaAssessment" + $ref: '#/components/schemas/FriaAssessment' rights: type: array items: - $ref: "#/components/schemas/FriaRight" + $ref: '#/components/schemas/FriaRight' riskItems: type: array items: - $ref: "#/components/schemas/FriaRiskItem" + $ref: '#/components/schemas/FriaRiskItem' modelLinks: type: array items: - $ref: "#/components/schemas/FriaModelLink" + $ref: '#/components/schemas/FriaModelLink' FriaRight: type: object properties: @@ -7064,9 +7105,9 @@ components: risk_description: type: string likelihood: - $ref: "#/components/schemas/FriaLikelihood" + $ref: '#/components/schemas/FriaLikelihood' severity: - $ref: "#/components/schemas/FriaSeverity" + $ref: '#/components/schemas/FriaSeverity' existing_controls: type: string nullable: true @@ -7099,9 +7140,9 @@ components: risk_description: type: string likelihood: - $ref: "#/components/schemas/FriaLikelihood" + $ref: '#/components/schemas/FriaLikelihood' severity: - $ref: "#/components/schemas/FriaSeverity" + $ref: '#/components/schemas/FriaSeverity' existing_controls: type: string further_action: @@ -7116,9 +7157,9 @@ components: risk_description: type: string likelihood: - $ref: "#/components/schemas/FriaLikelihood" + $ref: '#/components/schemas/FriaLikelihood' severity: - $ref: "#/components/schemas/FriaSeverity" + $ref: '#/components/schemas/FriaSeverity' existing_controls: type: string further_action: @@ -7175,19 +7216,19 @@ components: type: object properties: assessment: - $ref: "#/components/schemas/FriaAssessment" + $ref: '#/components/schemas/FriaAssessment' rights: type: array items: - $ref: "#/components/schemas/FriaRight" + $ref: '#/components/schemas/FriaRight' riskItems: type: array items: - $ref: "#/components/schemas/FriaRiskItem" + $ref: '#/components/schemas/FriaRiskItem' modelLinks: type: array items: - $ref: "#/components/schemas/FriaModelLink" + $ref: '#/components/schemas/FriaModelLink' snapshot_reason: type: string nullable: true @@ -7210,7 +7251,7 @@ components: message: type: string data: - description: "Response payload (present on 1xx-4xx)" + description: 'Response payload (present on 1xx-4xx)' error: type: string ErrorResponse400: @@ -7218,10 +7259,10 @@ components: properties: message: type: string - example: "Bad Request" + example: 'Bad Request' data: type: string - example: "userId query parameter is required" + example: 'userId query parameter is required' ErrorResponse401: type: object properties: @@ -7243,7 +7284,7 @@ components: properties: message: type: string - example: "Not Found" + example: 'Not Found' data: type: string ErrorResponse500: @@ -7251,17 +7292,17 @@ components: properties: message: type: string - example: "Internal Server Error" + example: 'Internal Server Error' error: type: string SlackNotificationRoutingType: type: string enum: - - "Membership and roles" - - "Projects and organizations" - - "Policy reminders and status" - - "Evidence and task alerts" - - "Control or policy changes" + - 'Membership and roles' + - 'Projects and organizations' + - 'Policy reminders and status' + - 'Evidence and task alerts' + - 'Control or policy changes' SlackWebhookFull: type: object properties: @@ -7274,19 +7315,19 @@ components: type: string scope: type: string - example: "incoming-webhook,chat:write" + example: 'incoming-webhook,chat:write' user_id: type: integer example: 5 team_name: type: string - example: "Acme Corp" + example: 'Acme Corp' team_id: type: string example: T01234ABC channel: type: string - example: "#general" + example: '#general' channel_id: type: string example: C01234ABC @@ -7303,7 +7344,7 @@ components: routing_type: type: array items: - $ref: "#/components/schemas/SlackNotificationRoutingType" + $ref: '#/components/schemas/SlackNotificationRoutingType' nullable: true created_at: type: string @@ -7336,7 +7377,7 @@ components: routing_type: type: array items: - $ref: "#/components/schemas/SlackNotificationRoutingType" + $ref: '#/components/schemas/SlackNotificationRoutingType' nullable: true CreateSlackWebhookRequest: type: object @@ -7359,7 +7400,7 @@ components: routing_type: type: array items: - $ref: "#/components/schemas/SlackNotificationRoutingType" + $ref: '#/components/schemas/SlackNotificationRoutingType' SendSlackMessageRequest: type: object required: @@ -7368,10 +7409,10 @@ components: properties: title: type: string - example: "New policy update" + example: 'New policy update' message: type: string - example: "Policy *Data Retention* has been updated." + example: 'Policy *Data Retention* has been updated.' SlackMessageSentResult: type: object properties: @@ -7380,7 +7421,7 @@ components: example: true messageId: type: string - example: "1234567890.123456" + example: '1234567890.123456' channel: type: string example: C01234ABC @@ -7392,7 +7433,7 @@ components: example: true token_name: type: string - example: "GitHub Personal Access Token" + example: 'GitHub Personal Access Token' last_used_at: type: string format: date-time @@ -7410,8 +7451,8 @@ components: example: ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx token_name: type: string - default: "GitHub Personal Access Token" - example: "CI/CD Scanner Token" + default: 'GitHub Personal Access Token' + example: 'CI/CD Scanner Token' TestGitHubTokenRequest: type: object required: @@ -7432,7 +7473,7 @@ components: type: string example: - repo - - "read:org" + - 'read:org' rate_limit: type: object properties: @@ -7447,7 +7488,7 @@ components: format: date-time error: type: string - example: "Invalid or expired token" + example: 'Invalid or expired token' GitHubWebhookPayload: type: object required: @@ -7485,7 +7526,7 @@ components: type: boolean reason: type: string - example: "Scan triggered for push to main" + example: 'Scan triggered for push to main' GitHubWebhookPong: type: object properties: @@ -7497,7 +7538,7 @@ components: properties: message: type: string - example: "Repository not registered or CI not enabled" + example: 'Repository not registered or CI not enabled' SuccessResponse_SlackWebhookArray: type: object properties: @@ -7507,7 +7548,7 @@ components: data: type: array items: - $ref: "#/components/schemas/SlackWebhookFull" + $ref: '#/components/schemas/SlackWebhookFull' SuccessResponse_SlackWebhookJSON: type: object properties: @@ -7515,7 +7556,7 @@ components: type: string example: OK data: - $ref: "#/components/schemas/SlackWebhookJSON" + $ref: '#/components/schemas/SlackWebhookJSON' CreatedResponse_SlackWebhookFull: type: object properties: @@ -7523,7 +7564,7 @@ components: type: string example: Created data: - $ref: "#/components/schemas/SlackWebhookFull" + $ref: '#/components/schemas/SlackWebhookFull' AcceptedResponse_SlackWebhookFull: type: object properties: @@ -7531,7 +7572,7 @@ components: type: string example: Accepted data: - $ref: "#/components/schemas/SlackWebhookFull" + $ref: '#/components/schemas/SlackWebhookFull' SuccessResponse_SlackMessageSent: type: object properties: @@ -7539,7 +7580,7 @@ components: type: string example: OK data: - $ref: "#/components/schemas/SlackMessageSentResult" + $ref: '#/components/schemas/SlackMessageSentResult' SuccessResponse_GitHubTokenStatus: type: object properties: @@ -7547,7 +7588,7 @@ components: type: string example: OK data: - $ref: "#/components/schemas/GitHubTokenStatus" + $ref: '#/components/schemas/GitHubTokenStatus' CreatedResponse_GitHubTokenStatus: type: object properties: @@ -7555,7 +7596,7 @@ components: type: string example: Created data: - $ref: "#/components/schemas/GitHubTokenStatus" + $ref: '#/components/schemas/GitHubTokenStatus' SuccessResponse_GitHubTokenDeleted: type: object properties: @@ -7567,7 +7608,7 @@ components: properties: message: type: string - example: "GitHub token deleted successfully" + example: 'GitHub token deleted successfully' SuccessResponse_GitHubTokenTest: type: object properties: @@ -7575,151 +7616,231 @@ components: type: string example: OK data: - $ref: "#/components/schemas/GitHubTokenTestResult" + $ref: '#/components/schemas/GitHubTokenTestResult' tags: - - name: "AI Advisor" - - name: "AI Approval Rules" - - name: "AI Approvals" - - name: "AI Apps" - - name: "AI Audit" - - name: "AI Confirmation" - - name: "AI Content" - - name: "AI Detection" - - name: "AI Trust Centre" - - name: "Agent Discovery" - - name: "Ai Trust Index" - - name: "Approval Workflows" - - name: Assessments - - name: Audit - - name: Authentication - - name: Automations - - name: "CE Marking" - - name: "Change History" - - name: Compliance - - name: "Custom Fields" - - name: Dashboard - - name: Datasets - - name: Deadlines - - name: "Demo Data" - - name: "EU AI Act" - - name: "Entity Graph" - - name: Evidence - - name: "Evidence AI" - - name: FRIA - - name: Files - - name: Frameworks - - name: "Governance OS" - - name: "ISO 27001" - - name: "ISO 42001" - - name: Incidents - - name: "Intake Forms" - - name: Integrations - - name: Internal - - name: Invitations - - name: "LLM Evals" - - name: "LLM Keys" - - name: Mail - - name: "Model Inventory" - - name: "Model Risks" - - name: "NIST AI RMF" - - name: Notes - - name: Notifications - - name: Organizations - - name: Plugins - - name: Policies - - name: "Post-Market Monitoring" - - name: "Project Risks" - - name: Projects - - name: "Quantitative Risks" - - name: Readiness - - name: Reporting - - name: "Risk Benchmarks" - - name: "Risk History" - - name: Roles - - name: "SSO Config" - - name: Search - - name: Settings - - name: "Shadow AI" - - name: "Share Links" - - name: Subscriptions - - name: "Super Admin" - - name: System - - name: Tasks - - name: Training - - name: Users - - name: "Vendor Risks" - - name: Vendors - - name: Webhooks + - + name: 'AI Advisor' + - + name: 'AI Approval Rules' + - + name: 'AI Approvals' + - + name: 'AI Apps' + - + name: 'AI Audit' + - + name: 'AI Confirmation' + - + name: 'AI Content' + - + name: 'AI Detection' + - + name: 'AI Trust Centre' + - + name: 'Agent Discovery' + - + name: 'Ai Trust Index' + - + name: 'Approval Workflows' + - + name: Assessments + - + name: Audit + - + name: Authentication + - + name: Automations + - + name: 'CE Marking' + - + name: 'Change History' + - + name: Compliance + - + name: 'Custom Fields' + - + name: Dashboard + - + name: Datasets + - + name: Deadlines + - + name: 'Demo Data' + - + name: 'EU AI Act' + - + name: 'Entity Graph' + - + name: Evidence + - + name: 'Evidence AI' + - + name: FRIA + - + name: Files + - + name: Frameworks + - + name: 'Governance OS' + - + name: 'ISO 27001' + - + name: 'ISO 42001' + - + name: Incidents + - + name: 'Intake Forms' + - + name: Integrations + - + name: Internal + - + name: Invitations + - + name: 'LLM Evals' + - + name: 'LLM Keys' + - + name: Mail + - + name: 'Model Inventory' + - + name: 'Model Risks' + - + name: 'NIST AI RMF' + - + name: Notes + - + name: Notifications + - + name: Organizations + - + name: Plugins + - + name: Policies + - + name: 'Post-Market Monitoring' + - + name: 'Project Risks' + - + name: Projects + - + name: 'Quantitative Risks' + - + name: Readiness + - + name: 'Regulations Tracker' + - + name: Reporting + - + name: 'Risk Benchmarks' + - + name: 'Risk History' + - + name: Roles + - + name: 'SSO Config' + - + name: Search + - + name: Settings + - + name: 'Shadow AI' + - + name: 'Share Links' + - + name: Subscriptions + - + name: 'Super Admin' + - + name: System + - + name: Tasks + - + name: Training + - + name: Users + - + name: 'Vendor Risks' + - + name: Vendors + - + name: Webhooks paths: /users: get: - summary: "List all users in organization" + summary: 'List all users in organization' description: "Returns all users belonging to the authenticated user's organization,\nordered by created_at DESC, id ASC. Password hashes are excluded.\n" tags: - Users security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": - description: "Users found" + '200': + description: 'Users found' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: { type: array, items: { $ref: "#/components/schemas/UserSafe" } } - "204": - description: "No users found (empty organization)" - "500": - description: "Internal server error" + message: {type: string, example: OK} + data: {type: array, items: {$ref: '#/components/schemas/UserSafe'}} + '204': + description: 'No users found (empty organization)' + '500': + description: 'Internal server error' operationId: getAllUsers - "/users/{id}": + '/users/{id}': get: - summary: "Get user by ID" + summary: 'Get user by ID' description: "Retrieves a single user by their numeric ID. Super-admins can access\nany user; regular users can only access users within their organization\n(or their own record).\n" tags: - Users security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - in: path + - + in: path name: id required: true schema: type: integer - description: "User ID" + description: 'User ID' responses: - "200": - description: "User found" + '200': + description: 'User found' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: { $ref: "#/components/schemas/UserSafe" } - "403": - description: "Access denied (user belongs to different organization)" - "404": - description: "User not found" - "500": - description: "Internal server error" + message: {type: string, example: OK} + data: {$ref: '#/components/schemas/UserSafe'} + '403': + description: 'Access denied (user belongs to different organization)' + '404': + description: 'User not found' + '500': + description: 'Internal server error' operationId: getUserById patch: - summary: "Update user by ID" + summary: 'Update user by ID' description: "Updates user fields (name, surname, email, roleId, last_login).\nOnly provided fields are updated. Organization isolation enforced.\nSends Slack notification on role change. Sends email notification\nwhen role changes from Editor (3) to Admin (1).\n" tags: - Users security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - in: path + - + in: path name: id required: true schema: type: integer - description: "User ID to update" + description: 'User ID to update' requestBody: required: true content: @@ -7729,11 +7850,11 @@ paths: properties: name: type: string - description: "First name" + description: 'First name' example: Jane surname: type: string - description: "Last name" + description: 'Last name' example: Smith email: type: string @@ -7741,70 +7862,72 @@ paths: example: jane@example.com roleId: type: integer - description: "New role ID (1=Admin, 2=Reviewer, 3=Editor, 4=Auditor). Can be sent as string." + description: 'New role ID (1=Admin, 2=Reviewer, 3=Editor, 4=Auditor). Can be sent as string.' example: 1 last_login: type: string format: date-time - description: "Override last login timestamp" - description: "All fields are optional; only provided fields are updated" + description: 'Override last login timestamp' + description: 'All fields are optional; only provided fields are updated' responses: - "202": - description: "User updated" + '202': + description: 'User updated' content: application/json: schema: type: object properties: - message: { type: string, example: Accepted } - data: { $ref: "#/components/schemas/UserSafe" } - "400": - description: "Validation error" - "403": - description: "Access denied or business logic error" - "404": - description: "User not found" - "500": - description: "Internal server error" + message: {type: string, example: Accepted} + data: {$ref: '#/components/schemas/UserSafe'} + '400': + description: 'Validation error' + '403': + description: 'Access denied or business logic error' + '404': + description: 'User not found' + '500': + description: 'Internal server error' operationId: updateUserById delete: - summary: "Delete user by ID" + summary: 'Delete user by ID' description: "Deletes a user and nullifies all their foreign key references across\nprojects, vendors, risks, vendor risks, files, automations, and invitations.\nAlso removes the user from projects_members. Demo users and super-admins\ncannot be deleted.\n" tags: - Users security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - in: path + - + in: path name: id required: true schema: type: integer - description: "User ID to delete" + description: 'User ID to delete' responses: - "202": - description: "User deleted" + '202': + description: 'User deleted' content: application/json: schema: type: object properties: - message: { type: string, example: Accepted } - data: { type: boolean, description: "true if deletion succeeded" } - "403": - description: "Forbidden: demo user, super-admin, or wrong organization" + message: {type: string, example: Accepted} + data: {type: boolean, description: 'true if deletion succeeded'} + '403': + description: 'Forbidden: demo user, super-admin, or wrong organization' content: application/json: schema: - $ref: "#/components/schemas/ErrorEnvelope" - "404": - description: "User not found" - "500": - description: "Internal server error" + $ref: '#/components/schemas/ErrorEnvelope' + '404': + description: 'User not found' + '500': + description: 'Internal server error' operationId: deleteUserById /users/register: post: - summary: "Register a new user" + summary: 'Register a new user' description: "Creates a new user account. Requires a valid registration JWT (set by registerJWT middleware).\nValidates email uniqueness, password strength, and required fields.\nMarks any pending invitation as accepted after successful creation.\n" tags: - Users @@ -7824,11 +7947,11 @@ paths: properties: name: type: string - description: "User's first name" + description: 'User''s first name' example: John surname: type: string - description: "User's last name" + description: 'User''s last name' example: Doe email: type: string @@ -7837,46 +7960,46 @@ paths: password: type: string format: password - description: "Must be 8+ chars with uppercase, lowercase, and digit" + description: 'Must be 8+ chars with uppercase, lowercase, and digit' example: SecurePassword123! roleId: type: integer - description: "1=Admin, 2=Reviewer, 3=Editor, 4=Auditor" + description: '1=Admin, 2=Reviewer, 3=Editor, 4=Auditor' example: 3 organizationId: type: integer - description: "Organization to assign the user to" + description: 'Organization to assign the user to' example: 1 responses: - "201": - description: "User created successfully" + '201': + description: 'User created successfully' content: application/json: schema: type: object properties: - message: { type: string, example: Created } - data: { $ref: "#/components/schemas/UserSafe" } - "400": - description: "Validation error (missing fields, weak password, invalid email)" + message: {type: string, example: Created} + data: {$ref: '#/components/schemas/UserSafe'} + '400': + description: 'Validation error (missing fields, weak password, invalid email)' content: application/json: schema: - $ref: "#/components/schemas/ErrorEnvelope" - "403": - description: "Business logic error" - "409": - description: "User with this email already exists" + $ref: '#/components/schemas/ErrorEnvelope' + '403': + description: 'Business logic error' + '409': + description: 'User with this email already exists' content: application/json: schema: - $ref: "#/components/schemas/ErrorEnvelope" - "500": - description: "Internal server error" + $ref: '#/components/schemas/ErrorEnvelope' + '500': + description: 'Internal server error' operationId: createNewUser /users/login: post: - summary: "Authenticate user" + summary: 'Authenticate user' description: "Validates email/password credentials via bcrypt. Returns a JWT access token\nin the response body and sets a refresh token in an HTTP-only cookie.\nRate-limited to 5 requests per minute per IP.\n" tags: - Users @@ -7899,115 +8022,85 @@ paths: format: password example: SecurePassword123! responses: - "202": - description: "Authentication successful" + '202': + description: 'Authentication successful' headers: Set-Cookie: schema: type: string - description: "refresh_token=; Path=/api/users; HttpOnly; Secure (prod); SameSite=none (prod) / lax (dev)" + description: 'refresh_token=; Path=/api/users; HttpOnly; Secure (prod); SameSite=none (prod) / lax (dev)' content: application/json: schema: type: object properties: - message: { type: string, example: Accepted } - data: - { - type: object, - properties: - { - token: { type: string, description: "JWT access token" }, - isSuperAdmin: - { - type: boolean, - description: "Only present when user is super-admin (role_id=5)", - }, - onboarding_status: - { - type: string, - description: "Organization onboarding status (not present for super-admin)", - example: completed, - }, - is_org_creator: - { - type: boolean, - description: "Whether user is the first admin of the org (not present for super-admin)", - }, - }, - } - "401": - description: "Invalid email or password" + message: {type: string, example: Accepted} + data: {type: object, properties: {token: {type: string, description: 'JWT access token'}, isSuperAdmin: {type: boolean, description: 'Only present when user is super-admin (role_id=5)'}, onboarding_status: {type: string, description: 'Organization onboarding status (not present for super-admin)', example: completed}, is_org_creator: {type: boolean, description: 'Whether user is the first admin of the org (not present for super-admin)'}}} + '401': + description: 'Invalid email or password' content: application/json: schema: - $ref: "#/components/schemas/ErrorEnvelope" - "429": - description: "Too many login attempts" - "500": - description: "Internal server error" + $ref: '#/components/schemas/ErrorEnvelope' + '429': + description: 'Too many login attempts' + '500': + description: 'Internal server error' operationId: loginUser /users/login-microsoft: post: - summary: "Login User With Microsoft" + summary: 'Login User With Microsoft' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - Users operationId: loginUserWithMicrosoft /users/refresh-token: post: - summary: "Refresh access token" + summary: 'Refresh access token' description: "Reads the refresh_token from an HTTP-only cookie and issues a new\nJWT access token if the refresh token is still valid.\n" tags: - Users parameters: - - in: cookie + - + in: cookie name: refresh_token required: true schema: type: string - description: "JWT refresh token set during login" + description: 'JWT refresh token set during login' responses: - "200": - description: "New access token issued" + '200': + description: 'New access token issued' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: - { - type: object, - properties: { token: { type: string, description: "New JWT access token" } }, - } - "400": - description: "Refresh token missing from cookie" - "401": - description: "Invalid refresh token" - "406": - description: "Refresh token expired" + message: {type: string, example: OK} + data: {type: object, properties: {token: {type: string, description: 'New JWT access token'}}} + '400': + description: 'Refresh token missing from cookie' + '401': + description: 'Invalid refresh token' + '406': + description: 'Refresh token expired' content: application/json: schema: type: object properties: - message: { type: string } - data: - { - type: object, - properties: { message: { type: string, example: "Token expired" } }, - } - "500": - description: "Internal server error" + message: {type: string} + data: {type: object, properties: {message: {type: string, example: 'Token expired'}}} + '500': + description: 'Internal server error' operationId: refreshAccessToken /users/reset-password: post: - summary: "Reset user password" + summary: 'Reset user password' description: "Resets the password for a user identified by email. Protected by\nresetPasswordMiddleware (validates reset token/permission).\nPassword is hashed via bcrypt before storage.\n" tags: - Users @@ -8028,42 +8121,44 @@ paths: newPassword: type: string format: password - description: "Must be 8+ chars with uppercase, lowercase, and digit" + description: 'Must be 8+ chars with uppercase, lowercase, and digit' example: NewSecure123! responses: - "202": - description: "Password reset successfully" + '202': + description: 'Password reset successfully' content: application/json: schema: type: object properties: - message: { type: string, example: Accepted } - data: { $ref: "#/components/schemas/UserSafe" } - "400": - description: "Validation error (weak password)" - "403": - description: "Business logic error" - "404": - description: "User not found" - "500": - description: "Internal server error" + message: {type: string, example: Accepted} + data: {$ref: '#/components/schemas/UserSafe'} + '400': + description: 'Validation error (weak password)' + '403': + description: 'Business logic error' + '404': + description: 'User not found' + '500': + description: 'Internal server error' operationId: resetPassword - "/users/chng-pass/{id}": + '/users/chng-pass/{id}': patch: - summary: "Change password (authenticated)" + summary: 'Change password (authenticated)' description: "Changes the password for the authenticated user. Requires the current\npassword for verification. Protected by selfOnly middleware (users can\nonly change their own password). Rate-limited.\n" tags: - Users security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - in: path + - + in: path name: id required: true schema: type: integer - description: "User ID (must match authenticated user via selfOnly middleware)" + description: 'User ID (must match authenticated user via selfOnly middleware)' requestBody: required: true content: @@ -8077,126 +8172,131 @@ paths: properties: id: type: integer - description: "User ID (must match path parameter)" + description: 'User ID (must match path parameter)' example: 1 currentPassword: type: string format: password - description: "Current password for verification" + description: 'Current password for verification' example: OldPassword123! newPassword: type: string format: password - description: "Must be 8+ chars with uppercase, lowercase, and digit" + description: 'Must be 8+ chars with uppercase, lowercase, and digit' example: NewPassword456! responses: - "202": - description: "Password changed successfully" + '202': + description: 'Password changed successfully' content: application/json: schema: type: object properties: - message: { type: string, example: "Password updated successfully" } - data: { $ref: "#/components/schemas/UserSafe" } - "400": - description: "Validation error (weak password, missing fields)" + message: {type: string, example: 'Password updated successfully'} + data: {$ref: '#/components/schemas/UserSafe'} + '400': + description: 'Validation error (weak password, missing fields)' content: application/json: schema: type: object properties: - message: { type: string } - "403": - description: "Business logic error (wrong current password)" + message: {type: string} + '403': + description: 'Business logic error (wrong current password)' content: application/json: schema: type: object properties: - message: { type: string } - "404": - description: "User not found" + message: {type: string} + '404': + description: 'User not found' content: application/json: schema: type: object properties: - message: { type: string, example: "User not found" } - "500": - description: "Internal server error" + message: {type: string, example: 'User not found'} + '500': + description: 'Internal server error' operationId: ChangePassword /users/check/exists: get: - summary: "Check if any user exists" + summary: 'Check if any user exists' description: "Returns a boolean indicating whether any user record exists in the database.\nUsed during initial setup flow to determine if onboarding is needed.\n" tags: - Users security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": - description: "Check result" + '200': + description: 'Check result' content: application/json: schema: type: boolean example: true - "500": - description: "Internal server error" + '500': + description: 'Internal server error' content: application/json: schema: type: object properties: - message: { type: string, example: "Internal server error" } + message: {type: string, example: 'Internal server error'} operationId: checkUserExists - "/users/{id}/calculate-progress": + '/users/{id}/calculate-progress': get: - summary: "Calculate user project progress" + summary: 'Calculate user project progress' description: "Computes completion metrics across all projects the user is a member of.\nCalculates subcontrol completion (status=\"Done\") and assessment question\ncompletion (has answer) per project and as aggregated totals.\n" tags: - Users security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - in: path + - + in: path name: id required: true schema: type: integer - description: "User ID to calculate progress for" + description: 'User ID to calculate progress for' responses: - "200": - description: "Progress calculated" + '200': + description: 'Progress calculated' content: application/json: schema: - $ref: "#/components/schemas/ProgressResponse" - "500": - description: "Internal server error" + $ref: '#/components/schemas/ProgressResponse' + '500': + description: 'Internal server error' content: application/json: schema: type: object properties: - message: { type: string, example: "Internal server error" } + message: {type: string, example: 'Internal server error'} operationId: calculateProgress - "/users/{id}/profile-photo": + '/users/{id}/profile-photo': post: - summary: "Upload profile photo" + summary: 'Upload profile photo' description: "Uploads a profile photo for the specified user. The file is stored in the\ntenant-scoped files table. If the user already has a profile photo, the old\none is deleted and replaced. Uses multer for multipart file handling.\n" tags: - Users security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - in: path + - + in: path name: id required: true schema: type: integer - description: "User ID" + description: 'User ID' requestBody: required: true content: @@ -8209,116 +8309,102 @@ paths: photo: type: string format: binary - description: "Image file (JPEG, PNG, etc.)" + description: 'Image file (JPEG, PNG, etc.)' responses: - "200": - description: "Profile photo uploaded" + '200': + description: 'Profile photo uploaded' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: - { - type: object, - properties: - { - profile_photo_id: - { type: integer, description: "ID of the new file record" }, - }, - } - "400": - description: "No file provided" - "403": - description: "Access denied (wrong organization)" - "500": - description: "Internal server error" + message: {type: string, example: OK} + data: {type: object, properties: {profile_photo_id: {type: integer, description: 'ID of the new file record'}}} + '400': + description: 'No file provided' + '403': + description: 'Access denied (wrong organization)' + '500': + description: 'Internal server error' operationId: uploadUserProfilePhoto get: - summary: "Get profile photo" + summary: 'Get profile photo' description: "Returns the profile photo binary content for the specified user.\nThe response includes the raw file content and its MIME type.\n" tags: - Users security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - in: path + - + in: path name: id required: true schema: type: integer - description: "User ID" + description: 'User ID' responses: - "200": - description: "Profile photo returned" + '200': + description: 'Profile photo returned' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: - { - type: object, - properties: - { - content: - { - type: string, - format: byte, - description: "Base64-encoded file content", - }, - type: { type: string, description: "MIME type", example: image/png }, - }, - } - "404": - description: "No profile photo found" - "500": - description: "Internal server error" + message: {type: string, example: OK} + data: {type: object, properties: {content: {type: string, format: byte, description: 'Base64-encoded file content'}, type: {type: string, description: 'MIME type', example: image/png}}} + '404': + description: 'No profile photo found' + '500': + description: 'Internal server error' operationId: getUserProfilePhoto delete: - summary: "Delete profile photo" + summary: 'Delete profile photo' description: "Removes the profile photo from the user record and deletes the associated\nfile from the files table.\n" tags: - Users security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - in: path + - + in: path name: id required: true schema: type: integer - description: "User ID" + description: 'User ID' responses: - "200": - description: "Profile photo deleted" + '200': + description: 'Profile photo deleted' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: { type: boolean, description: "true if photo was deleted" } - "500": - description: "Internal server error" + message: {type: string, example: OK} + data: {type: boolean, description: 'true if photo was deleted'} + '500': + description: 'Internal server error' operationId: deleteUserProfilePhoto - "/vendorRisks/by-projid/{id}": + '/vendorRisks/by-projid/{id}': get: tags: - - "Vendor Risks" - summary: "Get All Vendor Risks" + - 'Vendor Risks' + summary: 'Get All Vendor Risks' operationId: getAllVendorRisks security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: string - - name: filter + - + name: filter in: query required: false schema: @@ -8329,34 +8415,37 @@ paths: - all default: active responses: - "200": + '200': description: Success content: application/json: schema: type: object properties: - message: { type: string } - data: { type: array, items: { $ref: "#/components/schemas/VendorRiskResponse" } } - "401": + message: {type: string} + data: {type: array, items: {$ref: '#/components/schemas/VendorRiskResponse'}} + '401': description: Unauthorized - "500": - description: "Internal server error" - "/vendorRisks/by-vendorid/{id}": + '500': + description: 'Internal server error' + '/vendorRisks/by-vendorid/{id}': get: tags: - - "Vendor Risks" - summary: "Get All Vendor Risks By Vendor Id" + - 'Vendor Risks' + summary: 'Get All Vendor Risks By Vendor Id' operationId: getAllVendorRisksByVendorId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer - - name: filter + - + name: filter in: query required: false schema: @@ -8367,42 +8456,45 @@ paths: - all default: active responses: - "200": + '200': description: Success content: application/json: schema: type: object properties: - message: { type: string } - data: { type: array, items: { $ref: "#/components/schemas/VendorRiskResponse" } } - "401": + message: {type: string} + data: {type: array, items: {$ref: '#/components/schemas/VendorRiskResponse'}} + '401': description: Unauthorized - "500": - description: "Internal server error" - "/vendorRisks/by-frameworkid/{id}": + '500': + description: 'Internal server error' + '/vendorRisks/by-frameworkid/{id}': get: - summary: "Get Vendor Risks By Framework Id" + summary: 'Get Vendor Risks By Framework Id' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Vendor Risks" + - 'Vendor Risks' operationId: getVendorRisksByFrameworkId security: - - bearerAuth: [] + - + bearerAuth: [] /vendorRisks/all: get: tags: - - "Vendor Risks" - summary: "Get All Vendor Risks All Projects" + - 'Vendor Risks' + summary: 'Get All Vendor Risks All Projects' operationId: getAllVendorRisksAllProjects security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: filter + - + name: filter in: query required: false schema: @@ -8413,60 +8505,60 @@ paths: - all default: active responses: - "200": + '200': description: Success content: application/json: schema: type: object properties: - message: { type: string } - data: - { - type: array, - items: { $ref: "#/components/schemas/VendorRiskAllProjectsResponse" }, - } - "401": + message: {type: string} + data: {type: array, items: {$ref: '#/components/schemas/VendorRiskAllProjectsResponse'}} + '401': description: Unauthorized - "500": - description: "Internal server error" - "/vendorRisks/{id}": + '500': + description: 'Internal server error' + '/vendorRisks/{id}': get: tags: - - "Vendor Risks" - summary: "Get Vendor Risk By Id" + - 'Vendor Risks' + summary: 'Get Vendor Risk By Id' operationId: getVendorRiskById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success content: application/json: schema: type: object properties: - message: { type: string } - data: { $ref: "#/components/schemas/VendorRiskResponse" } - "401": + message: {type: string} + data: {$ref: '#/components/schemas/VendorRiskResponse'} + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' patch: tags: - - "Vendor Risks" - summary: "Update Vendor Risk By Id" + - 'Vendor Risks' + summary: 'Update Vendor Risk By Id' operationId: updateVendorRiskById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -8476,898 +8568,928 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/VendorRiskInput" + $ref: '#/components/schemas/VendorRiskInput' responses: - "202": + '202': description: Accepted content: application/json: schema: type: object properties: - message: { type: string } - data: { $ref: "#/components/schemas/VendorRiskResponse" } - "401": + message: {type: string} + data: {$ref: '#/components/schemas/VendorRiskResponse'} + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - - "Vendor Risks" - summary: "Delete Vendor Risk By Id" + - 'Vendor Risks' + summary: 'Delete Vendor Risk By Id' operationId: deleteVendorRiskById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "202": + '202': description: Accepted content: application/json: schema: type: object properties: - message: { type: string } - data: { $ref: "#/components/schemas/VendorRiskResponse" } - "401": + message: {type: string} + data: {$ref: '#/components/schemas/VendorRiskResponse'} + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /vendorRisks: post: tags: - - "Vendor Risks" - summary: "Create Vendor Risk" + - 'Vendor Risks' + summary: 'Create Vendor Risk' operationId: createVendorRisk security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/VendorRiskInput" + $ref: '#/components/schemas/VendorRiskInput' responses: - "201": - description: "Created successfully" + '201': + description: 'Created successfully' content: application/json: schema: type: object properties: - message: { type: string } - data: { $ref: "#/components/schemas/VendorRiskResponse" } - "401": + message: {type: string} + data: {$ref: '#/components/schemas/VendorRiskResponse'} + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /vendors: get: tags: - Vendors - summary: "Get all vendors" - description: "Retrieves all vendors for the authenticated user's organization, ordered by creation date descending. Each vendor includes its associated project IDs and the reviewer's full name." + summary: 'Get all vendors' + description: 'Retrieves all vendors for the authenticated user''s organization, ordered by creation date descending. Each vendor includes its associated project IDs and the reviewer''s full name.' security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": - description: "Vendors retrieved successfully" + '200': + description: 'Vendors retrieved successfully' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: - { type: array, items: { $ref: "#/components/schemas/VendorWithReviewerName" } } - "204": - description: "No vendors found" + message: {type: string, example: OK} + data: {type: array, items: {$ref: '#/components/schemas/VendorWithReviewerName'}} + '204': + description: 'No vendors found' content: application/json: schema: type: object properties: - message: { type: string, example: "No Content" } - data: { type: "null" } - "401": - description: "Unauthorized - missing or invalid JWT" - "500": - description: "Internal server error" + message: {type: string, example: 'No Content'} + data: {type: 'null'} + '401': + description: 'Unauthorized - missing or invalid JWT' + '500': + description: 'Internal server error' content: application/json: schema: type: object properties: - message: { type: string, example: "Internal Server Error" } - error: { type: string } + message: {type: string, example: 'Internal Server Error'} + error: {type: string} operationId: getAllVendors post: tags: - Vendors - summary: "Create a vendor" + summary: 'Create a vendor' description: "Creates a new vendor in the authenticated user's organization.\nValidates required fields, checks demo restrictions, associates\nprojects via the vendors_projects join table, records creation\nin change history, fires automation triggers (vendor_added),\nand sends in-app assignment notifications to assignee and reviewer.\n" security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/VendorInput" + $ref: '#/components/schemas/VendorInput' responses: - "201": - description: "Vendor created successfully" + '201': + description: 'Vendor created successfully' content: application/json: schema: type: object properties: - message: { type: string, example: Created } - data: { $ref: "#/components/schemas/Vendor" } - "400": - description: "Validation error (missing or invalid required fields)" + message: {type: string, example: Created} + data: {$ref: '#/components/schemas/Vendor'} + '400': + description: 'Validation error (missing or invalid required fields)' content: application/json: schema: type: object properties: - message: { type: string, example: "Bad Request" } - data: { type: string, description: "Validation error message" } - "401": - description: "Unauthorized - missing or invalid JWT" - "403": - description: "Business logic error (e.g. demo vendor restriction)" + message: {type: string, example: 'Bad Request'} + data: {type: string, description: 'Validation error message'} + '401': + description: 'Unauthorized - missing or invalid JWT' + '403': + description: 'Business logic error (e.g. demo vendor restriction)' content: application/json: schema: type: object properties: - message: { type: string, example: Forbidden } - data: { type: string } - "500": - description: "Internal server error" + message: {type: string, example: Forbidden} + data: {type: string} + '500': + description: 'Internal server error' content: application/json: schema: type: object properties: - message: { type: string, example: "Internal Server Error" } - error: { type: string } - "503": - description: "Service unavailable - vendor creation returned null" + message: {type: string, example: 'Internal Server Error'} + error: {type: string} + '503': + description: 'Service unavailable - vendor creation returned null' content: application/json: schema: type: object properties: - message: { type: string, example: "Service Unavailable" } - error: { type: object } + message: {type: string, example: 'Service Unavailable'} + error: {type: object} operationId: createVendor - "/vendors/project-id/{id}": + '/vendors/project-id/{id}': get: tags: - Vendors - summary: "Get vendors by project ID" - description: "Retrieves all vendors associated with a specific project. Returns 404 if the project does not exist." + summary: 'Get vendors by project ID' + description: 'Retrieves all vendors associated with a specific project. Returns 404 if the project does not exist.' security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer - description: "Project ID" + description: 'Project ID' responses: - "200": - description: "Vendors retrieved successfully" + '200': + description: 'Vendors retrieved successfully' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: { type: array, items: { $ref: "#/components/schemas/Vendor" } } - "401": - description: "Unauthorized - missing or invalid JWT" - "404": - description: "Project not found" + message: {type: string, example: OK} + data: {type: array, items: {$ref: '#/components/schemas/Vendor'}} + '401': + description: 'Unauthorized - missing or invalid JWT' + '404': + description: 'Project not found' content: application/json: schema: type: object properties: - message: { type: string, example: "Not Found" } - data: { type: array, items: {} } - "500": - description: "Internal server error" + message: {type: string, example: 'Not Found'} + data: {type: array, items: {}} + '500': + description: 'Internal server error' content: application/json: schema: type: object properties: - message: { type: string, example: "Internal Server Error" } - error: { type: string } + message: {type: string, example: 'Internal Server Error'} + error: {type: string} operationId: getVendorByProjectId - "/vendors/{id}": + '/vendors/{id}': get: tags: - Vendors - summary: "Get vendor by ID" - description: "Retrieves a single vendor by its ID, including associated project IDs." + summary: 'Get vendor by ID' + description: 'Retrieves a single vendor by its ID, including associated project IDs.' security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer - description: "Vendor ID" + description: 'Vendor ID' responses: - "200": - description: "Vendor retrieved successfully" + '200': + description: 'Vendor retrieved successfully' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: { $ref: "#/components/schemas/Vendor" } - "401": - description: "Unauthorized - missing or invalid JWT" - "404": - description: "Vendor not found" + message: {type: string, example: OK} + data: {$ref: '#/components/schemas/Vendor'} + '401': + description: 'Unauthorized - missing or invalid JWT' + '404': + description: 'Vendor not found' content: application/json: schema: type: object properties: - message: { type: string, example: "Not Found" } - data: { type: "null" } - "500": - description: "Internal server error" + message: {type: string, example: 'Not Found'} + data: {type: 'null'} + '500': + description: 'Internal server error' content: application/json: schema: type: object properties: - message: { type: string, example: "Internal Server Error" } - error: { type: string } + message: {type: string, example: 'Internal Server Error'} + error: {type: string} operationId: getVendorById patch: tags: - Vendors - summary: "Update a vendor" + summary: 'Update a vendor' description: "Partially updates an existing vendor. Only provided fields are updated.\nReview and scorecard fields can be explicitly set to null to clear them.\nRequired fields (vendor_name, vendor_provides, website, vendor_contact_person)\nare only updated if they have a non-empty value.\nRecords field-level changes in change history, fires automation triggers\n(vendor_updated), and sends in-app notifications when assignee or reviewer changes.\n" security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer - description: "Vendor ID" + description: 'Vendor ID' requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/VendorUpdate" + $ref: '#/components/schemas/VendorUpdate' responses: - "202": - description: "Vendor updated successfully" + '202': + description: 'Vendor updated successfully' content: application/json: schema: type: object properties: - message: { type: string, example: Accepted } - data: { $ref: "#/components/schemas/Vendor" } - "400": - description: "Validation error" + message: {type: string, example: Accepted} + data: {$ref: '#/components/schemas/Vendor'} + '400': + description: 'Validation error' content: application/json: schema: type: object properties: - message: { type: string, example: "Bad Request" } - data: { type: string } - "401": - description: "Unauthorized - missing or invalid JWT, or missing userId/role" + message: {type: string, example: 'Bad Request'} + data: {type: string} + '401': + description: 'Unauthorized - missing or invalid JWT, or missing userId/role' content: application/json: schema: type: object properties: - message: { type: string, example: Unauthorized } - "403": - description: "Business logic error (e.g. demo vendor restriction)" + message: {type: string, example: Unauthorized} + '403': + description: 'Business logic error (e.g. demo vendor restriction)' content: application/json: schema: type: object properties: - message: { type: string, example: Forbidden } - data: { type: string } - "404": - description: "Vendor not found" + message: {type: string, example: Forbidden} + data: {type: string} + '404': + description: 'Vendor not found' content: application/json: schema: type: object properties: - message: { type: string, example: "Not Found" } - data: { type: object } - "500": - description: "Internal server error" + message: {type: string, example: 'Not Found'} + data: {type: object} + '500': + description: 'Internal server error' content: application/json: schema: type: object properties: - message: { type: string, example: "Internal Server Error" } - error: { type: string } + message: {type: string, example: 'Internal Server Error'} + error: {type: string} operationId: updateVendorById delete: tags: - Vendors - summary: "Delete a vendor" + summary: 'Delete a vendor' description: "Deletes a vendor and all associated data in a transaction:\n1. Deletes vendor risks (vendor_risks table)\n2. Deletes project associations (vendors_projects table)\n3. Deletes the vendor record itself\nFires automation triggers (vendor_deleted).\n" security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer - description: "Vendor ID" + description: 'Vendor ID' responses: - "202": - description: "Vendor deleted successfully" + '202': + description: 'Vendor deleted successfully' content: application/json: schema: type: object properties: - message: { type: string, example: Accepted } - data: { type: boolean, example: true } - "401": - description: "Unauthorized - missing or invalid JWT" - "404": - description: "Vendor not found" + message: {type: string, example: Accepted} + data: {type: boolean, example: true} + '401': + description: 'Unauthorized - missing or invalid JWT' + '404': + description: 'Vendor not found' content: application/json: schema: type: object properties: - message: { type: string, example: "Not Found" } - data: { type: object } - "500": - description: "Internal server error" + message: {type: string, example: 'Not Found'} + data: {type: object} + '500': + description: 'Internal server error' content: application/json: schema: type: object properties: - message: { type: string, example: "Internal Server Error" } - error: { type: string } + message: {type: string, example: 'Internal Server Error'} + error: {type: string} operationId: deleteVendorById - "/vendor-change-history/{id}": + '/vendor-change-history/{id}': get: tags: - - "Change History" - summary: "Get Vendor Change History By Id" + - 'Change History' + summary: 'Get Vendor Change History By Id' operationId: getVendorChangeHistoryById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /projects: get: - summary: "Get all projects" + summary: 'Get all projects' description: "Returns all projects visible to the authenticated user. Admins and SuperAdmins see all projects in the organization; other roles see only projects they own or are members of.\n" operationId: getAllProjects tags: - Projects security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": - description: "List of projects retrieved successfully" + '200': + description: 'List of projects retrieved successfully' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: { type: array, items: { $ref: "#/components/schemas/ProjectListItem" } } - "401": - description: "Unauthorized — missing or invalid JWT" + message: {type: string, example: OK} + data: {type: array, items: {$ref: '#/components/schemas/ProjectListItem'}} + '401': + description: 'Unauthorized — missing or invalid JWT' content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - "500": - description: "Internal server error" + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 'Internal server error' content: application/json: schema: - $ref: "#/components/schemas/ServerError" + $ref: '#/components/schemas/ServerError' post: - summary: "Create a new project (use case)" + summary: 'Create a new project (use case)' description: "Creates a new project with associated members and frameworks. If an approval_workflow_id is provided, framework creation is deferred until the approval request is approved.\n" operationId: createProject tags: - Projects security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/CreateProjectRequest" + $ref: '#/components/schemas/CreateProjectRequest' responses: - "201": - description: "Project created successfully" + '201': + description: 'Project created successfully' content: application/json: schema: type: object properties: - message: { type: string, example: Created } - data: - { - type: object, - properties: - { - project: { $ref: "#/components/schemas/Project" }, - frameworks: { type: object, additionalProperties: true }, - }, - } - "400": - description: "Validation error" + message: {type: string, example: Created} + data: {type: object, properties: {project: {$ref: '#/components/schemas/Project'}, frameworks: {type: object, additionalProperties: true}}} + '400': + description: 'Validation error' content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - "403": - description: "Business logic error (e.g. framework not allowed)" + $ref: '#/components/schemas/ErrorResponse' + '403': + description: 'Business logic error (e.g. framework not allowed)' content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - "500": - description: "Internal server error" + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 'Internal server error' content: application/json: schema: - $ref: "#/components/schemas/ServerError" - "503": - description: "Service unavailable — project creation returned null" + $ref: '#/components/schemas/ServerError' + '503': + description: 'Service unavailable — project creation returned null' content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - "/projects/calculateProjectRisks/{id}": + $ref: '#/components/schemas/ErrorResponse' + '/projects/calculateProjectRisks/{id}': get: - summary: "Calculate project risk distribution" + summary: 'Calculate project risk distribution' operationId: getProjectRisksCalculations tags: - Projects security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - $ref: "#/components/parameters/ProjectId" + - + $ref: '#/components/parameters/ProjectId' responses: - "200": - description: "Risk calculations returned" + '200': + description: 'Risk calculations returned' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: { type: array, items: { $ref: "#/components/schemas/ProjectRiskCount" } } - "204": - description: "No risk data available" + message: {type: string, example: OK} + data: {type: array, items: {$ref: '#/components/schemas/ProjectRiskCount'}} + '204': + description: 'No risk data available' content: application/json: schema: type: object properties: - message: { type: string, example: "No Content" } - data: { nullable: true } - "500": - description: "Internal server error" + message: {type: string, example: 'No Content'} + data: {nullable: true} + '500': + description: 'Internal server error' content: application/json: schema: - $ref: "#/components/schemas/ServerError" - "/projects/calculateVendorRisks/{id}": + $ref: '#/components/schemas/ServerError' + '/projects/calculateVendorRisks/{id}': get: - summary: "Calculate vendor risk distribution" + summary: 'Calculate vendor risk distribution' operationId: getVendorRisksCalculations tags: - Projects security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - $ref: "#/components/parameters/ProjectId" + - + $ref: '#/components/parameters/ProjectId' responses: - "200": - description: "Vendor risk calculations returned" + '200': + description: 'Vendor risk calculations returned' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: { type: array, items: { $ref: "#/components/schemas/VendorRiskCount" } } - "204": - description: "No vendor risk data available" + message: {type: string, example: OK} + data: {type: array, items: {$ref: '#/components/schemas/VendorRiskCount'}} + '204': + description: 'No vendor risk data available' content: application/json: schema: type: object properties: - message: { type: string, example: "No Content" } - data: { nullable: true } - "500": - description: "Internal server error" + message: {type: string, example: 'No Content'} + data: {nullable: true} + '500': + description: 'Internal server error' content: application/json: schema: - $ref: "#/components/schemas/ServerError" - "/projects/{id}": + $ref: '#/components/schemas/ServerError' + '/projects/{id}': get: - summary: "Get a project by ID" - description: "Returns a single project with its frameworks, owner name, members, and approval status." + summary: 'Get a project by ID' + description: 'Returns a single project with its frameworks, owner name, members, and approval status.' operationId: getProjectById tags: - Projects security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - $ref: "#/components/parameters/ProjectId" + - + $ref: '#/components/parameters/ProjectId' responses: - "200": - description: "Project found" + '200': + description: 'Project found' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: { $ref: "#/components/schemas/ProjectDetail" } - "404": - description: "Project not found" + message: {type: string, example: OK} + data: {$ref: '#/components/schemas/ProjectDetail'} + '404': + description: 'Project not found' content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - "500": - description: "Internal server error" + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 'Internal server error' content: application/json: schema: - $ref: "#/components/schemas/ServerError" + $ref: '#/components/schemas/ServerError' patch: - summary: "Update a project by ID" - description: "Partially updates a project and its member list. Only provided fields are updated." + summary: 'Update a project by ID' + description: 'Partially updates a project and its member list. Only provided fields are updated.' operationId: updateProjectById tags: - Projects security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - $ref: "#/components/parameters/ProjectId" + - + $ref: '#/components/parameters/ProjectId' requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/UpdateProjectRequest" + $ref: '#/components/schemas/UpdateProjectRequest' responses: - "202": - description: "Project updated successfully" + '202': + description: 'Project updated successfully' content: application/json: schema: type: object properties: - message: { type: string, example: Accepted } - data: { $ref: "#/components/schemas/ProjectWithMembers" } - "400": - description: "Validation error" + message: {type: string, example: Accepted} + data: {$ref: '#/components/schemas/ProjectWithMembers'} + '400': + description: 'Validation error' content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - "401": + $ref: '#/components/schemas/ErrorResponse' + '401': description: Unauthorized content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - "403": - description: "Business logic error" + $ref: '#/components/schemas/ErrorResponse' + '403': + description: 'Business logic error' content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - "404": - description: "Project not found" + $ref: '#/components/schemas/ErrorResponse' + '404': + description: 'Project not found' content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - "500": - description: "Internal server error" + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 'Internal server error' content: application/json: schema: - $ref: "#/components/schemas/ServerError" + $ref: '#/components/schemas/ServerError' delete: - summary: "Delete a project by ID" - description: "Deletes a project and all dependent entities (files, risks, members, framework data)." + summary: 'Delete a project by ID' + description: 'Deletes a project and all dependent entities (files, risks, members, framework data).' operationId: deleteProjectById tags: - Projects security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - $ref: "#/components/parameters/ProjectId" + - + $ref: '#/components/parameters/ProjectId' responses: - "202": - description: "Project deleted successfully" + '202': + description: 'Project deleted successfully' content: application/json: schema: type: object properties: - message: { type: string, example: Accepted } - data: { type: boolean, example: true } - "404": - description: "Project not found" + message: {type: string, example: Accepted} + data: {type: boolean, example: true} + '404': + description: 'Project not found' content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - "500": - description: "Internal server error" + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 'Internal server error' content: application/json: schema: - $ref: "#/components/schemas/ServerError" - "/projects/stats/{id}": + $ref: '#/components/schemas/ServerError' + '/projects/stats/{id}': get: - summary: "Get project statistics by ID" + summary: 'Get project statistics by ID' operationId: getProjectStatsById tags: - Projects security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - $ref: "#/components/parameters/ProjectId" + - + $ref: '#/components/parameters/ProjectId' responses: - "202": - description: "Project stats retrieved" + '202': + description: 'Project stats retrieved' content: application/json: schema: type: object properties: - message: { type: string, example: Accepted } - data: { $ref: "#/components/schemas/ProjectStats" } - "500": - description: "Internal server error" + message: {type: string, example: Accepted} + data: {$ref: '#/components/schemas/ProjectStats'} + '500': + description: 'Internal server error' content: application/json: schema: - $ref: "#/components/schemas/ServerError" - "/projects/complainces/{projid}": + $ref: '#/components/schemas/ServerError' + '/projects/complainces/{projid}': get: - summary: "Get compliance data for a project" + summary: 'Get compliance data for a project' operationId: getCompliances tags: - Projects security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: projid + - + name: projid in: path required: true - description: "The project ID" + description: 'The project ID' schema: type: integer responses: - "200": - description: "Compliance data returned" + '200': + description: 'Compliance data returned' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: { type: array, items: { $ref: "#/components/schemas/ControlCategory" } } - "404": - description: "Project not found" + message: {type: string, example: OK} + data: {type: array, items: {$ref: '#/components/schemas/ControlCategory'}} + '404': + description: 'Project not found' content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - "500": - description: "Internal server error" + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 'Internal server error' content: application/json: schema: - $ref: "#/components/schemas/ServerError" - "/projects/compliance/progress/{id}": + $ref: '#/components/schemas/ServerError' + '/projects/compliance/progress/{id}': get: - summary: "Get compliance progress for a single project" + summary: 'Get compliance progress for a single project' operationId: projectComplianceProgress tags: - Projects security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - $ref: "#/components/parameters/ProjectId" + - + $ref: '#/components/parameters/ProjectId' responses: - "200": - description: "Compliance progress returned" + '200': + description: 'Compliance progress returned' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: { $ref: "#/components/schemas/ComplianceProgress" } - "404": - description: "Project not found" + message: {type: string, example: OK} + data: {$ref: '#/components/schemas/ComplianceProgress'} + '404': + description: 'Project not found' content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - "500": - description: "Internal server error" + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 'Internal server error' content: application/json: schema: - $ref: "#/components/schemas/ServerError" - "/projects/assessment/progress/{id}": + $ref: '#/components/schemas/ServerError' + '/projects/assessment/progress/{id}': get: - summary: "Get assessment progress for a single project" + summary: 'Get assessment progress for a single project' operationId: projectAssessmentProgress tags: - Projects security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - $ref: "#/components/parameters/ProjectId" + - + $ref: '#/components/parameters/ProjectId' responses: - "200": - description: "Assessment progress returned" + '200': + description: 'Assessment progress returned' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: { $ref: "#/components/schemas/AssessmentProgress" } - "404": - description: "Project not found" + message: {type: string, example: OK} + data: {$ref: '#/components/schemas/AssessmentProgress'} + '404': + description: 'Project not found' content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - "500": - description: "Internal server error" + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 'Internal server error' content: application/json: schema: - $ref: "#/components/schemas/ServerError" + $ref: '#/components/schemas/ServerError' /projects/all/compliance/progress: get: - summary: "Get compliance progress across all projects" + summary: 'Get compliance progress across all projects' operationId: allProjectsComplianceProgress tags: - Projects security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": - description: "Aggregated compliance progress returned" + '200': + description: 'Aggregated compliance progress returned' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: { $ref: "#/components/schemas/ComplianceProgress" } - "401": + message: {type: string, example: OK} + data: {$ref: '#/components/schemas/ComplianceProgress'} + '401': description: Unauthorized content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - "404": - description: "No projects found" + $ref: '#/components/schemas/ErrorResponse' + '404': + description: 'No projects found' content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - "500": - description: "Internal server error" + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 'Internal server error' content: application/json: schema: - $ref: "#/components/schemas/ServerError" + $ref: '#/components/schemas/ServerError' /projects/all/assessment/progress: get: - summary: "Get assessment progress across all projects" + summary: 'Get assessment progress across all projects' operationId: allProjectsAssessmentProgress tags: - Projects security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": - description: "Aggregated assessment progress returned" + '200': + description: 'Aggregated assessment progress returned' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: { $ref: "#/components/schemas/AssessmentProgress" } - "401": + message: {type: string, example: OK} + data: {$ref: '#/components/schemas/AssessmentProgress'} + '401': description: Unauthorized content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - "404": - description: "No projects found" + $ref: '#/components/schemas/ErrorResponse' + '404': + description: 'No projects found' content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - "500": - description: "Internal server error" + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 'Internal server error' content: application/json: schema: - $ref: "#/components/schemas/ServerError" - "/projects/{id}/status": + $ref: '#/components/schemas/ServerError' + '/projects/{id}/status': patch: - summary: "Update project status" + summary: 'Update project status' operationId: updateProjectStatus tags: - Projects security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - $ref: "#/components/parameters/ProjectId" + - + $ref: '#/components/parameters/ProjectId' requestBody: required: true content: @@ -9378,271 +9500,292 @@ paths: - status properties: status: - $ref: "#/components/schemas/ProjectStatus" + $ref: '#/components/schemas/ProjectStatus' responses: - "200": - description: "Project status updated successfully" + '200': + description: 'Project status updated successfully' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: { $ref: "#/components/schemas/ProjectWithMembers" } - "404": - description: "Project not found" + message: {type: string, example: OK} + data: {$ref: '#/components/schemas/ProjectWithMembers'} + '404': + description: 'Project not found' content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - "500": - description: "Internal server error" + $ref: '#/components/schemas/ErrorResponse' + '500': + description: 'Internal server error' content: application/json: schema: - $ref: "#/components/schemas/ServerError" + $ref: '#/components/schemas/ServerError' /questions: get: tags: - Assessments - summary: "Get All Questions" + summary: 'Get All Questions' operationId: getAllQuestions security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/questions/{id}": + '500': + description: 'Internal server error' + '/questions/{id}': get: tags: - Assessments - summary: "Get Question By Id" + summary: 'Get Question By Id' operationId: getQuestionById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/questions/bysubtopic/{id}": + '500': + description: 'Internal server error' + '/questions/bysubtopic/{id}': get: tags: - Assessments - summary: "Get Questions By Subtopic Id" + summary: 'Get Questions By Subtopic Id' operationId: getQuestionsBySubtopicId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/questions/bytopic/{id}": + '500': + description: 'Internal server error' + '/questions/bytopic/{id}': get: tags: - Assessments - summary: "Get Questions By Topic Id" + summary: 'Get Questions By Topic Id' operationId: getQuestionsByTopicId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /autoDrivers: post: tags: - - "Demo Data" - summary: "Post Auto Driver" + - 'Demo Data' + summary: 'Post Auto Driver' operationId: postAutoDriver security: - - bearerAuth: [] - description: "Requires role: Admin" + - + bearerAuth: [] + description: 'Requires role: Admin' requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' delete: tags: - - "Demo Data" - summary: "Delete Auto Driver" + - 'Demo Data' + summary: 'Delete Auto Driver' operationId: deleteAutoDriver security: - - bearerAuth: [] - description: "Requires role: Admin" + - + bearerAuth: [] + description: 'Requires role: Admin' responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' /assessments: get: tags: - Assessments - summary: "Get All Assessments" + summary: 'Get All Assessments' operationId: getAllAssessments security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: - summary: "Create Assessment" + summary: 'Create Assessment' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - Assessments operationId: createAssessment security: - - bearerAuth: [] - "/assessments/getAnswers/{id}": + - + bearerAuth: [] + '/assessments/getAnswers/{id}': get: tags: - Assessments - summary: "Get Answers" + summary: 'Get Answers' operationId: getAnswers security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/assessments/{id}": + '500': + description: 'Internal server error' + '/assessments/{id}': get: tags: - Assessments - summary: "Get Assessment By Id" + summary: 'Get Assessment By Id' operationId: getAssessmentById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' put: - summary: "Update Assessment By Id" + summary: 'Update Assessment By Id' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - Assessments operationId: updateAssessmentById security: - - bearerAuth: [] + - + bearerAuth: [] delete: - summary: "Delete Assessment By Id" + summary: 'Delete Assessment By Id' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - Assessments operationId: deleteAssessmentById security: - - bearerAuth: [] - "/assessments/project/byid/{id}": + - + bearerAuth: [] + '/assessments/project/byid/{id}': get: tags: - Assessments - summary: "Get Assessment By Project Id" + summary: 'Get Assessment By Project Id' operationId: getAssessmentByProjectId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /projectRisks: get: tags: - - "Project Risks" - summary: "Get All Risks" + - 'Project Risks' + summary: 'Get All Risks' operationId: getAllRisks security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: filter + - + name: filter in: query required: false schema: @@ -9652,63 +9795,67 @@ paths: - deleted - all default: active - description: "Filter by soft-delete state." + description: 'Filter by soft-delete state.' responses: - "200": + '200': description: Success content: application/json: schema: type: object properties: - message: { type: string } - data: { type: array, items: { $ref: "#/components/schemas/ProjectRiskResponse" } } - "401": + message: {type: string} + data: {type: array, items: {$ref: '#/components/schemas/ProjectRiskResponse'}} + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - - "Project Risks" - summary: "Create Risk" + - 'Project Risks' + summary: 'Create Risk' operationId: createRisk security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/ProjectRiskInput" + $ref: '#/components/schemas/ProjectRiskInput' responses: - "201": - description: "Created successfully" + '201': + description: 'Created successfully' content: application/json: schema: type: object properties: - message: { type: string } - data: { $ref: "#/components/schemas/ProjectRiskResponse" } - "401": + message: {type: string} + data: {$ref: '#/components/schemas/ProjectRiskResponse'} + '401': description: Unauthorized - "500": - description: "Internal server error" - "/projectRisks/by-projid/{id}": + '500': + description: 'Internal server error' + '/projectRisks/by-projid/{id}': get: tags: - - "Project Risks" - summary: "Get Risks By Project" + - 'Project Risks' + summary: 'Get Risks By Project' operationId: getRisksByProject security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: string - - name: filter + - + name: filter in: query required: false schema: @@ -9719,34 +9866,37 @@ paths: - all default: active responses: - "200": + '200': description: Success content: application/json: schema: type: object properties: - message: { type: string } - data: { type: array, items: { $ref: "#/components/schemas/ProjectRiskResponse" } } - "401": + message: {type: string} + data: {type: array, items: {$ref: '#/components/schemas/ProjectRiskResponse'}} + '401': description: Unauthorized - "500": - description: "Internal server error" - "/projectRisks/by-frameworkid/{id}": + '500': + description: 'Internal server error' + '/projectRisks/by-frameworkid/{id}': get: tags: - - "Project Risks" - summary: "Get Risks By Framework" + - 'Project Risks' + summary: 'Get Risks By Framework' operationId: getRisksByFramework security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer - - name: filter + - + name: filter in: query required: false schema: @@ -9757,56 +9907,60 @@ paths: - all default: active responses: - "200": + '200': description: Success content: application/json: schema: type: object properties: - message: { type: string } - data: { type: array, items: { $ref: "#/components/schemas/ProjectRiskResponse" } } - "401": + message: {type: string} + data: {type: array, items: {$ref: '#/components/schemas/ProjectRiskResponse'}} + '401': description: Unauthorized - "500": - description: "Internal server error" - "/projectRisks/{id}": + '500': + description: 'Internal server error' + '/projectRisks/{id}': get: tags: - - "Project Risks" - summary: "Get Risk By Id" + - 'Project Risks' + summary: 'Get Risk By Id' operationId: getRiskById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success content: application/json: schema: type: object properties: - message: { type: string } - data: { $ref: "#/components/schemas/ProjectRiskResponse" } - "401": + message: {type: string} + data: {$ref: '#/components/schemas/ProjectRiskResponse'} + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' put: tags: - - "Project Risks" - summary: "Update Risk By Id" + - 'Project Risks' + summary: 'Update Risk By Id' operationId: updateRiskById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -9816,286 +9970,308 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ProjectRiskInput" + $ref: '#/components/schemas/ProjectRiskInput' responses: - "200": + '200': description: Success content: application/json: schema: type: object properties: - message: { type: string } - data: { $ref: "#/components/schemas/ProjectRiskResponse" } - "401": + message: {type: string} + data: {$ref: '#/components/schemas/ProjectRiskResponse'} + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - - "Project Risks" - summary: "Delete Risk By Id" + - 'Project Risks' + summary: 'Delete Risk By Id' operationId: deleteRiskById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" + '200': + description: 'Deleted successfully' content: application/json: schema: type: object properties: - message: { type: string } - data: { $ref: "#/components/schemas/ProjectRiskResponse" } - "401": + message: {type: string} + data: {$ref: '#/components/schemas/ProjectRiskResponse'} + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /projectRisks/bulk: patch: - summary: "Bulk Update Project Risks" + summary: 'Bulk Update Project Risks' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Project Risks" + - 'Project Risks' operationId: bulkUpdateProjectRisks security: - - bearerAuth: [] - description: "Requires role: Admin or Editor" + - + bearerAuth: [] + description: 'Requires role: Admin or Editor' /roles: get: tags: - Roles - summary: "Get All Roles" + summary: 'Get All Roles' operationId: getAllRoles security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/roles/{id}": + '500': + description: 'Internal server error' + '/roles/{id}': get: tags: - Roles - summary: "Get Role By Id" + summary: 'Get Role By Id' operationId: getRoleById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /files: get: tags: - Files - summary: "Get User Files Meta Data" + summary: 'Get User Files Meta Data' operationId: files_getAllFolders security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - Files - summary: "Post File Content" + summary: 'Post File Content' operationId: files_createFolder security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/files/by-projid/{id}": + '500': + description: 'Internal server error' + '/files/by-projid/{id}': get: tags: - Files - summary: "Get File Meta By Project Id" + summary: 'Get File Meta By Project Id' operationId: getFileMetaByProjectId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/files/entity/{framework_type}/{entity_type}/{entity_id}": + '500': + description: 'Internal server error' + '/files/entity/{framework_type}/{entity_type}/{entity_id}': get: tags: - Files - summary: "Get Entity Files" + summary: 'Get Entity Files' operationId: getEntityFiles security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: framework_type + - + name: framework_type in: path required: true schema: type: string - - name: entity_type + - + name: entity_type in: path required: true schema: type: string - - name: entity_id + - + name: entity_id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /files/attach: post: tags: - Files - summary: "Attach File To Entity" + summary: 'Attach File To Entity' operationId: attachFileToEntity security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /files/attach-bulk: post: tags: - Files - summary: "Attach Files To Entity" + summary: 'Attach Files To Entity' operationId: attachFilesToEntity security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /files/detach: delete: tags: - Files - summary: "Detach File From Entity" + summary: 'Detach File From Entity' operationId: detachFileFromEntity security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /files/bulk-tags: patch: - summary: "Bulk Update File Tags" + summary: 'Bulk Update File Tags' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - Files operationId: bulkUpdateFileTags security: - - bearerAuth: [] - description: "Requires role: Admin or Editor" - "/files/{id}": + - + bearerAuth: [] + description: 'Requires role: Admin or Editor' + '/files/{id}': get: tags: - Files - summary: "Get File Content By Id" + summary: 'Get File Content By Id' operationId: files_getFolderById security: - - bearerAuth: [] - description: "Requires role: Admin" + - + bearerAuth: [] + description: 'Requires role: Admin' parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' patch: tags: - Files - summary: "Update Folder" + summary: 'Update Folder' operationId: files_updateFolder security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -10106,52 +10282,55 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - Files - summary: "Delete Folder" + summary: 'Delete Folder' operationId: files_deleteFolder security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /mail/invite: post: tags: - Mail - summary: "Invite Limiter" + summary: 'Invite Limiter' operationId: anonymous security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /mail/reset-password: post: tags: @@ -10164,63 +10343,68 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "500": - description: "Internal server error" + '201': + description: 'Created successfully' + '500': + description: 'Internal server error' /invitations: get: tags: - Invitations - summary: "Get Invitations" + summary: 'Get Invitations' operationId: getInvitations security: - - bearerAuth: [] - description: "Requires role: Admin or SuperAdmin" + - + bearerAuth: [] + description: 'Requires role: Admin or SuperAdmin' responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" - "/invitations/{id}": + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' + '/invitations/{id}': delete: tags: - Invitations - summary: "Revoke Invitation" + summary: 'Revoke Invitation' operationId: revokeInvitation security: - - bearerAuth: [] - description: "Requires role: Admin or SuperAdmin" + - + bearerAuth: [] + description: 'Requires role: Admin or SuperAdmin' parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" - "/invitations/{id}/resend": + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' + '/invitations/{id}/resend': post: tags: - Invitations - summary: "Resend Invitation" + summary: 'Resend Invitation' operationId: resendInvitation security: - - bearerAuth: [] - description: "Requires role: Admin or SuperAdmin" + - + bearerAuth: [] + description: 'Requires role: Admin or SuperAdmin' parameters: - - name: id + - + name: id in: path required: true schema: @@ -10231,330 +10415,357 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' /frameworks: get: tags: - Frameworks - summary: "Get All Frameworks" + summary: 'Get All Frameworks' operationId: getAllFrameworks security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/frameworks/{id}": + '500': + description: 'Internal server error' + '/frameworks/{id}': get: tags: - Frameworks - summary: "Get Framework By Id" + summary: 'Get Framework By Id' operationId: getFrameworkById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /frameworks/toProject: post: tags: - Frameworks - summary: "Add Framework To Project" + summary: 'Add Framework To Project' operationId: addFrameworkToProject security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /frameworks/fromProject: delete: tags: - Frameworks - summary: "Delete Framework From Project" + summary: 'Delete Framework From Project' operationId: deleteFrameworkFromProject security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /eu-ai-act/controlCategories: get: tags: - - "EU AI Act" - summary: "Get All Control Categories" + - 'EU AI Act' + summary: 'Get All Control Categories' operationId: getAllControlCategories security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/eu-ai-act/controls/byControlCategoryId/{id}": + '500': + description: 'Internal server error' + '/eu-ai-act/controls/byControlCategoryId/{id}': get: tags: - - "EU AI Act" - summary: "Get Controls By Control Category Id" + - 'EU AI Act' + summary: 'Get Controls By Control Category Id' operationId: getControlsByControlCategoryId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /eu-ai-act/topics: get: tags: - - "EU AI Act" - summary: "Get All Topics" + - 'EU AI Act' + summary: 'Get All Topics' operationId: getAllTopics security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/eu-ai-act/assessments/byProjectId/{id}": + '500': + description: 'Internal server error' + '/eu-ai-act/assessments/byProjectId/{id}': get: tags: - - "EU AI Act" - summary: "Get Assessments By Project Id" + - 'EU AI Act' + summary: 'Get Assessments By Project Id' operationId: getAssessmentsByProjectId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - - "EU AI Act" - summary: "Delete Assessments By Project Id" + - 'EU AI Act' + summary: 'Delete Assessments By Project Id' operationId: deleteAssessmentsByProjectId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/eu-ai-act/compliances/byProjectId/{id}": + '500': + description: 'Internal server error' + '/eu-ai-act/compliances/byProjectId/{id}': get: tags: - - "EU AI Act" - summary: "Get Compliances By Project Id" + - 'EU AI Act' + summary: 'Get Compliances By Project Id' operationId: getCompliancesByProjectId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - - "EU AI Act" - summary: "Delete Compliances By Project Id" + - 'EU AI Act' + summary: 'Delete Compliances By Project Id' operationId: deleteCompliancesByProjectId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/eu-ai-act/compliances/progress/{id}": + '500': + description: 'Internal server error' + '/eu-ai-act/compliances/progress/{id}': get: tags: - - "EU AI Act" - summary: "Get Project Compliance Progress" + - 'EU AI Act' + summary: 'Get Project Compliance Progress' operationId: getProjectComplianceProgress security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/eu-ai-act/assessments/progress/{id}": + '500': + description: 'Internal server error' + '/eu-ai-act/assessments/progress/{id}': get: tags: - - "EU AI Act" - summary: "Get Project Assessment Progress" + - 'EU AI Act' + summary: 'Get Project Assessment Progress' operationId: getProjectAssessmentProgress security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /eu-ai-act/all/compliances/progress: get: tags: - - "EU AI Act" - summary: "Get All Projects Compliance Progress" + - 'EU AI Act' + summary: 'Get All Projects Compliance Progress' operationId: getAllProjectsComplianceProgress security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /eu-ai-act/all/assessments/progress: get: tags: - - "EU AI Act" - summary: "Get All Projects Assessment Progress" + - 'EU AI Act' + summary: 'Get All Projects Assessment Progress' operationId: getAllProjectsAssessmentProgress security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /eu-ai-act/topicById: get: tags: - - "EU AI Act" - summary: "Get Topic By Id" + - 'EU AI Act' + summary: 'Get Topic By Id' operationId: getTopicById security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /eu-ai-act/controlById: get: tags: - - "EU AI Act" - summary: "Get Control By Id" + - 'EU AI Act' + summary: 'Get Control By Id' operationId: getControlById security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/eu-ai-act/saveControls/{id}": + '500': + description: 'Internal server error' + '/eu-ai-act/saveControls/{id}': patch: tags: - - "EU AI Act" - summary: "Save Controls" + - 'EU AI Act' + summary: 'Save Controls' operationId: saveControls security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -10565,22 +10776,24 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/eu-ai-act/saveAnswer/{id}": + '500': + description: 'Internal server error' + '/eu-ai-act/saveAnswer/{id}': patch: tags: - - "EU AI Act" - summary: "Update Question By Id" + - 'EU AI Act' + summary: 'Update Question By Id' operationId: updateQuestionById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -10591,53 +10804,57 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /organizations/exists: get: tags: - Organizations - summary: "Get Organizations Exists" + summary: 'Get Organizations Exists' operationId: getOrganizationsExists responses: - "200": + '200': description: Success - "500": - description: "Internal server error" - "/organizations/{id}": + '500': + description: 'Internal server error' + '/organizations/{id}': get: tags: - Organizations - summary: "Get Organization By Id" + summary: 'Get Organization By Id' operationId: getOrganizationById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' patch: tags: - Organizations - summary: "Update Organization By Id" + summary: 'Update Organization By Id' operationId: updateOrganizationById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -10648,43 +10865,46 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /organizations: post: tags: - Organizations - summary: "Create Organization" + summary: 'Create Organization' operationId: createOrganization security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - description: "Requires role: Super Admin" - "/organizations/{id}/onboarding-status": + '500': + description: 'Internal server error' + description: 'Requires role: Super Admin' + '/organizations/{id}/onboarding-status': patch: tags: - Organizations - summary: "Update Onboarding Status" + summary: 'Update Onboarding Status' operationId: updateOnboardingStatus security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -10695,416 +10915,454 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /iso-42001/clauses: get: tags: - - "ISO 42001" - summary: "Get All Clauses" + - 'ISO 42001' + summary: 'Get All Clauses' operationId: getAllClauses security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-42001/clauses/struct/byProjectId/{id}": + '500': + description: 'Internal server error' + '/iso-42001/clauses/struct/byProjectId/{id}': get: tags: - - "ISO 42001" - summary: "Get All Clauses Struct For Project" + - 'ISO 42001' + summary: 'Get All Clauses Struct For Project' operationId: getAllClausesStructForProject security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /iso-42001/annexes: get: tags: - - "ISO 42001" - summary: "Get All Annexes" + - 'ISO 42001' + summary: 'Get All Annexes' operationId: getAllAnnexes security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-42001/annexes/struct/byProjectId/{id}": + '500': + description: 'Internal server error' + '/iso-42001/annexes/struct/byProjectId/{id}': get: tags: - - "ISO 42001" - summary: "Get All Annexes Struct For Project" + - 'ISO 42001' + summary: 'Get All Annexes Struct For Project' operationId: getAllAnnexesStructForProject security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-42001/clauses/byProjectId/{id}": + '500': + description: 'Internal server error' + '/iso-42001/clauses/byProjectId/{id}': get: tags: - - "ISO 42001" - summary: "Get Clauses By Project Id" + - 'ISO 42001' + summary: 'Get Clauses By Project Id' operationId: getClausesByProjectId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - - "ISO 42001" - summary: "Delete Management System Clauses" + - 'ISO 42001' + summary: 'Delete Management System Clauses' operationId: deleteManagementSystemClauses security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-42001/annexes/byProjectId/{id}": + '500': + description: 'Internal server error' + '/iso-42001/annexes/byProjectId/{id}': get: tags: - - "ISO 42001" - summary: "Get Annexes By Project Id" + - 'ISO 42001' + summary: 'Get Annexes By Project Id' operationId: getAnnexesByProjectId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - - "ISO 42001" - summary: "Delete Reference Controls" + - 'ISO 42001' + summary: 'Delete Reference Controls' operationId: deleteReferenceControls security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-42001/subClauses/byClauseId/{id}": + '500': + description: 'Internal server error' + '/iso-42001/subClauses/byClauseId/{id}': get: tags: - - "ISO 42001" - summary: "Get Sub Clauses By Clause Id" + - 'ISO 42001' + summary: 'Get Sub Clauses By Clause Id' operationId: getSubClausesByClauseId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-42001/annexCategories/byAnnexId/{id}": + '500': + description: 'Internal server error' + '/iso-42001/annexCategories/byAnnexId/{id}': get: tags: - - "ISO 42001" - summary: "Get Annex Categories By Annex Id" + - 'ISO 42001' + summary: 'Get Annex Categories By Annex Id' operationId: getAnnexCategoriesByAnnexId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-42001/subClause/byId/{id}": + '500': + description: 'Internal server error' + '/iso-42001/subClause/byId/{id}': get: tags: - - "ISO 42001" - summary: "Get Sub Clause By Id" + - 'ISO 42001' + summary: 'Get Sub Clause By Id' operationId: getSubClauseById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-42001/subclauses/{id}/risks": + '500': + description: 'Internal server error' + '/iso-42001/subclauses/{id}/risks': get: tags: - - "ISO 42001" - summary: "Get Sub Clause Risks" + - 'ISO 42001' + summary: 'Get Sub Clause Risks' operationId: getSubClauseRisks security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-42001/annexCategories/{id}/risks": + '500': + description: 'Internal server error' + '/iso-42001/annexCategories/{id}/risks': get: tags: - - "ISO 42001" - summary: "Get Annex Category Risks" + - 'ISO 42001' + summary: 'Get Annex Category Risks' operationId: getAnnexCategoryRisks security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-42001/annexCategory/byId/{id}": + '500': + description: 'Internal server error' + '/iso-42001/annexCategory/byId/{id}': get: tags: - - "ISO 42001" - summary: "Get Annex Category By Id" + - 'ISO 42001' + summary: 'Get Annex Category By Id' operationId: getAnnexCategoryById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-42001/clauses/progress/{id}": + '500': + description: 'Internal server error' + '/iso-42001/clauses/progress/{id}': get: tags: - - "ISO 42001" - summary: "Get Project Clauses Progress" + - 'ISO 42001' + summary: 'Get Project Clauses Progress' operationId: getProjectClausesProgress security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-42001/annexes/progress/{id}": + '500': + description: 'Internal server error' + '/iso-42001/annexes/progress/{id}': get: tags: - - "ISO 42001" - summary: "Get Project Annxes Progress" + - 'ISO 42001' + summary: 'Get Project Annxes Progress' operationId: getProjectAnnxesProgress security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /iso-42001/all/clauses/progress: get: tags: - - "ISO 42001" - summary: "Get All Projects Clauses Progress" + - 'ISO 42001' + summary: 'Get All Projects Clauses Progress' operationId: getAllProjectsClausesProgress security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /iso-42001/all/annexes/progress: get: tags: - - "ISO 42001" - summary: "Get All Projects Annxes Progress" + - 'ISO 42001' + summary: 'Get All Projects Annxes Progress' operationId: getAllProjectsAnnxesProgress security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-42001/clauses/assignments/{id}": + '500': + description: 'Internal server error' + '/iso-42001/clauses/assignments/{id}': get: tags: - - "ISO 42001" - summary: "Get Project Clauses Assignments" + - 'ISO 42001' + summary: 'Get Project Clauses Assignments' operationId: getProjectClausesAssignments security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-42001/annexes/assignments/{id}": + '500': + description: 'Internal server error' + '/iso-42001/annexes/assignments/{id}': get: tags: - - "ISO 42001" - summary: "Get Project Annexes Assignments" + - 'ISO 42001' + summary: 'Get Project Annexes Assignments' operationId: getProjectAnnexesAssignments security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-42001/saveClauses/{id}": + '500': + description: 'Internal server error' + '/iso-42001/saveClauses/{id}': patch: tags: - - "ISO 42001" - summary: "Save Clauses" + - 'ISO 42001' + summary: 'Save Clauses' operationId: saveClauses security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -11115,22 +11373,24 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-42001/saveAnnexes/{id}": + '500': + description: 'Internal server error' + '/iso-42001/saveAnnexes/{id}': patch: tags: - - "ISO 42001" - summary: "Save Annexes" + - 'ISO 42001' + summary: 'Save Annexes' operationId: saveAnnexes security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -11141,374 +11401,408 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /iso-27001/clauses: get: tags: - - "ISO 27001" - summary: "Get All Clauses" + - 'ISO 27001' + summary: 'Get All Clauses' operationId: iso_27001_getAllClauses security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-27001/clauses/struct/byProjectId/{id}": + '500': + description: 'Internal server error' + '/iso-27001/clauses/struct/byProjectId/{id}': get: tags: - - "ISO 27001" - summary: "Get All Clauses Struct For Project" + - 'ISO 27001' + summary: 'Get All Clauses Struct For Project' operationId: iso_27001_getAllClausesStructForProject security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /iso-27001/annexes: get: tags: - - "ISO 27001" - summary: "Get All Annexes" + - 'ISO 27001' + summary: 'Get All Annexes' operationId: iso_27001_getAllAnnexes security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-27001/annexes/struct/byProjectId/{id}": + '500': + description: 'Internal server error' + '/iso-27001/annexes/struct/byProjectId/{id}': get: tags: - - "ISO 27001" - summary: "Get All Annexes Struct For Project" + - 'ISO 27001' + summary: 'Get All Annexes Struct For Project' operationId: iso_27001_getAllAnnexesStructForProject security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-27001/clauses/byProjectId/{id}": + '500': + description: 'Internal server error' + '/iso-27001/clauses/byProjectId/{id}': get: tags: - - "ISO 27001" - summary: "Get Clauses By Project Id" + - 'ISO 27001' + summary: 'Get Clauses By Project Id' operationId: iso_27001_getClausesByProjectId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - - "ISO 27001" - summary: "Delete Management System Clauses" + - 'ISO 27001' + summary: 'Delete Management System Clauses' operationId: iso_27001_deleteManagementSystemClauses security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-27001/annexes/byProjectId/{id}": + '500': + description: 'Internal server error' + '/iso-27001/annexes/byProjectId/{id}': get: tags: - - "ISO 27001" - summary: "Get Annexes By Project Id" + - 'ISO 27001' + summary: 'Get Annexes By Project Id' operationId: iso_27001_getAnnexesByProjectId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - - "ISO 27001" - summary: "Delete Reference Controls" + - 'ISO 27001' + summary: 'Delete Reference Controls' operationId: iso_27001_deleteReferenceControls security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-27001/subClauses/byClauseId/{id}": + '500': + description: 'Internal server error' + '/iso-27001/subClauses/byClauseId/{id}': get: tags: - - "ISO 27001" - summary: "Get Sub Clauses By Clause Id" + - 'ISO 27001' + summary: 'Get Sub Clauses By Clause Id' operationId: iso_27001_getSubClausesByClauseId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-27001/annexControls/byAnnexId/{id}": + '500': + description: 'Internal server error' + '/iso-27001/annexControls/byAnnexId/{id}': get: tags: - - "ISO 27001" - summary: "Get Annex Controls By Annex Id" + - 'ISO 27001' + summary: 'Get Annex Controls By Annex Id' operationId: getAnnexControlsByAnnexId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-27001/subClause/byId/{id}": + '500': + description: 'Internal server error' + '/iso-27001/subClause/byId/{id}': get: tags: - - "ISO 27001" - summary: "Get Sub Clause By Id" + - 'ISO 27001' + summary: 'Get Sub Clause By Id' operationId: iso_27001_getSubClauseById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-27001/annexControl/byId/{id}": + '500': + description: 'Internal server error' + '/iso-27001/annexControl/byId/{id}': get: tags: - - "ISO 27001" - summary: "Get Annex Control By Id" + - 'ISO 27001' + summary: 'Get Annex Control By Id' operationId: getAnnexControlById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-27001/clauses/progress/{id}": + '500': + description: 'Internal server error' + '/iso-27001/clauses/progress/{id}': get: tags: - - "ISO 27001" - summary: "Get Project Clauses Progress" + - 'ISO 27001' + summary: 'Get Project Clauses Progress' operationId: iso_27001_getProjectClausesProgress security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-27001/annexes/progress/{id}": + '500': + description: 'Internal server error' + '/iso-27001/annexes/progress/{id}': get: tags: - - "ISO 27001" - summary: "Get Project Annxes Progress" + - 'ISO 27001' + summary: 'Get Project Annxes Progress' operationId: iso_27001_getProjectAnnxesProgress security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /iso-27001/all/clauses/progress: get: tags: - - "ISO 27001" - summary: "Get All Projects Clauses Progress" + - 'ISO 27001' + summary: 'Get All Projects Clauses Progress' operationId: iso_27001_getAllProjectsClausesProgress security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /iso-27001/all/annexes/progress: get: tags: - - "ISO 27001" - summary: "Get All Projects Annxes Progress" + - 'ISO 27001' + summary: 'Get All Projects Annxes Progress' operationId: iso_27001_getAllProjectsAnnxesProgress security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-27001/clauses/assignments/{id}": + '500': + description: 'Internal server error' + '/iso-27001/clauses/assignments/{id}': get: tags: - - "ISO 27001" - summary: "Get Project Clauses Assignments" + - 'ISO 27001' + summary: 'Get Project Clauses Assignments' operationId: iso_27001_getProjectClausesAssignments security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-27001/annexes/assignments/{id}": + '500': + description: 'Internal server error' + '/iso-27001/annexes/assignments/{id}': get: tags: - - "ISO 27001" - summary: "Get Project Annexes Assignments" + - 'ISO 27001' + summary: 'Get Project Annexes Assignments' operationId: iso_27001_getProjectAnnexesAssignments security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-27001/saveClauses/{id}": + '500': + description: 'Internal server error' + '/iso-27001/saveClauses/{id}': patch: tags: - - "ISO 27001" - summary: "Save Clauses" + - 'ISO 27001' + summary: 'Save Clauses' operationId: iso_27001_saveClauses security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -11519,22 +11813,24 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/iso-27001/saveAnnexes/{id}": + '500': + description: 'Internal server error' + '/iso-27001/saveAnnexes/{id}': patch: tags: - - "ISO 27001" - summary: "Save Annexes" + - 'ISO 27001' + summary: 'Save Annexes' operationId: iso_27001_saveAnnexes security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -11545,77 +11841,83 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /training: get: tags: - Training - summary: "Get All Training Registar" + summary: 'Get All Training Registar' operationId: getAllTrainingRegistar security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - Training - summary: "Create New Training Registar" + summary: 'Create New Training Registar' operationId: createNewTrainingRegistar security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/training/training-id/{id}": + '500': + description: 'Internal server error' + '/training/training-id/{id}': get: tags: - Training - summary: "Get Training Registar By Id" + summary: 'Get Training Registar By Id' operationId: getTrainingRegistarById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/training/{id}": + '500': + description: 'Internal server error' + '/training/{id}': patch: tags: - Training - summary: "Update Training Registar By Id" + summary: 'Update Training Registar By Id' operationId: updateTrainingRegistarById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -11626,234 +11928,250 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - Training - summary: "Delete Training Registar By Id" + summary: 'Delete Training Registar By Id' operationId: deleteTrainingRegistarById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /aiTrustCentre/overview: get: tags: - - "AI Trust Centre" - summary: "Get A I Trust Centre Overview" + - 'AI Trust Centre' + summary: 'Get A I Trust Centre Overview' operationId: getAITrustCentreOverview security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' put: tags: - - "AI Trust Centre" - summary: "Update A I Trust Overview" + - 'AI Trust Centre' + summary: 'Update A I Trust Overview' operationId: updateAITrustOverview security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /aiTrustCentre/resources: get: tags: - - "AI Trust Centre" - summary: "Get A I Trust Centre Resources" + - 'AI Trust Centre' + summary: 'Get A I Trust Centre Resources' operationId: getAITrustCentreResources security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - - "AI Trust Centre" - summary: "Create A I Trust Resource" + - 'AI Trust Centre' + summary: 'Create A I Trust Resource' operationId: createAITrustResource security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /aiTrustCentre/subprocessors: get: tags: - - "AI Trust Centre" - summary: "Get A I Trust Centre Subprocessors" + - 'AI Trust Centre' + summary: 'Get A I Trust Centre Subprocessors' operationId: getAITrustCentreSubprocessors security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - - "AI Trust Centre" - summary: "Create A I Trust Subprocessor" + - 'AI Trust Centre' + summary: 'Create A I Trust Subprocessor' operationId: createAITrustSubprocessor security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/aiTrustCentre/{hash}": + '500': + description: 'Internal server error' + '/aiTrustCentre/{hash}': get: tags: - - "AI Trust Centre" - summary: "Get A I Trust Centre Public Page" + - 'AI Trust Centre' + summary: 'Get A I Trust Centre Public Page' operationId: getAITrustCentrePublicPage parameters: - - name: hash + - + name: hash in: path required: true schema: type: string responses: - "200": + '200': description: Success - "500": - description: "Internal server error" - "/aiTrustCentre/{hash}/logo": + '500': + description: 'Internal server error' + '/aiTrustCentre/{hash}/logo': get: tags: - - "AI Trust Centre" - summary: "Get Company Logo" + - 'AI Trust Centre' + summary: 'Get Company Logo' operationId: getCompanyLogo parameters: - - name: hash + - + name: hash in: path required: true schema: type: string responses: - "200": + '200': description: Success - "500": - description: "Internal server error" - "/aiTrustCentre/{hash}/resources/{id}": + '500': + description: 'Internal server error' + '/aiTrustCentre/{hash}/resources/{id}': get: tags: - - "AI Trust Centre" - summary: "Get A I Trust Centre Public Resource" + - 'AI Trust Centre' + summary: 'Get A I Trust Centre Public Resource' operationId: getAITrustCentrePublicResource parameters: - - name: hash + - + name: hash in: path required: true schema: type: string - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /aiTrustCentre/logo: post: tags: - - "AI Trust Centre" - summary: "Upload company logo" + - 'AI Trust Centre' + summary: 'Upload company logo' operationId: uploadCompanyLogo security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - - "AI Trust Centre" - summary: "Delete Company Logo" + - 'AI Trust Centre' + summary: 'Delete Company Logo' operationId: deleteCompanyLogo security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/aiTrustCentre/resources/{id}": + '500': + description: 'Internal server error' + '/aiTrustCentre/resources/{id}': put: tags: - - "AI Trust Centre" - summary: "Update A I Trust Resource" + - 'AI Trust Centre' + summary: 'Update A I Trust Resource' operationId: updateAITrustResource security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -11864,42 +12182,46 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - - "AI Trust Centre" - summary: "Delete A I Trust Resource" + - 'AI Trust Centre' + summary: 'Delete A I Trust Resource' operationId: deleteAITrustResource security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/aiTrustCentre/subprocessors/{id}": + '500': + description: 'Internal server error' + '/aiTrustCentre/subprocessors/{id}': put: tags: - - "AI Trust Centre" - summary: "Update A I Trust Subprocessor" + - 'AI Trust Centre' + summary: 'Update A I Trust Subprocessor' operationId: updateAITrustSubprocessor security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -11910,214 +12232,226 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - - "AI Trust Centre" - summary: "Delete A I Trust Subprocessor" + - 'AI Trust Centre' + summary: 'Delete A I Trust Subprocessor' operationId: deleteAITrustSubprocessor security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /logger/events: get: tags: - System - summary: "Get Events" + summary: 'Get Events' operationId: getEvents security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /logger/logs: get: tags: - System - summary: "Get Logs" + summary: 'Get Logs' operationId: getLogs security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /modelInventory: get: tags: - - "Model Inventory" - summary: "Get all model inventories" + - 'Model Inventory' + summary: 'Get all model inventories' description: "Returns every model inventory record belonging to the caller's organization, ordered by created_at DESC, id ASC. Each record includes its associated project and framework IDs.\n" operationId: getAllModelInventories security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": - description: "List of model inventories (may be empty)" + '200': + description: 'List of model inventories (may be empty)' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: - { type: array, items: { $ref: "#/components/schemas/ModelInventoryResponse" } } - "401": - description: "Missing or invalid JWT" - "500": - description: "Internal server error" + message: {type: string, example: OK} + data: {type: array, items: {$ref: '#/components/schemas/ModelInventoryResponse'}} + '401': + description: 'Missing or invalid JWT' + '500': + description: 'Internal server error' content: application/json: schema: - $ref: "#/components/schemas/ServerError" + $ref: '#/components/schemas/ServerError' post: tags: - - "Model Inventory" - summary: "Create a new model inventory" + - 'Model Inventory' + summary: 'Create a new model inventory' description: "Creates a model inventory record, links it to the supplied project and framework IDs, records a change-history entry, fires any \"model_added\" automations, and notifies the approver (if set).\n" operationId: createNewModelInventory security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/ModelInventoryCreateRequest" + $ref: '#/components/schemas/ModelInventoryCreateRequest' responses: - "201": - description: "Model inventory created" + '201': + description: 'Model inventory created' content: application/json: schema: type: object properties: - message: { type: string, example: Created } - data: { $ref: "#/components/schemas/ModelInventoryResponse" } - "401": - description: "Missing or invalid JWT" - "500": - description: "Internal server error" + message: {type: string, example: Created} + data: {$ref: '#/components/schemas/ModelInventoryResponse'} + '401': + description: 'Missing or invalid JWT' + '500': + description: 'Internal server error' content: application/json: schema: - $ref: "#/components/schemas/ServerError" + $ref: '#/components/schemas/ServerError' /modelInventory/evaluations: get: tags: - - "Model Inventory" - summary: "Get All Model Evaluations" + - 'Model Inventory' + summary: 'Get All Model Evaluations' operationId: getAllModelEvaluations security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/modelInventory/{id}/evaluations": + '500': + description: 'Internal server error' + '/modelInventory/{id}/evaluations': get: tags: - - "Model Inventory" - summary: "Get Model Evaluations" + - 'Model Inventory' + summary: 'Get Model Evaluations' operationId: getModelEvaluations security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/modelInventory/{id}": + '500': + description: 'Internal server error' + '/modelInventory/{id}': get: tags: - - "Model Inventory" - summary: "Get a model inventory by ID" + - 'Model Inventory' + summary: 'Get a model inventory by ID' description: "Returns a single model inventory record with its associated project and framework IDs. Returns 204 if the record does not exist.\n" operationId: getModelInventoryById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true - description: "Model inventory ID" + description: 'Model inventory ID' schema: type: integer responses: - "200": - description: "Model inventory found" + '200': + description: 'Model inventory found' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: { $ref: "#/components/schemas/ModelInventoryResponse" } - "204": - description: "No model inventory found for the given ID" + message: {type: string, example: OK} + data: {$ref: '#/components/schemas/ModelInventoryResponse'} + '204': + description: 'No model inventory found for the given ID' content: application/json: schema: type: object properties: - message: { type: string, example: "No Content" } - data: { nullable: true } - "401": - description: "Missing or invalid JWT" - "500": - description: "Internal server error" + message: {type: string, example: 'No Content'} + data: {nullable: true} + '401': + description: 'Missing or invalid JWT' + '500': + description: 'Internal server error' content: application/json: schema: - $ref: "#/components/schemas/ServerError" + $ref: '#/components/schemas/ServerError' patch: tags: - - "Model Inventory" - summary: "Update a model inventory by ID" + - 'Model Inventory' + summary: 'Update a model inventory by ID' description: "Partially updates a model inventory record. All body fields are optional; only provided fields are changed. Project and framework associations can be replaced or cleared. Fires \"model_updated\" automations and notifies a new approver if changed.\n" operationId: updateModelInventoryById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true - description: "Model inventory ID" + description: 'Model inventory ID' schema: type: integer requestBody: @@ -12125,314 +12459,331 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ModelInventoryUpdateRequest" + $ref: '#/components/schemas/ModelInventoryUpdateRequest' responses: - "200": - description: "Model inventory updated" + '200': + description: 'Model inventory updated' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: { $ref: "#/components/schemas/ModelInventoryResponse" } - "401": - description: "Missing or invalid JWT" - "404": - description: "Model inventory not found" + message: {type: string, example: OK} + data: {$ref: '#/components/schemas/ModelInventoryResponse'} + '401': + description: 'Missing or invalid JWT' + '404': + description: 'Model inventory not found' content: application/json: schema: type: object properties: - message: { type: string, example: "Not Found" } - data: { type: string, example: "Model inventory not found" } - "500": - description: "Internal server error" + message: {type: string, example: 'Not Found'} + data: {type: string, example: 'Model inventory not found'} + '500': + description: 'Internal server error' content: application/json: schema: - $ref: "#/components/schemas/ServerError" + $ref: '#/components/schemas/ServerError' delete: tags: - - "Model Inventory" - summary: "Delete a model inventory by ID" + - 'Model Inventory' + summary: 'Delete a model inventory by ID' description: "Deletes a model inventory record and its project/framework associations. Optionally deletes linked model risks when deleteRisks=true. Records deletion in change history and fires \"model_deleted\" automations.\n" operationId: deleteModelInventoryById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true - description: "Model inventory ID" + description: 'Model inventory ID' schema: type: integer - - name: deleteRisks + - + name: deleteRisks in: query required: false description: "When \"true\", also deletes associated rows from the model_risks table.\n" schema: type: string enum: - - "true" - - "false" - default: "false" + - 'true' + - 'false' + default: 'false' responses: - "200": - description: "Model inventory deleted" + '200': + description: 'Model inventory deleted' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: { type: string, example: "Model inventory deleted successfully" } - "401": - description: "Missing or invalid JWT" - "404": - description: "Model inventory not found" + message: {type: string, example: OK} + data: {type: string, example: 'Model inventory deleted successfully'} + '401': + description: 'Missing or invalid JWT' + '404': + description: 'Model inventory not found' content: application/json: schema: type: object properties: - message: { type: string, example: "Not Found" } - data: { type: string, example: "Model inventory not found" } - "500": - description: "Internal server error" + message: {type: string, example: 'Not Found'} + data: {type: string, example: 'Model inventory not found'} + '500': + description: 'Internal server error' content: application/json: schema: - $ref: "#/components/schemas/ServerError" - "/modelInventory/by-projectId/{projectId}": + $ref: '#/components/schemas/ServerError' + '/modelInventory/by-projectId/{projectId}': get: tags: - - "Model Inventory" - summary: "Get model inventories by project ID" + - 'Model Inventory' + summary: 'Get model inventories by project ID' description: "Returns all model inventories associated with a project (via the model_inventories_projects_frameworks join table where framework_id IS NULL). Non-numeric project IDs (e.g. plugin-sourced) return an empty array.\n" operationId: getModelByProjectId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: projectId + - + name: projectId in: path required: true - description: "Project ID (integer). Non-numeric values return an empty array." + description: 'Project ID (integer). Non-numeric values return an empty array.' schema: type: integer responses: - "200": - description: "List of model inventories for the project (may be empty)" + '200': + description: 'List of model inventories for the project (may be empty)' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: - { type: array, items: { $ref: "#/components/schemas/ModelInventoryResponse" } } - "401": - description: "Missing or invalid JWT" - "500": - description: "Internal server error" + message: {type: string, example: OK} + data: {type: array, items: {$ref: '#/components/schemas/ModelInventoryResponse'}} + '401': + description: 'Missing or invalid JWT' + '500': + description: 'Internal server error' content: application/json: schema: - $ref: "#/components/schemas/ServerError" - "/modelInventory/by-frameworkId/{frameworkId}": + $ref: '#/components/schemas/ServerError' + '/modelInventory/by-frameworkId/{frameworkId}': get: tags: - - "Model Inventory" - summary: "Get model inventories by framework ID" + - 'Model Inventory' + summary: 'Get model inventories by framework ID' description: "Returns all model inventories associated with a framework (via the model_inventories_projects_frameworks join table where framework_id matches).\n" operationId: getModelByFrameworkId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: frameworkId + - + name: frameworkId in: path required: true - description: "Framework ID" + description: 'Framework ID' schema: type: integer responses: - "200": - description: "List of model inventories for the framework (may be empty)" + '200': + description: 'List of model inventories for the framework (may be empty)' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: - { type: array, items: { $ref: "#/components/schemas/ModelInventoryResponse" } } - "401": - description: "Missing or invalid JWT" - "500": - description: "Internal server error" + message: {type: string, example: OK} + data: {type: array, items: {$ref: '#/components/schemas/ModelInventoryResponse'}} + '401': + description: 'Missing or invalid JWT' + '500': + description: 'Internal server error' content: application/json: schema: - $ref: "#/components/schemas/ServerError" + $ref: '#/components/schemas/ServerError' /modelInventoryHistory/timeseries: get: tags: - - "Model Inventory" - summary: "Get Timeseries" + - 'Model Inventory' + summary: 'Get Timeseries' operationId: getTimeseries security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /modelInventoryHistory/current-counts: get: tags: - - "Model Inventory" - summary: "Get Current Counts" + - 'Model Inventory' + summary: 'Get Current Counts' operationId: getCurrentCounts security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /modelInventoryHistory/snapshot: post: tags: - - "Model Inventory" - summary: "Create Snapshot" + - 'Model Inventory' + summary: 'Create Snapshot' operationId: createSnapshot security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /dataset-bulk-upload/upload: post: tags: - Datasets - summary: "Handle Multer Error" + summary: 'Handle Multer Error' operationId: uploadDatasetFile security: - - bearerAuth: [] - description: "Requires role: Admin or Editor" + - + bearerAuth: [] + description: 'Requires role: Admin or Editor' requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" - "/model-inventory-change-history/{id}": + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' + '/model-inventory-change-history/{id}': get: tags: - - "Change History" - summary: "Get Model Inventory Change History By Id" + - 'Change History' + summary: 'Get Model Inventory Change History By Id' operationId: getModelInventoryChangeHistoryById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /datasets: get: tags: - Datasets - summary: "Get All Datasets" + summary: 'Get All Datasets' operationId: getAllDatasets security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - Datasets - summary: "Create New Dataset" + summary: 'Create New Dataset' operationId: createNewDataset security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/datasets/{id}": + '500': + description: 'Internal server error' + '/datasets/{id}': get: tags: - Datasets - summary: "Get Dataset By Id" + summary: 'Get Dataset By Id' operationId: getDatasetById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' patch: tags: - Datasets - summary: "Update Dataset By Id" + summary: 'Update Dataset By Id' operationId: updateDatasetById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -12443,155 +12794,168 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - Datasets - summary: "Delete Dataset By Id" + summary: 'Delete Dataset By Id' operationId: deleteDatasetById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/datasets/by-model/{modelId}": + '500': + description: 'Internal server error' + '/datasets/by-model/{modelId}': get: tags: - Datasets - summary: "Get Datasets By Model Id" + summary: 'Get Datasets By Model Id' operationId: getDatasetsByModelId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: modelId + - + name: modelId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/datasets/by-project/{projectId}": + '500': + description: 'Internal server error' + '/datasets/by-project/{projectId}': get: tags: - Datasets - summary: "Get Datasets By Project Id" + summary: 'Get Datasets By Project Id' operationId: getDatasetsByProjectId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: projectId + - + name: projectId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/datasets/{id}/history": + '500': + description: 'Internal server error' + '/datasets/{id}/history': get: tags: - Datasets - summary: "Get Dataset History" + summary: 'Get Dataset History' operationId: getDatasetHistory security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /riskHistory/timeseries: get: tags: - - "Risk History" - summary: "Get Timeseries" + - 'Risk History' + summary: 'Get Timeseries' operationId: riskHistory_getTimeseries security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /riskHistory/current-counts: get: tags: - - "Risk History" - summary: "Get Current Counts" + - 'Risk History' + summary: 'Get Current Counts' operationId: riskHistory_getCurrentCounts security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /riskHistory/snapshot: post: tags: - - "Risk History" - summary: "Create Snapshot" + - 'Risk History' + summary: 'Create Snapshot' operationId: riskHistory_createSnapshot security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /modelRisks: get: tags: - - "Model Risks" - summary: "Get All Model Risks" + - 'Model Risks' + summary: 'Get All Model Risks' operationId: getAllModelRisks security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: filter + - + name: filter in: query required: false schema: @@ -12602,83 +12966,88 @@ paths: - all default: active responses: - "200": + '200': description: Success content: application/json: schema: type: object properties: - message: { type: string } - data: { type: array, items: { $ref: "#/components/schemas/ModelRiskResponse" } } - "401": + message: {type: string} + data: {type: array, items: {$ref: '#/components/schemas/ModelRiskResponse'}} + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - - "Model Risks" - summary: "Create New Model Risk" + - 'Model Risks' + summary: 'Create New Model Risk' operationId: createNewModelRisk security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/ModelRiskInput" + $ref: '#/components/schemas/ModelRiskInput' responses: - "201": - description: "Created successfully" + '201': + description: 'Created successfully' content: application/json: schema: type: object properties: - message: { type: string } - data: { $ref: "#/components/schemas/ModelRiskResponse" } - "401": + message: {type: string} + data: {$ref: '#/components/schemas/ModelRiskResponse'} + '401': description: Unauthorized - "500": - description: "Internal server error" - "/modelRisks/{id}": + '500': + description: 'Internal server error' + '/modelRisks/{id}': get: tags: - - "Model Risks" - summary: "Get Model Risk By Id" + - 'Model Risks' + summary: 'Get Model Risk By Id' operationId: getModelRiskById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success content: application/json: schema: type: object properties: - message: { type: string } - data: { $ref: "#/components/schemas/ModelRiskResponse" } - "401": + message: {type: string} + data: {$ref: '#/components/schemas/ModelRiskResponse'} + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' put: tags: - - "Model Risks" - summary: "Update Model Risk By Id" + - 'Model Risks' + summary: 'Update Model Risk By Id' operationId: updateModelRiskById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -12688,30 +13057,32 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ModelRiskInput" + $ref: '#/components/schemas/ModelRiskInput' responses: - "200": + '200': description: Success content: application/json: schema: type: object properties: - message: { type: string } - data: { $ref: "#/components/schemas/ModelRiskResponse" } - "401": + message: {type: string} + data: {$ref: '#/components/schemas/ModelRiskResponse'} + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' patch: tags: - - "Model Risks" - summary: "Update Model Risk By Id" + - 'Model Risks' + summary: 'Update Model Risk By Id' operationId: modelRisks_updateModelRiskById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -12721,202 +13092,216 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/ModelRiskInput" + $ref: '#/components/schemas/ModelRiskInput' responses: - "200": + '200': description: Success content: application/json: schema: type: object properties: - message: { type: string } - data: { $ref: "#/components/schemas/ModelRiskResponse" } - "401": + message: {type: string} + data: {$ref: '#/components/schemas/ModelRiskResponse'} + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - - "Model Risks" - summary: "Delete Model Risk By Id" + - 'Model Risks' + summary: 'Delete Model Risk By Id' operationId: deleteModelRiskById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /reporting/generate-report: post: tags: - Reporting - summary: "Generate Reports" + summary: 'Generate Reports' operationId: generateReports security: - - bearerAuth: [] - description: "Requires role: Admin" + - + bearerAuth: [] + description: 'Requires role: Admin' requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' get: tags: - Reporting - summary: "Get All Generated Reports" + summary: 'Get All Generated Reports' operationId: getAllGeneratedReports security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /reporting/v2/generate-report: post: tags: - Reporting - summary: "Generate Reports V2" + summary: 'Generate Reports V2' operationId: generateReportsV2 security: - - bearerAuth: [] - description: "Requires role: Admin" + - + bearerAuth: [] + description: 'Requires role: Admin' requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" - "/reporting/{id}": + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' + '/reporting/{id}': delete: tags: - Reporting - summary: "Delete Generated Report By Id" + summary: 'Delete Generated Report By Id' operationId: deleteGeneratedReportById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /dashboard: get: tags: - Dashboard - summary: "Get Dashboard Data" + summary: 'Get Dashboard Data' operationId: getDashboardData security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/tiers/features/{id}": + '500': + description: 'Internal server error' + '/tiers/features/{id}': get: tags: - Subscriptions - summary: "Get Tiers Features" + summary: 'Get Tiers Features' operationId: getTiersFeatures security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /subscriptions: get: tags: - Subscriptions - summary: "Get Subscription Controller" + summary: 'Get Subscription Controller' operationId: getSubscriptionController security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - Subscriptions - summary: "Create Subscription Controller" + summary: 'Create Subscription Controller' operationId: createSubscriptionController security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/subscriptions/{id}": + '500': + description: 'Internal server error' + '/subscriptions/{id}': put: tags: - Subscriptions - summary: "Update Subscription Controller" + summary: 'Update Subscription Controller' operationId: updateSubscriptionController security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -12927,76 +13312,82 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /tasks: get: tags: - Tasks - summary: "Get All Tasks" + summary: 'Get All Tasks' operationId: getAllTasks security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - Tasks - summary: "Create Task" + summary: 'Create Task' operationId: createTask security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/tasks/{id}": + '500': + description: 'Internal server error' + '/tasks/{id}': get: tags: - Tasks - summary: "Get Task By Id" + summary: 'Get Task By Id' operationId: getTaskById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' put: tags: - Tasks - summary: "Update Task" + summary: 'Update Task' operationId: updateTask security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -13007,62 +13398,68 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - Tasks - summary: "Delete Task" + summary: 'Delete Task' operationId: deleteTask security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/tasks/{id}/entities": + '500': + description: 'Internal server error' + '/tasks/{id}/entities': get: tags: - Tasks - summary: "Get Task Entity Links" + summary: 'Get Task Entity Links' operationId: getTaskEntityLinks security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - Tasks - summary: "Add Task Entity Link" + summary: 'Add Task Entity Link' operationId: addTaskEntityLink security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -13073,36 +13470,39 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /tasks/bulk: patch: - summary: "Bulk Update Tasks" + summary: 'Bulk Update Tasks' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - Tasks operationId: bulkUpdateTasks security: - - bearerAuth: [] - description: "Requires role: Admin or Editor" - "/tasks/{id}/restore": + - + bearerAuth: [] + description: 'Requires role: Admin or Editor' + '/tasks/{id}/restore': put: tags: - Tasks - summary: "Restore Task" + summary: 'Restore Task' operationId: restoreTask security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -13113,81 +13513,88 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/tasks/{id}/hard": + '500': + description: 'Internal server error' + '/tasks/{id}/hard': delete: tags: - Tasks - summary: "Hard Delete Task" + summary: 'Hard Delete Task' operationId: hardDeleteTask security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/tasks/{id}/entities/{linkId}": + '500': + description: 'Internal server error' + '/tasks/{id}/entities/{linkId}': delete: tags: - Tasks - summary: "Remove Task Entity Link" + summary: 'Remove Task Entity Link' operationId: removeTaskEntityLink security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer - - name: linkId + - + name: linkId in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /deadlines/summary: get: - summary: "Get Deadlines Summary" + summary: 'Get Deadlines Summary' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - Deadlines operationId: getDeadlinesSummary security: - - bearerAuth: [] + - + bearerAuth: [] /policies/import/docx: post: tags: - Policies - summary: "Import DOCX and convert to HTML" + summary: 'Import DOCX and convert to HTML' description: "Uploads a .docx file (max 10 MB) and converts it to HTML suitable for the policy content editor. Returns the converted HTML and any conversion warnings.\n" operationId: PolicyController.importDocx security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: required: true content: @@ -13200,169 +13607,133 @@ paths: file: type: string format: binary - description: "A .docx file (max 10 MB)." + description: 'A .docx file (max 10 MB).' responses: - "200": - description: "DOCX converted to HTML successfully" + '200': + description: 'DOCX converted to HTML successfully' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: - { - type: object, - properties: - { - html: { type: string, description: "The converted HTML content" }, - warnings: - { - type: array, - items: { type: string }, - description: "Conversion warnings (e.g., unsupported formatting)", - }, - }, - } - "400": - description: "Bad request — no file uploaded or invalid file type" + message: {type: string, example: OK} + data: {type: object, properties: {html: {type: string, description: 'The converted HTML content'}, warnings: {type: array, items: {type: string}, description: 'Conversion warnings (e.g., unsupported formatting)'}}} + '400': + description: 'Bad request — no file uploaded or invalid file type' content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - "500": - $ref: "#/components/responses/InternalServerError" + $ref: '#/components/schemas/ErrorResponse' + '500': + $ref: '#/components/responses/InternalServerError' /policies/bulk: patch: summary: Unknown responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - Policies operationId: PolicyController.bulkUpdatePolicies security: - - bearerAuth: [] - description: "Requires role: Admin or Editor" + - + bearerAuth: [] + description: 'Requires role: Admin or Editor' /policies: get: tags: - Policies - summary: "Get all policies" - description: "Returns all policies for the authenticated user's organization, including assigned reviewer IDs." + summary: 'Get all policies' + description: 'Returns all policies for the authenticated user''s organization, including assigned reviewer IDs.' operationId: PolicyController.getAllPolicies security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": - description: "List of policies retrieved successfully" + '200': + description: 'List of policies retrieved successfully' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: { type: array, items: { $ref: "#/components/schemas/PolicyWithReviewers" } } - "500": - $ref: "#/components/responses/InternalServerError" + message: {type: string, example: OK} + data: {type: array, items: {$ref: '#/components/schemas/PolicyWithReviewers'}} + '500': + $ref: '#/components/responses/InternalServerError' post: tags: - Policies - summary: "Create a new policy" - description: "Creates a new policy. The author_id and last_updated_by are set from the JWT token automatically." + summary: 'Create a new policy' + description: 'Creates a new policy. The author_id and last_updated_by are set from the JWT token automatically.' operationId: PolicyController.createPolicy security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/PolicyCreateRequest" + $ref: '#/components/schemas/PolicyCreateRequest' responses: - "201": - description: "Policy created successfully" + '201': + description: 'Policy created successfully' content: application/json: schema: type: object properties: - message: { type: string, example: Created } - data: { $ref: "#/components/schemas/PolicyWithReviewers" } - "500": - $ref: "#/components/responses/InternalServerError" - "503": - description: "Service unavailable — policy creation failed" + message: {type: string, example: Created} + data: {$ref: '#/components/schemas/PolicyWithReviewers'} + '500': + $ref: '#/components/responses/InternalServerError' + '503': + description: 'Service unavailable — policy creation failed' content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" + $ref: '#/components/schemas/ErrorResponse' /policies/tags: get: tags: - Policies - summary: "Get available policy tags" - description: "Returns the static list of allowed policy tags." + summary: 'Get available policy tags' + description: 'Returns the static list of allowed policy tags.' operationId: PolicyController.getPolicyTags security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": - description: "List of available tags" + '200': + description: 'List of available tags' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: - { - type: array, - items: - { - type: string, - enum: - [ - "AI ethics", - Fairness, - Transparency, - Explainability, - "Bias mitigation", - Privacy, - "Data governance", - "Model risk", - Accountability, - Security, - LLM, - "Human oversight", - "EU AI Act", - "ISO 42001", - "NIST RMF", - "Red teaming", - Audit, - Monitoring, - "Vendor management", - ], - }, - } - "500": - $ref: "#/components/responses/InternalServerError" - "/policies/{id}/export/pdf": + message: {type: string, example: OK} + data: {type: array, items: {type: string, enum: ['AI ethics', Fairness, Transparency, Explainability, 'Bias mitigation', Privacy, 'Data governance', 'Model risk', Accountability, Security, LLM, 'Human oversight', 'EU AI Act', 'ISO 42001', 'NIST RMF', 'Red teaming', Audit, Monitoring, 'Vendor management']}} + '500': + $ref: '#/components/responses/InternalServerError' + '/policies/{id}/export/pdf': get: tags: - Policies - summary: "Export policy as PDF" - description: "Generates and downloads the policy as a PDF file." + summary: 'Export policy as PDF' + description: 'Generates and downloads the policy as a PDF file.' operationId: PolicyController.exportPolicyPDF security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - $ref: "#/components/parameters/PolicyId" + - + $ref: '#/components/parameters/PolicyId' responses: - "200": - description: "PDF file stream" + '200': + description: 'PDF file stream' content: application/pdf: schema: @@ -13376,30 +13747,32 @@ paths: Content-Length: schema: type: integer - "400": - description: "Invalid policy ID" + '400': + description: 'Invalid policy ID' content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - "404": - $ref: "#/components/responses/NotFound" - "500": - $ref: "#/components/responses/InternalServerError" - "/policies/{id}/export/docx": + $ref: '#/components/schemas/ErrorResponse' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + '/policies/{id}/export/docx': get: tags: - Policies - summary: "Export policy as DOCX" - description: "Generates and downloads the policy as a DOCX file." + summary: 'Export policy as DOCX' + description: 'Generates and downloads the policy as a DOCX file.' operationId: PolicyController.exportPolicyDOCX security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - $ref: "#/components/parameters/PolicyId" + - + $ref: '#/components/parameters/PolicyId' responses: - "200": - description: "DOCX file stream" + '200': + description: 'DOCX file stream' content: application/vnd.openxmlformats-officedocument.wordprocessingml.document: schema: @@ -13413,106 +13786,114 @@ paths: Content-Length: schema: type: integer - "400": - description: "Invalid policy ID" + '400': + description: 'Invalid policy ID' content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - "404": - $ref: "#/components/responses/NotFound" - "500": - $ref: "#/components/responses/InternalServerError" - "/policies/{id}": + $ref: '#/components/schemas/ErrorResponse' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + '/policies/{id}': get: tags: - Policies - summary: "Get policy by ID" - description: "Returns a single policy by its ID, including assigned reviewer IDs." + summary: 'Get policy by ID' + description: 'Returns a single policy by its ID, including assigned reviewer IDs.' operationId: PolicyController.getPolicyById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - $ref: "#/components/parameters/PolicyId" + - + $ref: '#/components/parameters/PolicyId' responses: - "200": - description: "Policy retrieved successfully" + '200': + description: 'Policy retrieved successfully' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: { $ref: "#/components/schemas/PolicyWithReviewers" } - "404": - $ref: "#/components/responses/NotFound" - "500": - $ref: "#/components/responses/InternalServerError" + message: {type: string, example: OK} + data: {$ref: '#/components/schemas/PolicyWithReviewers'} + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' put: tags: - Policies - summary: "Update a policy" + summary: 'Update a policy' description: "Updates an existing policy. Only provided fields are updated. The last_updated_by and last_updated_at are set automatically from the JWT token. If assigned_reviewer_ids is provided, the reviewer list is fully replaced.\n" operationId: PolicyController.updatePolicy security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - $ref: "#/components/parameters/PolicyId" + - + $ref: '#/components/parameters/PolicyId' requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/PolicyUpdateRequest" + $ref: '#/components/schemas/PolicyUpdateRequest' responses: - "202": - description: "Policy updated successfully" + '202': + description: 'Policy updated successfully' content: application/json: schema: type: object properties: - message: { type: string, example: Accepted } - data: { $ref: "#/components/schemas/PolicyWithReviewers" } - "404": - $ref: "#/components/responses/NotFound" - "500": - $ref: "#/components/responses/InternalServerError" + message: {type: string, example: Accepted} + data: {$ref: '#/components/schemas/PolicyWithReviewers'} + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' delete: tags: - Policies - summary: "Delete a policy by ID" - description: "Permanently deletes a policy and its associated reviewer mappings (via CASCADE)." + summary: 'Delete a policy by ID' + description: 'Permanently deletes a policy and its associated reviewer mappings (via CASCADE).' operationId: PolicyController.deletePolicyById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - $ref: "#/components/parameters/PolicyId" + - + $ref: '#/components/parameters/PolicyId' responses: - "202": - description: "Policy deleted successfully" + '202': + description: 'Policy deleted successfully' content: application/json: schema: type: object properties: - message: { type: string, example: Accepted } - data: { type: boolean, example: true } - "404": - $ref: "#/components/responses/NotFound" - "500": - $ref: "#/components/responses/InternalServerError" - "/policies/{id}/review/request": + message: {type: string, example: Accepted} + data: {type: boolean, example: true} + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + '/policies/{id}/review/request': post: tags: - Policies - summary: "Request review for a policy" + summary: 'Request review for a policy' description: "Sets the policy review status to pending_review and sends in-app notifications to each specified reviewer.\n" operationId: PolicyController.requestReview security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - $ref: "#/components/parameters/PolicyId" + - + $ref: '#/components/parameters/PolicyId' requestBody: required: true content: @@ -13524,44 +13905,46 @@ paths: properties: reviewer_ids: type: array - items: { type: integer } - description: "List of user IDs to request review from" + items: {type: integer} + description: 'List of user IDs to request review from' example: [2, 5, 8] message: type: string - description: "Optional message to include in the review request notification" - example: "Please review the updated data governance section." + description: 'Optional message to include in the review request notification' + example: 'Please review the updated data governance section.' responses: - "200": - description: "Review requested successfully; returns the updated policy" + '200': + description: 'Review requested successfully; returns the updated policy' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: { $ref: "#/components/schemas/PolicyWithReviewers" } - "400": - description: "Invalid policy ID or missing reviewer_ids" + message: {type: string, example: OK} + data: {$ref: '#/components/schemas/PolicyWithReviewers'} + '400': + description: 'Invalid policy ID or missing reviewer_ids' content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - "404": - $ref: "#/components/responses/NotFound" - "500": - $ref: "#/components/responses/InternalServerError" - "/policies/{id}/review/approve": + $ref: '#/components/schemas/ErrorResponse' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + '/policies/{id}/review/approve': put: tags: - Policies - summary: "Approve a policy review" + summary: 'Approve a policy review' description: "Sets the policy review status to approved and sends an in-app notification to the policy author.\n" operationId: PolicyController.approveReview security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - $ref: "#/components/parameters/PolicyId" + - + $ref: '#/components/parameters/PolicyId' requestBody: required: false content: @@ -13571,39 +13954,41 @@ paths: properties: comment: type: string - description: "Optional approval comment" - example: "Looks good, approved." + description: 'Optional approval comment' + example: 'Looks good, approved.' responses: - "200": - description: "Policy review approved; returns the updated policy" + '200': + description: 'Policy review approved; returns the updated policy' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: { $ref: "#/components/schemas/PolicyWithReviewers" } - "400": - description: "Invalid policy ID" + message: {type: string, example: OK} + data: {$ref: '#/components/schemas/PolicyWithReviewers'} + '400': + description: 'Invalid policy ID' content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - "404": - $ref: "#/components/responses/NotFound" - "500": - $ref: "#/components/responses/InternalServerError" - "/policies/{id}/review/reject": + $ref: '#/components/schemas/ErrorResponse' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + '/policies/{id}/review/reject': put: tags: - Policies - summary: "Reject a policy review (request changes)" + summary: 'Reject a policy review (request changes)' description: "Sets the policy review status to changes_requested and sends an in-app notification to the policy author. A comment is required.\n" operationId: PolicyController.rejectReview security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - $ref: "#/components/parameters/PolicyId" + - + $ref: '#/components/parameters/PolicyId' requestBody: required: true content: @@ -13615,79 +14000,85 @@ paths: properties: comment: type: string - description: "Reason for requesting changes (required)" - example: "Section 3 needs more detail on bias mitigation procedures." + description: 'Reason for requesting changes (required)' + example: 'Section 3 needs more detail on bias mitigation procedures.' responses: - "200": - description: "Policy review rejected; returns the updated policy" + '200': + description: 'Policy review rejected; returns the updated policy' content: application/json: schema: type: object properties: - message: { type: string, example: OK } - data: { $ref: "#/components/schemas/PolicyWithReviewers" } - "400": - description: "Invalid policy ID or missing comment" + message: {type: string, example: OK} + data: {$ref: '#/components/schemas/PolicyWithReviewers'} + '400': + description: 'Invalid policy ID or missing comment' content: application/json: schema: - $ref: "#/components/schemas/ErrorResponse" - "404": - $ref: "#/components/responses/NotFound" - "500": - $ref: "#/components/responses/InternalServerError" - "/policies/folders/{folderId}/policies": + $ref: '#/components/schemas/ErrorResponse' + '404': + $ref: '#/components/responses/NotFound' + '500': + $ref: '#/components/responses/InternalServerError' + '/policies/folders/{folderId}/policies': get: tags: - Policies - summary: "Get Policies In Folder" + summary: 'Get Policies In Folder' operationId: getPoliciesInFolder security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: folderId + - + name: folderId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/policies/{id}/folders": + '500': + description: 'Internal server error' + '/policies/{id}/folders': get: tags: - Policies - summary: "Get Policy Folders" + summary: 'Get Policy Folders' operationId: getPolicyFolders security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' patch: tags: - Policies - summary: "Update Policy Folders" + summary: 'Update Policy Folders' operationId: updatePolicyFolders security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -13698,76 +14089,82 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /slackWebhooks: get: tags: - Integrations - summary: "Get All Slack Webhooks" + summary: 'Get All Slack Webhooks' operationId: getAllSlackWebhooks security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - Integrations - summary: "Create New Slack Webhook" + summary: 'Create New Slack Webhook' operationId: createNewSlackWebhook security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/slackWebhooks/{id}": + '500': + description: 'Internal server error' + '/slackWebhooks/{id}': get: tags: - Integrations - summary: "Get Slack Webhook By Id" + summary: 'Get Slack Webhook By Id' operationId: getSlackWebhookById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' patch: tags: - Integrations - summary: "Update Slack Webhook By Id" + summary: 'Update Slack Webhook By Id' operationId: updateSlackWebhookById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -13778,42 +14175,46 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - Integrations - summary: "Delete Slack Webhook By Id" + summary: 'Delete Slack Webhook By Id' operationId: deleteSlackWebhookById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/slackWebhooks/{id}/send": + '500': + description: 'Internal server error' + '/slackWebhooks/{id}/send': post: tags: - Integrations - summary: "Send Slack Message" + summary: 'Send Slack Message' operationId: sendSlackMessage security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -13824,144 +14225,155 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /plugins/marketplace: get: tags: - Plugins - summary: "Get All Plugins" + summary: 'Get All Plugins' operationId: getAllPlugins security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/plugins/marketplace/{key}": + '500': + description: 'Internal server error' + '/plugins/marketplace/{key}': get: tags: - Plugins - summary: "Get Plugin By Key" + summary: 'Get Plugin By Key' operationId: getPluginByKey security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: key + - + name: key in: path required: true schema: type: string responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /plugins/marketplace/search: get: tags: - Plugins - summary: "Search Plugins" + summary: 'Search Plugins' operationId: searchPlugins security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /plugins/categories: get: tags: - Plugins - summary: "Get Categories" + summary: 'Get Categories' operationId: getCategories security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /plugins/install: post: tags: - Plugins - summary: "Install Plugin" + summary: 'Install Plugin' operationId: installPlugin security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/plugins/installations/{id}": + '500': + description: 'Internal server error' + '/plugins/installations/{id}': delete: tags: - Plugins - summary: "Uninstall Plugin" + summary: 'Uninstall Plugin' operationId: uninstallPlugin security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /plugins/installations: get: tags: - Plugins - summary: "Get Installed Plugins" + summary: 'Get Installed Plugins' operationId: getInstalledPlugins security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/plugins/installations/{id}/configuration": + '500': + description: 'Internal server error' + '/plugins/installations/{id}/configuration': put: tags: - Plugins - summary: "Update Plugin Configuration" + summary: 'Update Plugin Configuration' operationId: updatePluginConfiguration security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -13972,22 +14384,24 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/plugins/{key}/test-connection": + '500': + description: 'Internal server error' + '/plugins/{key}/test-connection': post: tags: - Plugins - summary: "Test Plugin Connection" + summary: 'Test Plugin Connection' operationId: testPluginConnection security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: key + - + name: key in: path required: true schema: @@ -13998,196 +14412,212 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/plugins/{key}/ui/dist/{filename}": + '500': + description: 'Internal server error' + '/plugins/{key}/ui/dist/{filename}': get: tags: - Plugins - summary: "Serve plugin UI assets" + summary: 'Serve plugin UI assets' operationId: plugins_anonymous security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: key + - + name: key in: path required: true schema: type: string - - name: filename + - + name: filename in: path required: true schema: type: string responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /tokens: get: tags: - Authentication - summary: "Get Api Tokens" + summary: 'Get Api Tokens' operationId: getApiTokens security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - Authentication - summary: "Create Api Token" + summary: 'Create Api Token' operationId: createApiToken security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/tokens/{id}/revoke": + '500': + description: 'Internal server error' + '/tokens/{id}/revoke': post: - summary: "Revoke Api Token" + summary: 'Revoke Api Token' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - Authentication operationId: revokeApiToken security: - - bearerAuth: [] - "/tokens/{id}": + - + bearerAuth: [] + '/tokens/{id}': delete: tags: - Authentication - summary: "Delete Api Token" + summary: 'Delete Api Token' operationId: deleteApiToken security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/shares/token/{token}": + '500': + description: 'Internal server error' + '/shares/token/{token}': get: tags: - - "Share Links" - summary: "Get Share Link By Token" + - 'Share Links' + summary: 'Get Share Link By Token' operationId: getShareLinkByToken parameters: - - name: token + - + name: token in: path required: true schema: type: string responses: - "200": + '200': description: Success - "500": - description: "Internal server error" - "/shares/view/{token}": + '500': + description: 'Internal server error' + '/shares/view/{token}': get: tags: - - "Share Links" - summary: "Get Shared Data By Token" + - 'Share Links' + summary: 'Get Shared Data By Token' operationId: getSharedDataByToken parameters: - - name: token + - + name: token in: path required: true schema: type: string responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /shares: post: tags: - - "Share Links" - summary: "Create Share Link" + - 'Share Links' + summary: 'Create Share Link' operationId: createShareLink security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/shares/{resourceType}/{resourceId}": + '500': + description: 'Internal server error' + '/shares/{resourceType}/{resourceId}': get: tags: - - "Share Links" - summary: "Get Share Links For Resource" + - 'Share Links' + summary: 'Get Share Links For Resource' operationId: getShareLinksForResource security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: resourceType + - + name: resourceType in: path required: true schema: type: string - - name: resourceId + - + name: resourceId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/shares/{id}": + '500': + description: 'Internal server error' + '/shares/{id}': patch: tags: - - "Share Links" - summary: "Update Share Link" + - 'Share Links' + summary: 'Update Share Link' operationId: updateShareLink security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -14198,177 +14628,191 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - - "Share Links" - summary: "Delete Share Link" + - 'Share Links' + summary: 'Delete Share Link' operationId: deleteShareLink security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /file-manager: post: tags: - Files - summary: "Upload File" + summary: 'Upload File' operationId: uploadFile security: - - bearerAuth: [] - description: "Requires role: Admin or Reviewer or Editor" + - + bearerAuth: [] + description: 'Requires role: Admin or Reviewer or Editor' requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' get: tags: - Files - summary: "List Files" + summary: 'List Files' operationId: listFiles security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /file-manager/search: get: tags: - Files - summary: "Search Files" + summary: 'Search Files' operationId: searchFiles security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /file-manager/with-metadata: get: tags: - Files - summary: "List Files With Metadata" + summary: 'List Files With Metadata' operationId: listFilesWithMetadata security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/file-manager/{id}": + '500': + description: 'Internal server error' + '/file-manager/{id}': get: tags: - Files - summary: "Download File" + summary: 'Download File' operationId: downloadFile security: - - bearerAuth: [] - description: "Requires role: Admin" + - + bearerAuth: [] + description: 'Requires role: Admin' parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' delete: tags: - Files - summary: "Remove File" + summary: 'Remove File' operationId: removeFile security: - - bearerAuth: [] - description: "Requires role: Admin or Reviewer or Editor" + - + bearerAuth: [] + description: 'Requires role: Admin or Reviewer or Editor' parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" - "/file-manager/{id}/metadata": + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' + '/file-manager/{id}/metadata': get: tags: - Files - summary: "Get File Metadata" + summary: 'Get File Metadata' operationId: getFileMetadata security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' patch: tags: - Files - summary: "Update Metadata" + summary: 'Update Metadata' operationId: updateMetadata security: - - bearerAuth: [] - description: "Requires role: Admin or Reviewer or Editor" + - + bearerAuth: [] + description: 'Requires role: Admin or Reviewer or Editor' parameters: - - name: id + - + name: id in: path required: true schema: @@ -14379,198 +14823,215 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" - "/file-manager/{id}/versions": + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' + '/file-manager/{id}/versions': get: tags: - Files - summary: "Get File Version History" + summary: 'Get File Version History' operationId: getFileVersionHistory security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/file-manager/{id}/preview": + '500': + description: 'Internal server error' + '/file-manager/{id}/preview': get: tags: - Files - summary: "Preview File" + summary: 'Preview File' operationId: previewFile security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /automations: get: tags: - Automations - summary: "Get All Automations" + summary: 'Get All Automations' operationId: getAllAutomations security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - Automations - summary: "Create Automation" + summary: 'Create Automation' operationId: createAutomation security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /automations/triggers: get: tags: - Automations - summary: "Get All Automation Triggers" + summary: 'Get All Automation Triggers' operationId: getAllAutomationTriggers security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/automations/actions/by-triggerId/{triggerId}": + '500': + description: 'Internal server error' + '/automations/actions/by-triggerId/{triggerId}': get: tags: - Automations - summary: "Get All Automation Actions By Trigger Id" + summary: 'Get All Automation Actions By Trigger Id' operationId: getAllAutomationActionsByTriggerId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: triggerId + - + name: triggerId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/automations/{id}/history": + '500': + description: 'Internal server error' + '/automations/{id}/history': get: tags: - Automations - summary: "Get Automation History" + summary: 'Get Automation History' operationId: getAutomationHistory security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/automations/{id}/stats": + '500': + description: 'Internal server error' + '/automations/{id}/stats': get: tags: - Automations - summary: "Get Automation Stats" + summary: 'Get Automation Stats' operationId: getAutomationStats security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/automations/{id}": + '500': + description: 'Internal server error' + '/automations/{id}': get: tags: - Automations - summary: "Get Automation By Id" + summary: 'Get Automation By Id' operationId: getAutomationById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' put: tags: - Automations - summary: "Update Automation" + summary: 'Update Automation' operationId: updateAutomation security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -14581,62 +15042,68 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - Automations - summary: "Delete Automation By Id" + summary: 'Delete Automation By Id' operationId: deleteAutomationById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/user-preferences/{userId}": + '500': + description: 'Internal server error' + '/user-preferences/{userId}': get: tags: - Users - summary: "Get Preferences By User" + summary: 'Get Preferences By User' operationId: getPreferencesByUser security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: userId + - + name: userId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' patch: tags: - Users - summary: "Update User Preferences" + summary: 'Update User Preferences' operationId: updateUserPreferences security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: userId + - + name: userId in: path required: true schema: @@ -14647,112 +15114,120 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /user-preferences: post: tags: - Users - summary: "Create User Preferences" + summary: 'Create User Preferences' operationId: createUserPreferences security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /llm-keys: get: tags: - - "LLM Keys" - summary: "Get L L M Keys" + - 'LLM Keys' + summary: 'Get L L M Keys' operationId: getLLMKeys security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - - "LLM Keys" - summary: "Create L L M Key" + - 'LLM Keys' + summary: 'Create L L M Key' operationId: createLLMKey security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /llm-keys/status: get: tags: - - "LLM Keys" - summary: "Get L L M Key Status" + - 'LLM Keys' + summary: 'Get L L M Key Status' operationId: getLLMKeyStatus security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/llm-keys/{name}": + '500': + description: 'Internal server error' + '/llm-keys/{name}': get: tags: - - "LLM Keys" - summary: "Get L L M Key" + - 'LLM Keys' + summary: 'Get L L M Key' operationId: getLLMKey security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: name + - + name: name in: path required: true schema: type: string responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/llm-keys/{id}": + '500': + description: 'Internal server error' + '/llm-keys/{id}': patch: tags: - - "LLM Keys" - summary: "Update L L M Key" + - 'LLM Keys' + summary: 'Update L L M Key' operationId: updateLLMKey security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -14763,167 +15238,183 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - - "LLM Keys" - summary: "Delete L L M Key" + - 'LLM Keys' + summary: 'Delete L L M Key' operationId: deleteLLMKey security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /nist-ai-rmf/functions: get: tags: - - "NIST AI RMF" - summary: "Get All N I S T A I R M Ffunctions" + - 'NIST AI RMF' + summary: 'Get All N I S T A I R M Ffunctions' operationId: getAllNISTAIRMFfunctions security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/nist-ai-rmf/functions/{id}": + '500': + description: 'Internal server error' + '/nist-ai-rmf/functions/{id}': get: tags: - - "NIST AI RMF" - summary: "Get N I S T A I R M Ffunction By Id" + - 'NIST AI RMF' + summary: 'Get N I S T A I R M Ffunction By Id' operationId: getNISTAIRMFfunctionById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/nist-ai-rmf/categories/{title}": + '500': + description: 'Internal server error' + '/nist-ai-rmf/categories/{title}': get: tags: - - "NIST AI RMF" - summary: "Get All N I S T A I R M F Categories Byfunction Id" + - 'NIST AI RMF' + summary: 'Get All N I S T A I R M F Categories Byfunction Id' operationId: getAllNISTAIRMFCategoriesByfunctionId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: title + - + name: title in: path required: true schema: type: string responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/nist-ai-rmf/subcategories/byId/{id}": + '500': + description: 'Internal server error' + '/nist-ai-rmf/subcategories/byId/{id}': get: tags: - - "NIST AI RMF" - summary: "Get N I S T A I R M F Subcategory By Id" + - 'NIST AI RMF' + summary: 'Get N I S T A I R M F Subcategory By Id' operationId: getNISTAIRMFSubcategoryById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/nist-ai-rmf/subcategories/{id}/risks": + '500': + description: 'Internal server error' + '/nist-ai-rmf/subcategories/{id}/risks': get: tags: - - "NIST AI RMF" - summary: "Get N I S T A I R M F Subcategory Risks" + - 'NIST AI RMF' + summary: 'Get N I S T A I R M F Subcategory Risks' operationId: getNISTAIRMFSubcategoryRisks security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/nist-ai-rmf/subcategories/{categoryId}/{title}": + '500': + description: 'Internal server error' + '/nist-ai-rmf/subcategories/{categoryId}/{title}': get: tags: - - "NIST AI RMF" - summary: "Get All N I S T A I R M F Subcategories Bycategory Id Andtitle" + - 'NIST AI RMF' + summary: 'Get All N I S T A I R M F Subcategories Bycategory Id Andtitle' operationId: getAllNISTAIRMFSubcategoriesBycategoryIdAndtitle security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: categoryId + - + name: categoryId in: path required: true schema: type: integer - - name: title + - + name: title in: path required: true schema: type: string responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/nist-ai-rmf/subcategories/{id}": + '500': + description: 'Internal server error' + '/nist-ai-rmf/subcategories/{id}': patch: tags: - - "NIST AI RMF" - summary: "Update N I S T A I R M F Subcategory By Id" + - 'NIST AI RMF' + summary: 'Update N I S T A I R M F Subcategory By Id' operationId: updateNISTAIRMFSubcategoryById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -14934,22 +15425,24 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/nist-ai-rmf/subcategories/{id}/status": + '500': + description: 'Internal server error' + '/nist-ai-rmf/subcategories/{id}/status': patch: tags: - - "NIST AI RMF" - summary: "Update N I S T A I R M F Subcategory Status" + - 'NIST AI RMF' + summary: 'Update N I S T A I R M F Subcategory Status' operationId: updateNISTAIRMFSubcategoryStatus security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -14960,166 +15453,178 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /nist-ai-rmf/progress: get: tags: - - "NIST AI RMF" - summary: "Get N I S T A I R M F Progress" + - 'NIST AI RMF' + summary: 'Get N I S T A I R M F Progress' operationId: getNISTAIRMFProgress security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /nist-ai-rmf/progress-by-function: get: tags: - - "NIST AI RMF" - summary: "Get N I S T A I R M F Progress By Function" + - 'NIST AI RMF' + summary: 'Get N I S T A I R M F Progress By Function' operationId: getNISTAIRMFProgressByFunction security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /nist-ai-rmf/assignments: get: tags: - - "NIST AI RMF" - summary: "Get N I S T A I R M F Assignments" + - 'NIST AI RMF' + summary: 'Get N I S T A I R M F Assignments' operationId: getNISTAIRMFAssignments security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /nist-ai-rmf/assignments-by-function: get: tags: - - "NIST AI RMF" - summary: "Get N I S T A I R M F Assignments By Function" + - 'NIST AI RMF' + summary: 'Get N I S T A I R M F Assignments By Function' operationId: getNISTAIRMFAssignmentsByFunction security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /nist-ai-rmf/status-breakdown: get: tags: - - "NIST AI RMF" - summary: "Get N I S T A I R M F Status Breakdown" + - 'NIST AI RMF' + summary: 'Get N I S T A I R M F Status Breakdown' operationId: getNISTAIRMFStatusBreakdown security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /nist-ai-rmf/overview: get: tags: - - "NIST AI RMF" - summary: "Get N I S T A I R M F Overview" + - 'NIST AI RMF' + summary: 'Get N I S T A I R M F Overview' operationId: getNISTAIRMFOverview security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /evidenceHub: get: tags: - Evidence - summary: "Get All Evidences" + summary: 'Get All Evidences' operationId: getAllEvidences security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - Evidence - summary: "Create New Evidence" + summary: 'Create New Evidence' operationId: createNewEvidence security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/evidenceHub/{id}": + '500': + description: 'Internal server error' + '/evidenceHub/{id}': get: tags: - Evidence - summary: "Get Evidence By Id" + summary: 'Get Evidence By Id' operationId: getEvidenceById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' patch: tags: - Evidence - summary: "Update Evidence By Id" + summary: 'Update Evidence By Id' operationId: updateEvidenceById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -15130,772 +15635,833 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - Evidence - summary: "Delete Evidence By Id" + summary: 'Delete Evidence By Id' operationId: deleteEvidenceById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/evidence-ai/analyze/{fileId}": + '500': + description: 'Internal server error' + '/evidence-ai/analyze/{fileId}': post: - summary: "Analyze File" + summary: 'Analyze File' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Evidence AI" + - 'Evidence AI' operationId: analyzeFile security: - - bearerAuth: [] - "/evidence-ai/analysis/{fileId}": + - + bearerAuth: [] + '/evidence-ai/analysis/{fileId}': get: - summary: "Get Analysis" + summary: 'Get Analysis' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Evidence AI" + - 'Evidence AI' operationId: getAnalysis security: - - bearerAuth: [] + - + bearerAuth: [] /evidence-ai/quality-scores: get: - summary: "Get Quality Scores" + summary: 'Get Quality Scores' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Evidence AI" + - 'Evidence AI' operationId: getQualityScores security: - - bearerAuth: [] + - + bearerAuth: [] /evidence-ai/gaps: get: - summary: "Get Gaps" + summary: 'Get Gaps' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Evidence AI" + - 'Evidence AI' operationId: getGaps security: - - bearerAuth: [] - "/evidence-ai/suggestions/{fileId}": + - + bearerAuth: [] + '/evidence-ai/suggestions/{fileId}': get: - summary: "Get Suggestions" + summary: 'Get Suggestions' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Evidence AI" + - 'Evidence AI' operationId: getSuggestions security: - - bearerAuth: [] - "/evidence-ai/suggestions/{fileId}/apply": + - + bearerAuth: [] + '/evidence-ai/suggestions/{fileId}/apply': post: - summary: "Apply Suggestions" + summary: 'Apply Suggestions' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Evidence AI" + - 'Evidence AI' operationId: applySuggestions security: - - bearerAuth: [] + - + bearerAuth: [] /readiness/calculate: post: - summary: "Calculate All" + summary: 'Calculate All' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - Readiness operationId: calculateAll security: - - bearerAuth: [] - "/readiness/calculate/{frameworkType}": + - + bearerAuth: [] + '/readiness/calculate/{frameworkType}': post: - summary: "Calculate For Framework" + summary: 'Calculate For Framework' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - Readiness operationId: calculateForFramework security: - - bearerAuth: [] + - + bearerAuth: [] /readiness/scores: get: - summary: "Get Scores" + summary: 'Get Scores' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - Readiness operationId: getScores security: - - bearerAuth: [] - "/readiness/scores/{frameworkType}": + - + bearerAuth: [] + '/readiness/scores/{frameworkType}': get: - summary: "Get Scores By Framework" + summary: 'Get Scores By Framework' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - Readiness operationId: getScoresByFramework security: - - bearerAuth: [] - "/readiness/controls/{frameworkType}": + - + bearerAuth: [] + '/readiness/controls/{frameworkType}': get: - summary: "Get Control Scores" + summary: 'Get Control Scores' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - Readiness operationId: getControlScores security: - - bearerAuth: [] + - + bearerAuth: [] /readiness/weakest: get: - summary: "Get Weakest" + summary: 'Get Weakest' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - Readiness operationId: getWeakest security: - - bearerAuth: [] + - + bearerAuth: [] /readiness/recommendations: get: - summary: "Get Recommendations" + summary: 'Get Recommendations' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - Readiness operationId: getRecommendations security: - - bearerAuth: [] + - + bearerAuth: [] /readiness/history: get: - summary: "Get History" + summary: 'Get History' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - Readiness operationId: getHistory security: - - bearerAuth: [] + - + bearerAuth: [] /ai-content/stats: get: - summary: "Get Stats" + summary: 'Get Stats' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Content" + - 'AI Content' operationId: getStats security: - - bearerAuth: [] + - + bearerAuth: [] /ai-content/unreviewed: get: - summary: "Get Unreviewed" + summary: 'Get Unreviewed' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Content" + - 'AI Content' operationId: getUnreviewed security: - - bearerAuth: [] - "/ai-content/{entityType}/{entityId}": + - + bearerAuth: [] + '/ai-content/{entityType}/{entityId}': get: - summary: "Get Badges" + summary: 'Get Badges' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Content" + - 'AI Content' operationId: getBadges security: - - bearerAuth: [] - "/ai-content/{id}/review": + - + bearerAuth: [] + '/ai-content/{id}/review': patch: - summary: "Review Content" + summary: 'Review Content' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Content" + - 'AI Content' operationId: reviewContent security: - - bearerAuth: [] - "/ai-confirmation/approve/{id}": + - + bearerAuth: [] + '/ai-confirmation/approve/{id}': post: - summary: "Approve Confirmation" + summary: 'Approve Confirmation' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Confirmation" + - 'AI Confirmation' operationId: approveConfirmation security: - - bearerAuth: [] - "/ai-confirmation/reject/{id}": + - + bearerAuth: [] + '/ai-confirmation/reject/{id}': post: - summary: "Reject Confirmation" + summary: 'Reject Confirmation' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Confirmation" + - 'AI Confirmation' operationId: rejectConfirmation security: - - bearerAuth: [] + - + bearerAuth: [] /ai-confirmation/pending: get: - summary: "Get Pending Confirmations" + summary: 'Get Pending Confirmations' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Confirmation" + - 'AI Confirmation' operationId: getPendingConfirmations security: - - bearerAuth: [] + - + bearerAuth: [] /ai-approvals/stats: get: - summary: "Get Approval Stats Ctrl" + summary: 'Get Approval Stats Ctrl' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Approvals" + - 'AI Approvals' operationId: getApprovalStatsCtrl security: - - bearerAuth: [] + - + bearerAuth: [] /ai-approvals: get: - summary: "List Approvals Ctrl" + summary: 'List Approvals Ctrl' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Approvals" + - 'AI Approvals' operationId: listApprovalsCtrl security: - - bearerAuth: [] - "/ai-approvals/{id}": + - + bearerAuth: [] + '/ai-approvals/{id}': get: - summary: "Get Approval Detail Ctrl" + summary: 'Get Approval Detail Ctrl' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Approvals" + - 'AI Approvals' operationId: getApprovalDetailCtrl security: - - bearerAuth: [] - "/ai-approvals/{id}/approve": + - + bearerAuth: [] + '/ai-approvals/{id}/approve': post: - summary: "Approve Approval Ctrl" + summary: 'Approve Approval Ctrl' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Approvals" + - 'AI Approvals' operationId: approveApprovalCtrl security: - - bearerAuth: [] - "/ai-approvals/{id}/reject": + - + bearerAuth: [] + '/ai-approvals/{id}/reject': post: - summary: "Reject Approval Ctrl" + summary: 'Reject Approval Ctrl' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Approvals" + - 'AI Approvals' operationId: rejectApprovalCtrl security: - - bearerAuth: [] + - + bearerAuth: [] /ai-approval-rules/test: post: - summary: "Test Rule Ctrl" + summary: 'Test Rule Ctrl' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Approval Rules" + - 'AI Approval Rules' operationId: testRuleCtrl security: - - bearerAuth: [] + - + bearerAuth: [] /ai-approval-rules: get: - summary: "List Rules Ctrl" + summary: 'List Rules Ctrl' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Approval Rules" + - 'AI Approval Rules' operationId: listRulesCtrl security: - - bearerAuth: [] + - + bearerAuth: [] post: - summary: "Create Rule Ctrl" + summary: 'Create Rule Ctrl' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Approval Rules" + - 'AI Approval Rules' operationId: createRuleCtrl security: - - bearerAuth: [] - "/ai-approval-rules/{id}": + - + bearerAuth: [] + '/ai-approval-rules/{id}': put: - summary: "Update Rule Ctrl" + summary: 'Update Rule Ctrl' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Approval Rules" + - 'AI Approval Rules' operationId: updateRuleCtrl security: - - bearerAuth: [] + - + bearerAuth: [] delete: - summary: "Delete Rule Ctrl" + summary: 'Delete Rule Ctrl' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Approval Rules" + - 'AI Approval Rules' operationId: deleteRuleCtrl security: - - bearerAuth: [] + - + bearerAuth: [] /ai-apps: get: - summary: "Get All Ai Apps" + summary: 'Get All Ai Apps' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Apps" + - 'AI Apps' operationId: getAllAiApps security: - - bearerAuth: [] + - + bearerAuth: [] post: - summary: "Create Ai App" + summary: 'Create Ai App' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Apps" + - 'AI Apps' operationId: createAiApp security: - - bearerAuth: [] + - + bearerAuth: [] /ai-apps/policy-suggestions: get: - summary: "Get Policy Suggestions" + summary: 'Get Policy Suggestions' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Apps" + - 'AI Apps' operationId: getPolicySuggestions security: - - bearerAuth: [] - "/ai-apps/{id}": + - + bearerAuth: [] + '/ai-apps/{id}': get: - summary: "Get Ai App By Id" + summary: 'Get Ai App By Id' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Apps" + - 'AI Apps' operationId: getAiAppById security: - - bearerAuth: [] + - + bearerAuth: [] patch: - summary: "Update Ai App By Id" + summary: 'Update Ai App By Id' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Apps" + - 'AI Apps' operationId: updateAiAppById security: - - bearerAuth: [] + - + bearerAuth: [] delete: - summary: "Delete Ai App By Id" + summary: 'Delete Ai App By Id' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Apps" + - 'AI Apps' operationId: deleteAiAppById security: - - bearerAuth: [] - "/ai-apps/{id}/models": + - + bearerAuth: [] + '/ai-apps/{id}/models': post: - summary: "Link Models To Ai App" + summary: 'Link Models To Ai App' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Apps" + - 'AI Apps' operationId: linkModelsToAiApp security: - - bearerAuth: [] - "/ai-apps/{id}/policies": + - + bearerAuth: [] + '/ai-apps/{id}/policies': post: - summary: "Set Policies For Ai App" + summary: 'Set Policies For Ai App' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Apps" + - 'AI Apps' operationId: setPoliciesForAiApp security: - - bearerAuth: [] - "/ai-apps/{id}/data-exposure": + - + bearerAuth: [] + '/ai-apps/{id}/data-exposure': post: - summary: "Set Data Exposure For Ai App" + summary: 'Set Data Exposure For Ai App' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Apps" + - 'AI Apps' operationId: setDataExposureForAiApp security: - - bearerAuth: [] - "/ai-apps/from-shadow-ai/{shadowAiToolId}": + - + bearerAuth: [] + '/ai-apps/from-shadow-ai/{shadowAiToolId}': post: - summary: "Promote From Shadow Ai" + summary: 'Promote From Shadow Ai' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Apps" + - 'AI Apps' operationId: promoteFromShadowAi security: - - bearerAuth: [] - "/ai-apps/{id}/status": + - + bearerAuth: [] + '/ai-apps/{id}/status': patch: - summary: "Update Ai App Status" + summary: 'Update Ai App Status' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Apps" + - 'AI Apps' operationId: updateAiAppStatus security: - - bearerAuth: [] + - + bearerAuth: [] /ai-audit/analytics: get: - summary: "Get Analytics" + summary: 'Get Analytics' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Audit" + - 'AI Audit' operationId: getAnalytics security: - - bearerAuth: [] + - + bearerAuth: [] /ai-audit/export: get: - summary: "Export Audit Log" + summary: 'Export Audit Log' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Audit" + - 'AI Audit' operationId: exportAuditLog security: - - bearerAuth: [] - "/ai-audit/log/{actionId}": + - + bearerAuth: [] + '/ai-audit/log/{actionId}': get: - summary: "Get Action Audit Trail" + summary: 'Get Action Audit Trail' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Audit" + - 'AI Audit' operationId: getActionAuditTrail security: - - bearerAuth: [] + - + bearerAuth: [] /ai-audit/log: get: - summary: "Get Audit Log" + summary: 'Get Audit Log' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Audit" + - 'AI Audit' operationId: getAuditLog security: - - bearerAuth: [] + - + bearerAuth: [] /advisor: post: tags: - - "AI Advisor" - summary: "Run Advisor" + - 'AI Advisor' + summary: 'Run Advisor' operationId: runAdvisor security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /advisor/stream: post: tags: - - "AI Advisor" - summary: "Stream Advisor" + - 'AI Advisor' + summary: 'Stream Advisor' operationId: streamAdvisor security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /advisor/chat: post: tags: - - "AI Advisor" - summary: "Stream Advisor V2" + - 'AI Advisor' + summary: 'Stream Advisor V2' operationId: streamAdvisorV2 security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/advisor/conversations/{domain}": + '500': + description: 'Internal server error' + '/advisor/conversations/{domain}': get: tags: - - "AI Advisor" - summary: "List conversations for a domain" - description: "Returns all conversations the current user has in the given advisor domain, most recent first. Lightweight summaries only — no message bodies." + - 'AI Advisor' + summary: 'List conversations for a domain' + description: 'Returns all conversations the current user has in the given advisor domain, most recent first. Lightweight summaries only — no message bodies.' operationId: listConversations security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: domain + - + name: domain in: path required: true schema: type: string responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - - "AI Advisor" - summary: "Create a new empty conversation" - description: "Creates a fresh empty conversation in the given domain. Title is derived automatically when the first user message is saved." + - 'AI Advisor' + summary: 'Create a new empty conversation' + description: 'Creates a fresh empty conversation in the given domain. Title is derived automatically when the first user message is saved.' operationId: createConversation security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: domain + - + name: domain in: path required: true schema: type: string responses: - "201": - description: "Conversation created" - "401": + '201': + description: 'Conversation created' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/advisor/conversations/{domain}/{id}": + '500': + description: 'Internal server error' + '/advisor/conversations/{domain}/{id}': get: tags: - - "AI Advisor" - summary: "Get a single conversation" - description: "Returns the full conversation including its messages array." + - 'AI Advisor' + summary: 'Get a single conversation' + description: 'Returns the full conversation including its messages array.' operationId: getConversationById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: domain + - + name: domain in: path required: true schema: type: string - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "404": - description: "Conversation not found" - "500": - description: "Internal server error" + '404': + description: 'Conversation not found' + '500': + description: 'Internal server error' put: tags: - - "AI Advisor" - summary: "Update conversation messages" - description: "Replaces the messages array of an existing conversation. Bumps last_message_at and auto-derives the title on first save." + - 'AI Advisor' + summary: 'Update conversation messages' + description: 'Replaces the messages array of an existing conversation. Bumps last_message_at and auto-derives the title on first save.' operationId: updateConversation security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: domain + - + name: domain in: path required: true schema: type: string - - name: id + - + name: id in: path required: true schema: @@ -15908,138 +16474,150 @@ paths: properties: messages: type: array - items: { type: object } + items: {type: object} responses: - "200": - description: "Updated successfully" - "401": + '200': + description: 'Updated successfully' + '401': description: Unauthorized - "404": - description: "Conversation not found" - "500": - description: "Internal server error" + '404': + description: 'Conversation not found' + '500': + description: 'Internal server error' delete: tags: - - "AI Advisor" - summary: "Delete a conversation" + - 'AI Advisor' + summary: 'Delete a conversation' operationId: deleteConversation security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: domain + - + name: domain in: path required: true schema: type: string - - name: id + - + name: id in: path required: true schema: type: integer responses: - "204": + '204': description: Deleted - "401": + '401': description: Unauthorized - "404": - description: "Conversation not found" - "500": - description: "Internal server error" + '404': + description: 'Conversation not found' + '500': + description: 'Internal server error' /advisor/memory: get: - summary: "Get Memory Summary" + summary: 'Get Memory Summary' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Advisor" + - 'AI Advisor' operationId: getMemorySummary security: - - bearerAuth: [] + - + bearerAuth: [] delete: - summary: "Delete My Memory" + summary: 'Delete My Memory' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Advisor" + - 'AI Advisor' operationId: deleteMyMemory security: - - bearerAuth: [] - "/advisor/memory/admin/agent/{agentName}": + - + bearerAuth: [] + '/advisor/memory/admin/agent/{agentName}': get: - summary: "Admin List Agent Messages" + summary: 'Admin List Agent Messages' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Advisor" + - 'AI Advisor' operationId: adminListAgentMessages security: - - bearerAuth: [] + - + bearerAuth: [] delete: - summary: "Admin Clear Agent Memory" + summary: 'Admin Clear Agent Memory' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Advisor" + - 'AI Advisor' operationId: adminClearAgentMemory security: - - bearerAuth: [] + - + bearerAuth: [] /policy-linked: get: tags: - Policies - summary: "Get All Linked Objects" + summary: 'Get All Linked Objects' operationId: getAllLinkedObjects security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/policy-linked/{policyId}/linked-objects": + '500': + description: 'Internal server error' + '/policy-linked/{policyId}/linked-objects': get: tags: - Policies - summary: "Get Linked Objects For Policy" + summary: 'Get Linked Objects For Policy' operationId: getLinkedObjects security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: policyId + - + name: policyId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - Policies - summary: "Create Linked Object For Policy" + summary: 'Create Linked Object For Policy' operationId: createLinkedObject security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: policyId + - + name: policyId in: path required: true schema: @@ -16050,21 +16628,23 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - Policies - summary: "Delete Linked Object For Policy" + summary: 'Delete Linked Object For Policy' operationId: deleteLinkedObject security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: policyId + - + name: policyId in: path required: true schema: @@ -16075,118 +16655,128 @@ paths: schema: type: object responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/policy-linked/risk/{riskId}/unlink-all": + '500': + description: 'Internal server error' + '/policy-linked/risk/{riskId}/unlink-all': delete: tags: - Policies - summary: "Unlink Risk From All Policies" + summary: 'Unlink Risk From All Policies' operationId: deleteRiskFromAllPolicies security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: riskId + - + name: riskId in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/policy-linked/evidence/{evidenceId}/unlink-all": + '500': + description: 'Internal server error' + '/policy-linked/evidence/{evidenceId}/unlink-all': delete: tags: - Policies - summary: "Unlink Evidence From All Policies" + summary: 'Unlink Evidence From All Policies' operationId: deleteEvidenceFromAllPolicies security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: evidenceId + - + name: evidenceId in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /ai-incident-managements: get: tags: - Incidents - summary: "Get All Incidents" + summary: 'Get All Incidents' operationId: getAllIncidents security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - Incidents - summary: "Create New Incident" + summary: 'Create New Incident' operationId: createNewIncident security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/ai-incident-managements/{id}": + '500': + description: 'Internal server error' + '/ai-incident-managements/{id}': get: tags: - Incidents - summary: "Get Incident By Id" + summary: 'Get Incident By Id' operationId: getIncidentById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' patch: tags: - Incidents - summary: "Update Incident By Id" + summary: 'Update Incident By Id' operationId: updateIncidentById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -16197,42 +16787,46 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - Incidents - summary: "Delete Incident By Id" + summary: 'Delete Incident By Id' operationId: deleteIncidentById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/ai-incident-managements/{id}/archive": + '500': + description: 'Internal server error' + '/ai-incident-managements/{id}/archive': patch: tags: - Incidents - summary: "Archive Incident By Id" + summary: 'Archive Incident By Id' operationId: archiveIncidentById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -16243,42 +16837,46 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/ce-marking/{projectId}": + '500': + description: 'Internal server error' + '/ce-marking/{projectId}': get: tags: - - "CE Marking" - summary: "Get C E Marking" + - 'CE Marking' + summary: 'Get C E Marking' operationId: getCEMarking security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: projectId + - + name: projectId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' put: tags: - - "CE Marking" - summary: "Update C E Marking" + - 'CE Marking' + summary: 'Update C E Marking' operationId: updateCEMarking security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: projectId + - + name: projectId in: path required: true schema: @@ -16289,12 +16887,12 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /search: get: tags: @@ -16302,50 +16900,54 @@ paths: summary: Search operationId: search security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /deepeval/playground/chat: post: summary: Provider responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "LLM Evals" + - 'LLM Evals' operationId: provider security: - - bearerAuth: [] + - + bearerAuth: [] /evaluation-llm-keys: get: tags: - - "LLM Evals" - summary: "Get All Evaluation LLM Keys" + - 'LLM Evals' + summary: 'Get All Evaluation LLM Keys' operationId: evalKeysRetired security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - - "LLM Evals" - summary: "Add Evaluation LLM Key" + - 'LLM Evals' + summary: 'Add Evaluation LLM Key' operationId: evaluation_llm_keys_evalKeysRetired security: - - bearerAuth: [] - description: "Requires role: Admin" + - + bearerAuth: [] + description: 'Requires role: Admin' requestBody: required: true content: @@ -16362,23 +16964,24 @@ paths: apiKey: type: string responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' /evaluation-llm-keys/verify: post: tags: - - "LLM Evals" - summary: "Verify Evaluation LLM Key" + - 'LLM Evals' + summary: 'Verify Evaluation LLM Key' operationId: evaluation_llm_keys_evalKeysRetired security: - - bearerAuth: [] - description: "Requires role: Admin" + - + bearerAuth: [] + description: 'Requires role: Admin' requestBody: required: true content: @@ -16395,25 +16998,27 @@ paths: apiKey: type: string responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" - "/evaluation-llm-keys/{provider}": + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' + '/evaluation-llm-keys/{provider}': delete: tags: - - "LLM Evals" - summary: "Delete Evaluation LLM Key" + - 'LLM Evals' + summary: 'Delete Evaluation LLM Key' operationId: evaluation_llm_keys_evalKeysRetired security: - - bearerAuth: [] - description: "Requires role: Admin" + - + bearerAuth: [] + description: 'Requires role: Admin' parameters: - - name: provider + - + name: provider in: path required: true schema: @@ -16426,69 +17031,73 @@ paths: - mistral - huggingface responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' /evaluation-llm-keys/internal/decrypted: get: - summary: "Eval Keys Retired" + summary: 'Eval Keys Retired' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "LLM Evals" + - 'LLM Evals' operationId: evaluation_llm_keys_evalKeysRetired /notes: post: tags: - Notes - summary: "Create Note" + summary: 'Create Note' operationId: createNote security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' get: tags: - Notes - summary: "Get Notes" + summary: 'Get Notes' operationId: getNotes security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/notes/{id}": + '500': + description: 'Internal server error' + '/notes/{id}': put: tags: - Notes - summary: "Update Note" + summary: 'Update Note' operationId: updateNote security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -16499,203 +17108,221 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - Notes - summary: "Delete Note" + summary: 'Delete Note' operationId: deleteNote security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /entity-graph/annotations: post: tags: - - "Entity Graph" - summary: "Save Annotation" + - 'Entity Graph' + summary: 'Save Annotation' operationId: saveAnnotation security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' get: tags: - - "Entity Graph" - summary: "Get Annotations" + - 'Entity Graph' + summary: 'Get Annotations' operationId: getAnnotations security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/entity-graph/annotations/{entityType}/{entityId}": + '500': + description: 'Internal server error' + '/entity-graph/annotations/{entityType}/{entityId}': get: tags: - - "Entity Graph" - summary: "Get Annotation By Entity" + - 'Entity Graph' + summary: 'Get Annotation By Entity' operationId: getAnnotationByEntity security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: entityType + - + name: entityType in: path required: true schema: type: string - - name: entityId + - + name: entityId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/entity-graph/annotations/{id}": + '500': + description: 'Internal server error' + '/entity-graph/annotations/{id}': delete: tags: - - "Entity Graph" - summary: "Delete Annotation" + - 'Entity Graph' + summary: 'Delete Annotation' operationId: deleteAnnotation security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/entity-graph/annotations/entity/{entityType}/{entityId}": + '500': + description: 'Internal server error' + '/entity-graph/annotations/entity/{entityType}/{entityId}': delete: tags: - - "Entity Graph" - summary: "Delete Annotation By Entity" + - 'Entity Graph' + summary: 'Delete Annotation By Entity' operationId: deleteAnnotationByEntity security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: entityType + - + name: entityType in: path required: true schema: type: string - - name: entityId + - + name: entityId in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /entity-graph/views: post: tags: - - "Entity Graph" - summary: "Create View" + - 'Entity Graph' + summary: 'Create View' operationId: createView security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' get: tags: - - "Entity Graph" - summary: "Get Views" + - 'Entity Graph' + summary: 'Get Views' operationId: getViews security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/entity-graph/views/{id}": + '500': + description: 'Internal server error' + '/entity-graph/views/{id}': get: tags: - - "Entity Graph" - summary: "Get View By Id" + - 'Entity Graph' + summary: 'Get View By Id' operationId: getViewById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' put: tags: - - "Entity Graph" - summary: "Update View" + - 'Entity Graph' + summary: 'Update View' operationId: updateView security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -16706,369 +17333,400 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - - "Entity Graph" - summary: "Delete View" + - 'Entity Graph' + summary: 'Delete View' operationId: deleteView security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /entity-graph/gap-rules/defaults: get: tags: - - "Entity Graph" - summary: "Get Default Gap Rules" + - 'Entity Graph' + summary: 'Get Default Gap Rules' operationId: getDefaultGapRules responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /entity-graph/gap-rules: post: tags: - - "Entity Graph" - summary: "Save Gap Rules" + - 'Entity Graph' + summary: 'Save Gap Rules' operationId: saveGapRules security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' get: tags: - - "Entity Graph" - summary: "Get Gap Rules" + - 'Entity Graph' + summary: 'Get Gap Rules' operationId: getGapRules security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - - "Entity Graph" - summary: "Reset Gap Rules" + - 'Entity Graph' + summary: 'Reset Gap Rules' operationId: resetGapRules security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/vendor-risk-change-history/{id}": + '500': + description: 'Internal server error' + '/vendor-risk-change-history/{id}': get: tags: - - "Change History" - summary: "Get Vendor Risk Change History By Id" + - 'Change History' + summary: 'Get Vendor Risk Change History By Id' operationId: getVendorRiskChangeHistoryById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/policy-change-history/{id}": + '500': + description: 'Internal server error' + '/policy-change-history/{id}': get: tags: - - "Change History" - summary: "Get Policy Change History By Id" + - 'Change History' + summary: 'Get Policy Change History By Id' operationId: getPolicyChangeHistoryById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/incident-change-history/{incidentId}": + '500': + description: 'Internal server error' + '/incident-change-history/{incidentId}': get: tags: - - "Change History" - summary: "Get Incident History" + - 'Change History' + summary: 'Get Incident History' operationId: getIncidentHistory security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: incidentId + - + name: incidentId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/use-case-change-history/{useCaseId}": + '500': + description: 'Internal server error' + '/use-case-change-history/{useCaseId}': get: tags: - - "Change History" - summary: "Get Use Case History" + - 'Change History' + summary: 'Get Use Case History' operationId: getUseCaseHistory security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: useCaseId + - + name: useCaseId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/risk-change-history/{projectRiskId}": + '500': + description: 'Internal server error' + '/risk-change-history/{projectRiskId}': get: tags: - - "Change History" - summary: "Get Project Risk Change History By Risk Id" + - 'Change History' + summary: 'Get Project Risk Change History By Risk Id' operationId: getProjectRiskChangeHistoryByRiskId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: projectRiskId + - + name: projectRiskId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/file-change-history/{id}": + '500': + description: 'Internal server error' + '/file-change-history/{id}': get: tags: - - "Change History" - summary: "Get File Change History By Id" + - 'Change History' + summary: 'Get File Change History By Id' operationId: getFileChangeHistoryById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/task-change-history/{id}": + '500': + description: 'Internal server error' + '/task-change-history/{id}': get: tags: - - "Change History" - summary: "Get Task Change History By Id" + - 'Change History' + summary: 'Get Task Change History By Id' operationId: getTaskChangeHistoryById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/training-change-history/{id}": + '500': + description: 'Internal server error' + '/training-change-history/{id}': get: tags: - - "Change History" - summary: "Get Training Change History By Id" + - 'Change History' + summary: 'Get Training Change History By Id' operationId: getTrainingChangeHistoryById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/model-risk-change-history/{id}": + '500': + description: 'Internal server error' + '/model-risk-change-history/{id}': get: tags: - - "Change History" - summary: "Get Model Risk Change History By Id" + - 'Change History' + summary: 'Get Model Risk Change History By Id' operationId: getModelRiskChangeHistoryById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/dataset-change-history/{id}": + '500': + description: 'Internal server error' + '/dataset-change-history/{id}': get: tags: - - "Change History" - summary: "Get Dataset Change History By Id" + - 'Change History' + summary: 'Get Dataset Change History By Id' operationId: getDatasetChangeHistoryById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /approval-workflows: get: tags: - - "Approval Workflows" - summary: "Get All Approval Workflows" + - 'Approval Workflows' + summary: 'Get All Approval Workflows' operationId: getAllApprovalWorkflows security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - - "Approval Workflows" - summary: "Create Approval Workflow" + - 'Approval Workflows' + summary: 'Create Approval Workflow' operationId: createApprovalWorkflow security: - - bearerAuth: [] - description: "Requires role: Admin" + - + bearerAuth: [] + description: 'Requires role: Admin' requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" - "/approval-workflows/{id}": + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' + '/approval-workflows/{id}': get: tags: - - "Approval Workflows" - summary: "Get Approval Workflow By Id" + - 'Approval Workflows' + summary: 'Get Approval Workflow By Id' operationId: getApprovalWorkflowById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' put: tags: - - "Approval Workflows" - summary: "Update Approval Workflow" + - 'Approval Workflows' + summary: 'Update Approval Workflow' operationId: updateApprovalWorkflow security: - - bearerAuth: [] - description: "Requires role: Admin" + - + bearerAuth: [] + description: 'Requires role: Admin' parameters: - - name: id + - + name: id in: path required: true schema: @@ -17079,136 +17737,146 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' delete: tags: - - "Approval Workflows" - summary: "Delete Approval Workflow" + - 'Approval Workflows' + summary: 'Delete Approval Workflow' operationId: deleteApprovalWorkflow security: - - bearerAuth: [] - description: "Requires role: Admin" + - + bearerAuth: [] + description: 'Requires role: Admin' parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' /approval-requests: post: tags: - - "Approval Workflows" - summary: "Create Approval Request" + - 'Approval Workflows' + summary: 'Create Approval Request' operationId: createApprovalRequest security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /approval-requests/my-requests: get: tags: - - "Approval Workflows" - summary: "Get My Approval Requests" + - 'Approval Workflows' + summary: 'Get My Approval Requests' operationId: getMyApprovalRequests security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /approval-requests/pending-approvals: get: tags: - - "Approval Workflows" - summary: "Get Pending Approvals" + - 'Approval Workflows' + summary: 'Get Pending Approvals' operationId: getPendingApprovals security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /approval-requests/all: get: tags: - - "Approval Workflows" - summary: "Get All Approval Requests" + - 'Approval Workflows' + summary: 'Get All Approval Requests' operationId: getAllApprovalRequests security: - - bearerAuth: [] - description: "Requires role: Admin" + - + bearerAuth: [] + description: 'Requires role: Admin' responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" - "/approval-requests/{id}": + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' + '/approval-requests/{id}': get: tags: - - "Approval Workflows" - summary: "Get Approval Request By Id" + - 'Approval Workflows' + summary: 'Get Approval Request By Id' operationId: getApprovalRequestById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/approval-requests/{id}/approve": + '500': + description: 'Internal server error' + '/approval-requests/{id}/approve': post: tags: - - "Approval Workflows" - summary: "Approve Request" + - 'Approval Workflows' + summary: 'Approve Request' operationId: approveRequest security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -17219,22 +17887,24 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/approval-requests/{id}/reject": + '500': + description: 'Internal server error' + '/approval-requests/{id}/reject': post: tags: - - "Approval Workflows" - summary: "Reject Request" + - 'Approval Workflows' + summary: 'Reject Request' operationId: rejectRequest security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -17245,22 +17915,24 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/approval-requests/{id}/withdraw": + '500': + description: 'Internal server error' + '/approval-requests/{id}/withdraw': post: tags: - - "Approval Workflows" - summary: "Withdraw approval request" + - 'Approval Workflows' + summary: 'Withdraw approval request' operationId: withdrawRequest security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -17271,17 +17943,17 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /webhooks/github: post: tags: - Webhooks - summary: "Github Webhook Controller" + summary: 'Github Webhook Controller' operationId: githubWebhookController requestBody: content: @@ -17289,194 +17961,211 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "500": - description: "Internal server error" + '201': + description: 'Created successfully' + '500': + description: 'Internal server error' /ai-detection/scans: post: tags: - - "AI Detection" - summary: "Start Scan Controller" + - 'AI Detection' + summary: 'Start Scan Controller' operationId: startScanController security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' get: tags: - - "AI Detection" - summary: "Get Scans Controller" + - 'AI Detection' + summary: 'Get Scans Controller' operationId: getScansController security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /ai-detection/scans/active: get: tags: - - "AI Detection" - summary: "Get Active Scan Controller" + - 'AI Detection' + summary: 'Get Active Scan Controller' operationId: getActiveScanController security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/ai-detection/scans/{scanId}": + '500': + description: 'Internal server error' + '/ai-detection/scans/{scanId}': get: tags: - - "AI Detection" - summary: "Get Scan Controller" + - 'AI Detection' + summary: 'Get Scan Controller' operationId: getScanController security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: scanId + - + name: scanId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - - "AI Detection" - summary: "Delete Scan Controller" + - 'AI Detection' + summary: 'Delete Scan Controller' operationId: deleteScanController security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: scanId + - + name: scanId in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/ai-detection/scans/{scanId}/status": + '500': + description: 'Internal server error' + '/ai-detection/scans/{scanId}/status': get: tags: - - "AI Detection" - summary: "Get Scan Status Controller" + - 'AI Detection' + summary: 'Get Scan Status Controller' operationId: getScanStatusController security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: scanId + - + name: scanId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/ai-detection/scans/{scanId}/findings": + '500': + description: 'Internal server error' + '/ai-detection/scans/{scanId}/findings': get: tags: - - "AI Detection" - summary: "Get Scan Findings Controller" + - 'AI Detection' + summary: 'Get Scan Findings Controller' operationId: getScanFindingsController security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: scanId + - + name: scanId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/ai-detection/scans/{scanId}/security-findings": + '500': + description: 'Internal server error' + '/ai-detection/scans/{scanId}/security-findings': get: tags: - - "AI Detection" - summary: "Get Security Findings Controller" + - 'AI Detection' + summary: 'Get Security Findings Controller' operationId: getSecurityFindingsController security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: scanId + - + name: scanId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/ai-detection/scans/{scanId}/security-summary": + '500': + description: 'Internal server error' + '/ai-detection/scans/{scanId}/security-summary': get: tags: - - "AI Detection" - summary: "Get Security Summary Controller" + - 'AI Detection' + summary: 'Get Security Summary Controller' operationId: getSecuritySummaryController security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: scanId + - + name: scanId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/ai-detection/scans/{scanId}/cancel": + '500': + description: 'Internal server error' + '/ai-detection/scans/{scanId}/cancel': post: tags: - - "AI Detection" - summary: "Cancel Scan Controller" + - 'AI Detection' + summary: 'Cancel Scan Controller' operationId: cancelScanController security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: scanId + - + name: scanId in: path required: true schema: @@ -17487,27 +18176,30 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/ai-detection/scans/{scanId}/findings/{findingId}/governance": + '500': + description: 'Internal server error' + '/ai-detection/scans/{scanId}/findings/{findingId}/governance': patch: tags: - - "AI Detection" - summary: "Update Governance Status Controller" + - 'AI Detection' + summary: 'Update Governance Status Controller' operationId: updateGovernanceStatusController security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: scanId + - + name: scanId in: path required: true schema: type: integer - - name: findingId + - + name: findingId in: path required: true schema: @@ -17518,142 +18210,155 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/ai-detection/scans/{scanId}/governance-summary": + '500': + description: 'Internal server error' + '/ai-detection/scans/{scanId}/governance-summary': get: tags: - - "AI Detection" - summary: "Get Governance Summary Controller" + - 'AI Detection' + summary: 'Get Governance Summary Controller' operationId: getGovernanceSummaryController security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: scanId + - + name: scanId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /ai-detection/stats: get: tags: - - "AI Detection" - summary: "Get A I Detection Stats Controller" + - 'AI Detection' + summary: 'Get A I Detection Stats Controller' operationId: getAIDetectionStatsController security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/ai-detection/scans/{scanId}/export/ai-bom": + '500': + description: 'Internal server error' + '/ai-detection/scans/{scanId}/export/ai-bom': get: tags: - - "AI Detection" - summary: "Export A I B O M Controller" + - 'AI Detection' + summary: 'Export A I B O M Controller' operationId: exportAIBOMController security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: scanId + - + name: scanId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/ai-detection/scans/{scanId}/dependency-graph": + '500': + description: 'Internal server error' + '/ai-detection/scans/{scanId}/dependency-graph': get: tags: - - "AI Detection" - summary: "Get Dependency Graph Controller" + - 'AI Detection' + summary: 'Get Dependency Graph Controller' operationId: getDependencyGraphController security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: scanId + - + name: scanId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/ai-detection/scans/{scanId}/compliance": + '500': + description: 'Internal server error' + '/ai-detection/scans/{scanId}/compliance': get: tags: - - "AI Detection" - summary: "Get Compliance Mapping Controller" + - 'AI Detection' + summary: 'Get Compliance Mapping Controller' operationId: getComplianceMappingController security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: scanId + - + name: scanId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/ai-detection/scans/{scanId}/risk-score": + '500': + description: 'Internal server error' + '/ai-detection/scans/{scanId}/risk-score': get: tags: - - "AI Detection" - summary: "Get Risk Score Controller" + - 'AI Detection' + summary: 'Get Risk Score Controller' operationId: getRiskScoreController security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: scanId + - + name: scanId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/ai-detection/scans/{scanId}/risk-score/recalculate": + '500': + description: 'Internal server error' + '/ai-detection/scans/{scanId}/risk-score/recalculate': post: tags: - - "AI Detection" - summary: "Recalculate Risk Score Controller" + - 'AI Detection' + summary: 'Recalculate Risk Score Controller' operationId: recalculateRiskScoreController security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: scanId + - + name: scanId in: path required: true schema: @@ -17664,148 +18369,159 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /ai-detection/risk-scoring/config: get: tags: - - "AI Detection" - summary: "Get Risk Scoring Config Controller" + - 'AI Detection' + summary: 'Get Risk Scoring Config Controller' operationId: getRiskScoringConfigController security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' patch: tags: - - "AI Detection" - summary: "Update Risk Scoring Config Controller" + - 'AI Detection' + summary: 'Update Risk Scoring Config Controller' operationId: updateRiskScoringConfigController security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /ai-detection/suppressions: post: - summary: "Create Suppression Controller" + summary: 'Create Suppression Controller' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Detection" + - 'AI Detection' operationId: createSuppressionController security: - - bearerAuth: [] + - + bearerAuth: [] get: - summary: "List Suppressions Controller" + summary: 'List Suppressions Controller' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Detection" + - 'AI Detection' operationId: listSuppressionsController security: - - bearerAuth: [] - "/ai-detection/suppressions/{id}": + - + bearerAuth: [] + '/ai-detection/suppressions/{id}': delete: - summary: "Delete Suppression Controller" + summary: 'Delete Suppression Controller' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "AI Detection" + - 'AI Detection' operationId: deleteSuppressionController security: - - bearerAuth: [] + - + bearerAuth: [] /ai-detection/repositories: get: tags: - - "AI Detection" - summary: "List Repositories" + - 'AI Detection' + summary: 'List Repositories' operationId: listRepositories security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - - "AI Detection" - summary: "Create Repository" + - 'AI Detection' + summary: 'Create Repository' operationId: createRepository security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/ai-detection/repositories/{id}": + '500': + description: 'Internal server error' + '/ai-detection/repositories/{id}': get: tags: - - "AI Detection" - summary: "Get Repository" + - 'AI Detection' + summary: 'Get Repository' operationId: getRepository security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' patch: tags: - - "AI Detection" - summary: "Update Repository" + - 'AI Detection' + summary: 'Update Repository' operationId: updateRepository security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -17816,42 +18532,46 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - - "AI Detection" - summary: "Delete Repository" + - 'AI Detection' + summary: 'Delete Repository' operationId: deleteRepository security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/ai-detection/repositories/{id}/scan": + '500': + description: 'Internal server error' + '/ai-detection/repositories/{id}/scan': post: tags: - - "AI Detection" - summary: "Trigger Repository Scan" + - 'AI Detection' + summary: 'Trigger Repository Scan' operationId: triggerRepositoryScan security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -17862,22 +18582,24 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/ai-detection/repositories/{id}/webhook-secret": + '500': + description: 'Internal server error' + '/ai-detection/repositories/{id}/webhook-secret': post: tags: - - "AI Detection" - summary: "Generate Webhook Secret Controller" + - 'AI Detection' + summary: 'Generate Webhook Secret Controller' operationId: generateWebhookSecretController security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -17888,191 +18610,204 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/ai-detection/repositories/{id}/scans": + '500': + description: 'Internal server error' + '/ai-detection/repositories/{id}/scans': get: tags: - - "AI Detection" - summary: "Get Repository Scans" + - 'AI Detection' + summary: 'Get Repository Scans' operationId: getRepositoryScans security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /integrations/github/token: get: tags: - Integrations - summary: "Get Git Hub Token Status Controller" + summary: 'Get Git Hub Token Status Controller' operationId: getGitHubTokenStatusController security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - Integrations - summary: "Save Git Hub Token Controller" + summary: 'Save Git Hub Token Controller' operationId: saveGitHubTokenController security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - Integrations - summary: "Delete Git Hub Token Controller" + summary: 'Delete Git Hub Token Controller' operationId: deleteGitHubTokenController security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /integrations/github/token/test: post: tags: - Integrations - summary: "Test Git Hub Token Controller" + summary: 'Test Git Hub Token Controller' operationId: testGitHubTokenController security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /notifications/stream: get: tags: - Notifications - summary: "Stream Notifications" + summary: 'Stream Notifications' operationId: streamNotifications security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /notifications: get: tags: - Notifications - summary: "Get Notifications" + summary: 'Get Notifications' operationId: getNotifications security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /notifications/summary: get: tags: - Notifications - summary: "Get Notification Summary" + summary: 'Get Notification Summary' operationId: getNotificationSummary security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /notifications/unread-count: get: tags: - Notifications - summary: "Get Unread Count" + summary: 'Get Unread Count' operationId: getUnreadCount security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /notifications/read-all: patch: tags: - Notifications - summary: "Mark All As Read" + summary: 'Mark All As Read' operationId: markAllAsRead security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/notifications/{id}/read": + '500': + description: 'Internal server error' + '/notifications/{id}/read': patch: tags: - Notifications - summary: "Mark As Read" + summary: 'Mark As Read' operationId: markAsRead security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -18083,84 +18818,91 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/notifications/{id}": + '500': + description: 'Internal server error' + '/notifications/{id}': delete: tags: - Notifications - summary: "Delete Notification" + summary: 'Delete Notification' operationId: deleteNotification security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/pmm/config/{projectId}": + '500': + description: 'Internal server error' + '/pmm/config/{projectId}': get: tags: - - "Post-Market Monitoring" - summary: "Get Config By Project Id" + - 'Post-Market Monitoring' + summary: 'Get Config By Project Id' operationId: getConfigByProjectId security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: projectId + - + name: projectId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /pmm/config: post: tags: - - "Post-Market Monitoring" - summary: "Create Config" + - 'Post-Market Monitoring' + summary: 'Create Config' operationId: createConfig security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/pmm/config/{configId}": + '500': + description: 'Internal server error' + '/pmm/config/{configId}': put: tags: - - "Post-Market Monitoring" - summary: "Update Config" + - 'Post-Market Monitoring' + summary: 'Update Config' operationId: updateConfig security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: configId + - + name: configId in: path required: true schema: @@ -18171,62 +18913,68 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - - "Post-Market Monitoring" - summary: "Delete Config" + - 'Post-Market Monitoring' + summary: 'Delete Config' operationId: deleteConfig security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: configId + - + name: configId in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/pmm/config/{configId}/questions": + '500': + description: 'Internal server error' + '/pmm/config/{configId}/questions': get: tags: - - "Post-Market Monitoring" - summary: "Get Questions" + - 'Post-Market Monitoring' + summary: 'Get Questions' operationId: getQuestions security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: configId + - + name: configId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - - "Post-Market Monitoring" - summary: "Add Question" + - 'Post-Market Monitoring' + summary: 'Add Question' operationId: addQuestion security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: configId + - + name: configId in: path required: true schema: @@ -18237,37 +18985,40 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /pmm/org/questions: get: tags: - - "Post-Market Monitoring" - summary: "Get Questions" + - 'Post-Market Monitoring' + summary: 'Get Questions' operationId: pmm_getQuestions security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/pmm/questions/{questionId}": + '500': + description: 'Internal server error' + '/pmm/questions/{questionId}': put: tags: - - "Post-Market Monitoring" - summary: "Update Question" + - 'Post-Market Monitoring' + summary: 'Update Question' operationId: updateQuestion security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: questionId + - + name: questionId in: path required: true schema: @@ -18278,124 +19029,135 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - - "Post-Market Monitoring" - summary: "Delete Question" + - 'Post-Market Monitoring' + summary: 'Delete Question' operationId: deleteQuestion security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: questionId + - + name: questionId in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /pmm/questions/reorder: post: tags: - - "Post-Market Monitoring" - summary: "Reorder Questions" + - 'Post-Market Monitoring' + summary: 'Reorder Questions' operationId: reorderQuestions security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/pmm/active-cycle/{projectId}": + '500': + description: 'Internal server error' + '/pmm/active-cycle/{projectId}': get: tags: - - "Post-Market Monitoring" - summary: "Get Active Cycle" + - 'Post-Market Monitoring' + summary: 'Get Active Cycle' operationId: getActiveCycle security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: projectId + - + name: projectId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/pmm/cycles/{cycleId}": + '500': + description: 'Internal server error' + '/pmm/cycles/{cycleId}': get: tags: - - "Post-Market Monitoring" - summary: "Get Cycle By Id" + - 'Post-Market Monitoring' + summary: 'Get Cycle By Id' operationId: getCycleById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: cycleId + - + name: cycleId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/pmm/cycles/{cycleId}/responses": + '500': + description: 'Internal server error' + '/pmm/cycles/{cycleId}/responses': get: tags: - - "Post-Market Monitoring" - summary: "Get Responses" + - 'Post-Market Monitoring' + summary: 'Get Responses' operationId: getResponses security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: cycleId + - + name: cycleId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - - "Post-Market Monitoring" - summary: "Save Responses" + - 'Post-Market Monitoring' + summary: 'Save Responses' operationId: saveResponses security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: cycleId + - + name: cycleId in: path required: true schema: @@ -18406,22 +19168,24 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/pmm/cycles/{cycleId}/submit": + '500': + description: 'Internal server error' + '/pmm/cycles/{cycleId}/submit': post: tags: - - "Post-Market Monitoring" - summary: "Submit Cycle" + - 'Post-Market Monitoring' + summary: 'Submit Cycle' operationId: submitCycle security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: cycleId + - + name: cycleId in: path required: true schema: @@ -18432,22 +19196,24 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/pmm/cycles/{cycleId}/flag": + '500': + description: 'Internal server error' + '/pmm/cycles/{cycleId}/flag': post: tags: - - "Post-Market Monitoring" - summary: "Flag Concern" + - 'Post-Market Monitoring' + summary: 'Flag Concern' operationId: flagConcern security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: cycleId + - + name: cycleId in: path required: true schema: @@ -18458,58 +19224,63 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /pmm/reports: get: tags: - - "Post-Market Monitoring" - summary: "Get Reports" + - 'Post-Market Monitoring' + summary: 'Get Reports' operationId: getReports security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/pmm/reports/{reportId}/download": + '500': + description: 'Internal server error' + '/pmm/reports/{reportId}/download': get: tags: - - "Post-Market Monitoring" - summary: "Download Report" + - 'Post-Market Monitoring' + summary: 'Download Report' operationId: downloadReport security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: reportId + - + name: reportId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/pmm/cycles/{cycleId}/reassign": + '500': + description: 'Internal server error' + '/pmm/cycles/{cycleId}/reassign': post: tags: - - "Post-Market Monitoring" - summary: "Reassign Stakeholder" + - 'Post-Market Monitoring' + summary: 'Reassign Stakeholder' operationId: reassignStakeholder security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: cycleId + - + name: cycleId in: path required: true schema: @@ -18520,22 +19291,24 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/pmm/projects/{projectId}/start-cycle": + '500': + description: 'Internal server error' + '/pmm/projects/{projectId}/start-cycle': post: tags: - - "Post-Market Monitoring" - summary: "Start New Cycle" + - 'Post-Market Monitoring' + summary: 'Start New Cycle' operationId: startNewCycle security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: projectId + - + name: projectId in: path required: true schema: @@ -18546,163 +19319,176 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /compliance/score: get: tags: - Compliance - summary: "Get Compliance Score" + summary: 'Get Compliance Score' operationId: getComplianceScore security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/compliance/score/{organizationId}": + '500': + description: 'Internal server error' + '/compliance/score/{organizationId}': get: tags: - Compliance - summary: "Get Compliance Score By Organization" + summary: 'Get Compliance Score By Organization' operationId: getComplianceScoreByOrganization security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: organizationId + - + name: organizationId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/compliance/details/{organizationId}": + '500': + description: 'Internal server error' + '/compliance/details/{organizationId}': get: tags: - Compliance - summary: "Get Compliance Details" + summary: 'Get Compliance Details' operationId: getComplianceDetails security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: organizationId + - + name: organizationId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /virtual-folders: get: tags: - Files - summary: "Get All Folders" + summary: 'Get All Folders' operationId: getAllFolders security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - Files - summary: "Create Folder" + summary: 'Create Folder' operationId: createFolder security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /virtual-folders/tree: get: tags: - Files - summary: "Get Folder Tree" + summary: 'Get Folder Tree' operationId: getFolderTree security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /virtual-folders/uncategorized: get: tags: - Files - summary: "Get Uncategorized Files" + summary: 'Get Uncategorized Files' operationId: getUncategorizedFiles security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/virtual-folders/{id}": + '500': + description: 'Internal server error' + '/virtual-folders/{id}': get: tags: - Files - summary: "Get Folder By Id" + summary: 'Get Folder By Id' operationId: getFolderById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' patch: tags: - Files - summary: "Update Folder" + summary: 'Update Folder' operationId: updateFolder security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -18713,83 +19499,91 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - Files - summary: "Delete Folder" + summary: 'Delete Folder' operationId: deleteFolder security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/virtual-folders/{id}/path": + '500': + description: 'Internal server error' + '/virtual-folders/{id}/path': get: tags: - Files - summary: "Get Folder Path" + summary: 'Get Folder Path' operationId: getFolderPath security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/virtual-folders/{id}/files": + '500': + description: 'Internal server error' + '/virtual-folders/{id}/files': get: tags: - Files - summary: "Get Files In Folder" + summary: 'Get Files In Folder' operationId: getFilesInFolder security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - Files - summary: "Assign Files To Folder" + summary: 'Assign Files To Folder' operationId: assignFilesToFolder security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -18800,119 +19594,130 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/virtual-folders/{id}/files/{fileId}": + '500': + description: 'Internal server error' + '/virtual-folders/{id}/files/{fileId}': delete: tags: - Files - summary: "Remove File From Folder" + summary: 'Remove File From Folder' operationId: removeFileFromFolder security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer - - name: fileId + - + name: fileId in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /files/tree: get: tags: - Files - summary: "Get Folder Tree" + summary: 'Get Folder Tree' operationId: files_getFolderTree security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /files/uncategorized: get: tags: - Files - summary: "Get Uncategorized Files" + summary: 'Get Uncategorized Files' operationId: files_getUncategorizedFiles security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/files/{id}/path": + '500': + description: 'Internal server error' + '/files/{id}/path': get: tags: - Files - summary: "Get Folder Path" + summary: 'Get Folder Path' operationId: files_getFolderPath security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/files/{id}/files": + '500': + description: 'Internal server error' + '/files/{id}/files': get: tags: - Files - summary: "Get Files In Folder" + summary: 'Get Files In Folder' operationId: files_getFilesInFolder security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - Files - summary: "Assign Files To Folder" + summary: 'Assign Files To Folder' operationId: files_assignFilesToFolder security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -18923,286 +19728,309 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/files/{id}/files/{fileId}": + '500': + description: 'Internal server error' + '/files/{id}/files/{fileId}': delete: tags: - Files - summary: "Remove File From Folder" + summary: 'Remove File From Folder' operationId: files_removeFileFromFolder security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer - - name: fileId + - + name: fileId in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /shadow-ai/api-keys: post: tags: - - "Shadow AI" - summary: "Create Api Key" + - 'Shadow AI' + summary: 'Create Api Key' operationId: createApiKey security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' get: tags: - - "Shadow AI" - summary: "List Api Keys" + - 'Shadow AI' + summary: 'List Api Keys' operationId: listApiKeys security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/shadow-ai/api-keys/{id}": + '500': + description: 'Internal server error' + '/shadow-ai/api-keys/{id}': delete: tags: - - "Shadow AI" - summary: "Revoke Api Key" + - 'Shadow AI' + summary: 'Revoke Api Key' operationId: revokeApiKey security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/shadow-ai/api-keys/{id}/permanent": + '500': + description: 'Internal server error' + '/shadow-ai/api-keys/{id}/permanent': delete: tags: - - "Shadow AI" - summary: "Delete Api Key" + - 'Shadow AI' + summary: 'Delete Api Key' operationId: deleteApiKey security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /shadow-ai/insights/summary: get: tags: - - "Shadow AI" - summary: "Get Insights Summary" + - 'Shadow AI' + summary: 'Get Insights Summary' operationId: getInsightsSummary security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /shadow-ai/insights/tools-by-events: get: tags: - - "Shadow AI" - summary: "Get Tools By Events" + - 'Shadow AI' + summary: 'Get Tools By Events' operationId: getToolsByEvents security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /shadow-ai/insights/tools-by-users: get: tags: - - "Shadow AI" - summary: "Get Tools By Users" + - 'Shadow AI' + summary: 'Get Tools By Users' operationId: getToolsByUsers security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /shadow-ai/insights/users-by-department: get: tags: - - "Shadow AI" - summary: "Get Users By Department" + - 'Shadow AI' + summary: 'Get Users By Department' operationId: getUsersByDepartment security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /shadow-ai/insights/trend: get: tags: - - "Shadow AI" - summary: "Get Trend" + - 'Shadow AI' + summary: 'Get Trend' operationId: getTrend security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /shadow-ai/users: get: tags: - - "Shadow AI" - summary: "Get Users" + - 'Shadow AI' + summary: 'Get Users' operationId: getUsers security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/shadow-ai/users/{email}/activity": + '500': + description: 'Internal server error' + '/shadow-ai/users/{email}/activity': get: tags: - - "Shadow AI" - summary: "Get User Detail" + - 'Shadow AI' + summary: 'Get User Detail' operationId: getUserDetail security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: email + - + name: email in: path required: true schema: type: string responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /shadow-ai/departments: get: tags: - - "Shadow AI" - summary: "Get Department Activity" + - 'Shadow AI' + summary: 'Get Department Activity' operationId: getDepartmentActivity security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /shadow-ai/tools: get: tags: - - "Shadow AI" - summary: "Get Tools" + - 'Shadow AI' + summary: 'Get Tools' operationId: getTools security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/shadow-ai/tools/{id}": + '500': + description: 'Internal server error' + '/shadow-ai/tools/{id}': get: tags: - - "Shadow AI" - summary: "Get Tool By Id" + - 'Shadow AI' + summary: 'Get Tool By Id' operationId: getToolById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/shadow-ai/tools/{id}/status": + '500': + description: 'Internal server error' + '/shadow-ai/tools/{id}/status': patch: tags: - - "Shadow AI" - summary: "Update Tool Status" + - 'Shadow AI' + summary: 'Update Tool Status' operationId: updateToolStatus security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -19213,22 +20041,24 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/shadow-ai/tools/{id}/start-governance": + '500': + description: 'Internal server error' + '/shadow-ai/tools/{id}/start-governance': post: tags: - - "Shadow AI" - summary: "Start Governance" + - 'Shadow AI' + summary: 'Start Governance' operationId: startGovernance security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -19239,56 +20069,60 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /shadow-ai/rules: get: tags: - - "Shadow AI" - summary: "Get Rules" + - 'Shadow AI' + summary: 'Get Rules' operationId: getRules security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - - "Shadow AI" - summary: "Create Rule" + - 'Shadow AI' + summary: 'Create Rule' operationId: createRule security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/shadow-ai/rules/{id}": + '500': + description: 'Internal server error' + '/shadow-ai/rules/{id}': patch: tags: - - "Shadow AI" - summary: "Update Rule" + - 'Shadow AI' + summary: 'Update Rule' operationId: updateRule security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -19299,91 +20133,98 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - - "Shadow AI" - summary: "Delete Rule" + - 'Shadow AI' + summary: 'Delete Rule' operationId: deleteRule security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /shadow-ai/rules/alert-history: get: tags: - - "Shadow AI" - summary: "Get Alert History" + - 'Shadow AI' + summary: 'Get Alert History' operationId: getAlertHistory security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /shadow-ai/config/syslog: get: tags: - - "Shadow AI" - summary: "Get Syslog Configs" + - 'Shadow AI' + summary: 'Get Syslog Configs' operationId: getSyslogConfigs security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - - "Shadow AI" - summary: "Create Syslog Config" + - 'Shadow AI' + summary: 'Create Syslog Config' operationId: createSyslogConfig security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/shadow-ai/config/syslog/{id}": + '500': + description: 'Internal server error' + '/shadow-ai/config/syslog/{id}': patch: tags: - - "Shadow AI" - summary: "Update Syslog Config" + - 'Shadow AI' + summary: 'Update Syslog Config' operationId: updateSyslogConfig security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -19394,71 +20235,75 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - - "Shadow AI" - summary: "Delete Syslog Config" + - 'Shadow AI' + summary: 'Delete Syslog Config' operationId: deleteSyslogConfig security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /shadow-ai/settings: get: tags: - - "Shadow AI" - summary: "Get Settings" + - 'Shadow AI' + summary: 'Get Settings' operationId: getSettings security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' patch: tags: - - "Shadow AI" - summary: "Update Settings" + - 'Shadow AI' + summary: 'Update Settings' operationId: updateSettings security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /v1/shadow-ai/events: post: tags: - - "Shadow AI" - summary: "Ingest Events" + - 'Shadow AI' + summary: 'Ingest Events' operationId: ingestEvents requestBody: content: @@ -19466,119 +20311,128 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "500": - description: "Internal server error" + '201': + description: 'Created successfully' + '500': + description: 'Internal server error' /agent-primitives: get: tags: - - "Agent Discovery" - summary: "Get All Agent Primitives" + - 'Agent Discovery' + summary: 'Get All Agent Primitives' operationId: getAllAgentPrimitives security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - - "Agent Discovery" - summary: "Create Agent Primitive" + - 'Agent Discovery' + summary: 'Create Agent Primitive' operationId: createAgentPrimitive security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /agent-primitives/stats: get: tags: - - "Agent Discovery" - summary: "Get Agent Stats" + - 'Agent Discovery' + summary: 'Get Agent Stats' operationId: getAgentStats security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /agent-primitives/sync/logs: get: tags: - - "Agent Discovery" - summary: "Get Sync Logs" + - 'Agent Discovery' + summary: 'Get Sync Logs' operationId: getSyncLogs security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /agent-primitives/sync/status: get: tags: - - "Agent Discovery" - summary: "Get Sync Status" + - 'Agent Discovery' + summary: 'Get Sync Status' operationId: getSyncStatus security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/agent-primitives/{id}": + '500': + description: 'Internal server error' + '/agent-primitives/{id}': get: tags: - - "Agent Discovery" - summary: "Get Agent Primitive By Id" + - 'Agent Discovery' + summary: 'Get Agent Primitive By Id' operationId: getAgentPrimitiveById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' patch: tags: - - "Agent Discovery" - summary: "Update Agent Primitive" + - 'Agent Discovery' + summary: 'Update Agent Primitive' operationId: updateAgentPrimitive security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -19589,62 +20443,67 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - - "Agent Discovery" - summary: "Delete Agent Primitive By Id" + - 'Agent Discovery' + summary: 'Delete Agent Primitive By Id' operationId: deleteAgentPrimitiveById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /agent-primitives/sync: post: tags: - - "Agent Discovery" - summary: "Trigger Sync" + - 'Agent Discovery' + summary: 'Trigger Sync' operationId: triggerSync security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/agent-primitives/{id}/review": + '500': + description: 'Internal server error' + '/agent-primitives/{id}/review': patch: tags: - - "Agent Discovery" - summary: "Review Agent Primitive" + - 'Agent Discovery' + summary: 'Review Agent Primitive' operationId: reviewAgentPrimitive security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -19655,22 +20514,24 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/agent-primitives/{id}/link-model": + '500': + description: 'Internal server error' + '/agent-primitives/{id}/link-model': patch: tags: - - "Agent Discovery" - summary: "Link Model To Agent" + - 'Agent Discovery' + summary: 'Link Model To Agent' operationId: linkModelToAgent security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -19681,22 +20542,24 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/agent-primitives/{id}/unlink-model": + '500': + description: 'Internal server error' + '/agent-primitives/{id}/unlink-model': patch: tags: - - "Agent Discovery" - summary: "Unlink Model From Agent" + - 'Agent Discovery' + summary: 'Unlink Model From Agent' operationId: unlinkModelFromAgent security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -19707,97 +20570,105 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/agent-primitives/{id}/audit-logs": + '500': + description: 'Internal server error' + '/agent-primitives/{id}/audit-logs': get: tags: - - "Agent Discovery" - summary: "Get Agent Audit Logs" + - 'Agent Discovery' + summary: 'Get Agent Audit Logs' operationId: getAgentAuditLogs security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /intake/forms: get: tags: - - "Intake Forms" - summary: "Get All Intake Forms" + - 'Intake Forms' + summary: 'Get All Intake Forms' operationId: getAllIntakeForms security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - - "Intake Forms" - summary: "Create Intake Form" + - 'Intake Forms' + summary: 'Create Intake Form' operationId: createIntakeForm security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/intake/forms/{id}": + '500': + description: 'Internal server error' + '/intake/forms/{id}': get: tags: - - "Intake Forms" - summary: "Get Intake Form By Id" + - 'Intake Forms' + summary: 'Get Intake Form By Id' operationId: getIntakeFormById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' patch: tags: - - "Intake Forms" - summary: "Update Intake Form" + - 'Intake Forms' + summary: 'Update Intake Form' operationId: updateIntakeForm security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -19808,42 +20679,46 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' delete: tags: - - "Intake Forms" - summary: "Delete Intake Form" + - 'Intake Forms' + summary: 'Delete Intake Form' operationId: deleteIntakeForm security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/intake/forms/{id}/archive": + '500': + description: 'Internal server error' + '/intake/forms/{id}/archive': post: tags: - - "Intake Forms" - summary: "Archive Intake Form" + - 'Intake Forms' + summary: 'Archive Intake Form' operationId: archiveIntakeForm security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -19854,181 +20729,196 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/intake/forms/{id}/preview": + '500': + description: 'Internal server error' + '/intake/forms/{id}/preview': get: tags: - - "Intake Forms" - summary: "Preview Form" + - 'Intake Forms' + summary: 'Preview Form' operationId: previewForm security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /intake/forms/suggested-questions: post: tags: - - "Intake Forms" - summary: "Get L L M Suggested Questions" + - 'Intake Forms' + summary: 'Get L L M Suggested Questions' operationId: getLLMSuggestedQuestions security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /intake/forms/field-guidance: post: tags: - - "Intake Forms" - summary: "Get Field Guidance" + - 'Intake Forms' + summary: 'Get Field Guidance' operationId: getFieldGuidance security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /intake/submissions: get: tags: - - "Intake Forms" - summary: "Get Pending Submissions" + - 'Intake Forms' + summary: 'Get Pending Submissions' operationId: getPendingSubmissions security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /intake/submissions/stats: get: tags: - - "Intake Forms" - summary: "Get Submission Stats" + - 'Intake Forms' + summary: 'Get Submission Stats' operationId: getSubmissionStats security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/intake/submissions/by-entity/{entityType}/{entityId}": + '500': + description: 'Internal server error' + '/intake/submissions/by-entity/{entityType}/{entityId}': get: tags: - - "Intake Forms" - summary: "Get Submission By Entity" + - 'Intake Forms' + summary: 'Get Submission By Entity' operationId: getSubmissionByEntity security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: entityType + - + name: entityType in: path required: true schema: type: string - - name: entityId + - + name: entityId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/intake/submissions/{id}": + '500': + description: 'Internal server error' + '/intake/submissions/{id}': get: tags: - - "Intake Forms" - summary: "Get Submission By Id" + - 'Intake Forms' + summary: 'Get Submission By Id' operationId: getSubmissionById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/intake/submissions/{id}/preview": + '500': + description: 'Internal server error' + '/intake/submissions/{id}/preview': get: tags: - - "Intake Forms" - summary: "Get Submission Preview" + - 'Intake Forms' + summary: 'Get Submission Preview' operationId: getSubmissionPreview security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/intake/submissions/{id}/risk-override": + '500': + description: 'Internal server error' + '/intake/submissions/{id}/risk-override': patch: tags: - - "Intake Forms" - summary: "Override Submission Risk" + - 'Intake Forms' + summary: 'Override Submission Risk' operationId: overrideSubmissionRisk security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -20039,43 +20929,47 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/intake/forms/{id}/submissions": + '500': + description: 'Internal server error' + '/intake/forms/{id}/submissions': get: tags: - - "Intake Forms" - summary: "Get Form Submissions" + - 'Intake Forms' + summary: 'Get Form Submissions' operationId: getFormSubmissions security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/intake/submissions/{id}/approve": + '500': + description: 'Internal server error' + '/intake/submissions/{id}/approve': post: tags: - - "Intake Forms" - summary: "Approve Submission" + - 'Intake Forms' + summary: 'Approve Submission' operationId: approveSubmission security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -20086,22 +20980,24 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" - "/intake/submissions/{id}/reject": + '500': + description: 'Internal server error' + '/intake/submissions/{id}/reject': post: tags: - - "Intake Forms" - summary: "Reject Submission" + - 'Intake Forms' + summary: 'Reject Submission' operationId: rejectSubmission security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: @@ -20112,47 +21008,49 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /intake/public/captcha: get: tags: - - "Intake Forms" - summary: "Get Captcha" + - 'Intake Forms' + summary: 'Get Captcha' operationId: getCaptcha responses: - "200": + '200': description: Success - "500": - description: "Internal server error" - "/intake/public/by-id/{publicId}": + '500': + description: 'Internal server error' + '/intake/public/by-id/{publicId}': get: tags: - - "Intake Forms" - summary: "Get Public Form By Public Id" + - 'Intake Forms' + summary: 'Get Public Form By Public Id' operationId: getPublicFormByPublicId parameters: - - name: publicId + - + name: publicId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - - "Intake Forms" - summary: "Submit Public Form By Public Id" + - 'Intake Forms' + summary: 'Submit Public Form By Public Id' operationId: submitPublicFormByPublicId parameters: - - name: publicId + - + name: publicId in: path required: true schema: @@ -20163,44 +21061,48 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "500": - description: "Internal server error" - "/intake/public/{tenantSlug}/{formSlug}": + '201': + description: 'Created successfully' + '500': + description: 'Internal server error' + '/intake/public/{tenantSlug}/{formSlug}': get: tags: - - "Intake Forms" - summary: "Get Public Form" + - 'Intake Forms' + summary: 'Get Public Form' operationId: getPublicForm parameters: - - name: tenantSlug + - + name: tenantSlug in: path required: true schema: type: string - - name: formSlug + - + name: formSlug in: path required: true schema: type: string responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - - "Intake Forms" - summary: "Submit Public Form" + - 'Intake Forms' + summary: 'Submit Public Form' operationId: submitPublicForm parameters: - - name: tenantSlug + - + name: tenantSlug in: path required: true schema: type: string - - name: formSlug + - + name: formSlug in: path required: true schema: @@ -20211,102 +21113,108 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "500": - description: "Internal server error" + '201': + description: 'Created successfully' + '500': + description: 'Internal server error' /version: get: tags: - System - summary: "Get application version" + summary: 'Get application version' operationId: version_anonymous responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /audit-ledger: get: tags: - Audit - summary: "Get Audit Ledger" + summary: 'Get Audit Ledger' operationId: getAuditLedger security: - - bearerAuth: [] - description: "Requires role: Admin or SuperAdmin" + - + bearerAuth: [] + description: 'Requires role: Admin or SuperAdmin' responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' /audit-ledger/verify: get: tags: - Audit - summary: "Verify Audit Ledger" + summary: 'Verify Audit Ledger' operationId: verifyAuditLedger security: - - bearerAuth: [] - description: "Requires role: Admin" + - + bearerAuth: [] + description: 'Requires role: Admin' responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' /feature-settings: get: tags: - Settings - summary: "Get Feature Settings" + summary: 'Get Feature Settings' operationId: getFeatureSettings security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' patch: tags: - Settings - summary: "Update Feature Settings" + summary: 'Update Feature Settings' operationId: updateFeatureSettings security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/fria/{friaId}/rights": + '500': + description: 'Internal server error' + '/fria/{friaId}/rights': put: tags: - FRIA - summary: "Update Fria Rights" + summary: 'Update Fria Rights' operationId: updateFriaRights security: - - bearerAuth: [] - description: "Requires role: Admin or Editor" + - + bearerAuth: [] + description: 'Requires role: Admin or Editor' parameters: - - name: friaId + - + name: friaId in: path required: true schema: @@ -20317,45 +21225,49 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" - "/fria/{friaId}/risk-items": + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' + '/fria/{friaId}/risk-items': get: tags: - FRIA - summary: "Get Risk Items" + summary: 'Get Risk Items' operationId: getRiskItems security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: friaId + - + name: friaId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - FRIA - summary: "Add Risk Item" + summary: 'Add Risk Item' operationId: addRiskItem security: - - bearerAuth: [] - description: "Requires role: Admin or Editor" + - + bearerAuth: [] + description: 'Requires role: Admin or Editor' parameters: - - name: friaId + - + name: friaId in: path required: true schema: @@ -20366,30 +21278,33 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" - "/fria/{friaId}/risk-items/{itemId}": + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' + '/fria/{friaId}/risk-items/{itemId}': patch: tags: - FRIA - summary: "Update Risk Item" + summary: 'Update Risk Item' operationId: updateRiskItem security: - - bearerAuth: [] - description: "Requires role: Admin or Editor" + - + bearerAuth: [] + description: 'Requires role: Admin or Editor' parameters: - - name: friaId + - + name: friaId in: path required: true schema: type: integer - - name: itemId + - + name: itemId in: path required: true schema: @@ -20400,79 +21315,87 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' delete: tags: - FRIA - summary: "Delete Risk Item" + summary: 'Delete Risk Item' operationId: deleteRiskItem security: - - bearerAuth: [] - description: "Requires role: Admin or Editor" + - + bearerAuth: [] + description: 'Requires role: Admin or Editor' parameters: - - name: friaId + - + name: friaId in: path required: true schema: type: integer - - name: itemId + - + name: itemId in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" - "/fria/{friaId}/models": + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' + '/fria/{friaId}/models': get: tags: - FRIA - summary: "Get Model Links" + summary: 'Get Model Links' operationId: getModelLinks security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: friaId + - + name: friaId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/fria/{friaId}/models/{modelId}": + '500': + description: 'Internal server error' + '/fria/{friaId}/models/{modelId}': post: tags: - FRIA - summary: "Link Model" + summary: 'Link Model' operationId: linkModel security: - - bearerAuth: [] - description: "Requires role: Admin or Editor" + - + bearerAuth: [] + description: 'Requires role: Admin or Editor' parameters: - - name: friaId + - + name: friaId in: path required: true schema: type: integer - - name: modelId + - + name: modelId in: path required: true schema: @@ -20483,73 +21406,80 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' delete: tags: - FRIA - summary: "Unlink Model" + summary: 'Unlink Model' operationId: unlinkModel security: - - bearerAuth: [] - description: "Requires role: Admin or Editor" + - + bearerAuth: [] + description: 'Requires role: Admin or Editor' parameters: - - name: friaId + - + name: friaId in: path required: true schema: type: integer - - name: modelId + - + name: modelId in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" - "/fria/{friaId}/evidence": + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' + '/fria/{friaId}/evidence': get: tags: - FRIA - summary: "Get Fria Evidence" + summary: 'Get Fria Evidence' operationId: getFriaEvidence security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: friaId + - + name: friaId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' post: tags: - FRIA - summary: "Link Fria Evidence" + summary: 'Link Fria Evidence' operationId: linkFriaEvidence security: - - bearerAuth: [] - description: "Requires role: Admin or Editor" + - + bearerAuth: [] + description: 'Requires role: Admin or Editor' parameters: - - name: friaId + - + name: friaId in: path required: true schema: @@ -20560,54 +21490,59 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" - "/fria/{friaId}/evidence/{linkId}": + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' + '/fria/{friaId}/evidence/{linkId}': delete: tags: - FRIA - summary: "Unlink Fria Evidence" + summary: 'Unlink Fria Evidence' operationId: unlinkFriaEvidence security: - - bearerAuth: [] - description: "Requires role: Admin or Editor" + - + bearerAuth: [] + description: 'Requires role: Admin or Editor' parameters: - - name: friaId + - + name: friaId in: path required: true schema: type: integer - - name: linkId + - + name: linkId in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" - "/fria/{friaId}/submit": + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' + '/fria/{friaId}/submit': post: tags: - FRIA - summary: "Submit Fria" + summary: 'Submit Fria' operationId: submitFria security: - - bearerAuth: [] - description: "Requires role: Admin or Editor" + - + bearerAuth: [] + description: 'Requires role: Admin or Editor' parameters: - - name: friaId + - + name: friaId in: path required: true schema: @@ -20618,92 +21553,101 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" - "/fria/{friaId}/versions": + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' + '/fria/{friaId}/versions': get: tags: - FRIA - summary: "Get Versions" + summary: 'Get Versions' operationId: getVersions security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: friaId + - + name: friaId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/fria/{friaId}/versions/{version}": + '500': + description: 'Internal server error' + '/fria/{friaId}/versions/{version}': get: tags: - FRIA - summary: "Get Version" + summary: 'Get Version' operationId: getVersion security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: friaId + - + name: friaId in: path required: true schema: type: integer - - name: version + - + name: version in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/fria/{projectId}": + '500': + description: 'Internal server error' + '/fria/{projectId}': get: tags: - FRIA - summary: "Get Fria" + summary: 'Get Fria' operationId: getFria security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: projectId + - + name: projectId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' put: tags: - FRIA - summary: "Update Fria" + summary: 'Update Fria' operationId: updateFria security: - - bearerAuth: [] - description: "Requires role: Admin or Editor" + - + bearerAuth: [] + description: 'Requires role: Admin or Editor' parameters: - - name: projectId + - + name: projectId in: path required: true schema: @@ -20714,448 +21658,483 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' /governance-os/mappings: get: - summary: "Get All Mappings" + summary: 'Get All Mappings' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Governance OS" + - 'Governance OS' operationId: getAllMappings security: - - bearerAuth: [] + - + bearerAuth: [] post: - summary: "Create Mapping" + summary: 'Create Mapping' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Governance OS" + - 'Governance OS' operationId: createMapping security: - - bearerAuth: [] - description: "Requires role: Admin or Editor" - "/governance-os/mappings/between/{sourceId}/{targetId}": + - + bearerAuth: [] + description: 'Requires role: Admin or Editor' + '/governance-os/mappings/between/{sourceId}/{targetId}': get: - summary: "Get Mappings Between" + summary: 'Get Mappings Between' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Governance OS" + - 'Governance OS' operationId: getMappingsBetween security: - - bearerAuth: [] - "/governance-os/mappings/control/{controlType}/{controlId}": + - + bearerAuth: [] + '/governance-os/mappings/control/{controlType}/{controlId}': get: - summary: "Get Mappings For Control" + summary: 'Get Mappings For Control' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Governance OS" + - 'Governance OS' operationId: getMappingsForControl security: - - bearerAuth: [] - "/governance-os/mappings/{id}": + - + bearerAuth: [] + '/governance-os/mappings/{id}': put: - summary: "Update Mapping" + summary: 'Update Mapping' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Governance OS" + - 'Governance OS' operationId: updateMapping security: - - bearerAuth: [] - description: "Requires role: Admin or Editor" + - + bearerAuth: [] + description: 'Requires role: Admin or Editor' delete: - summary: "Delete Mapping" + summary: 'Delete Mapping' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Governance OS" + - 'Governance OS' operationId: deleteMapping security: - - bearerAuth: [] - description: "Requires role: Admin" + - + bearerAuth: [] + description: 'Requires role: Admin' /governance-os/mappings/bulk: post: - summary: "Create Bulk Mappings" + summary: 'Create Bulk Mappings' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Governance OS" + - 'Governance OS' operationId: createBulkMappings security: - - bearerAuth: [] - description: "Requires role: Admin or Editor" + - + bearerAuth: [] + description: 'Requires role: Admin or Editor' /governance-os/scenarios: get: - summary: "Get All Scenarios" + summary: 'Get All Scenarios' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Governance OS" + - 'Governance OS' operationId: getAllScenarios security: - - bearerAuth: [] + - + bearerAuth: [] post: - summary: "Create Scenario" + summary: 'Create Scenario' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Governance OS" + - 'Governance OS' operationId: createScenario security: - - bearerAuth: [] - description: "Requires role: Admin or Editor" - "/governance-os/scenarios/{id}": + - + bearerAuth: [] + description: 'Requires role: Admin or Editor' + '/governance-os/scenarios/{id}': get: - summary: "Get Scenario By Id" + summary: 'Get Scenario By Id' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Governance OS" + - 'Governance OS' operationId: getScenarioById security: - - bearerAuth: [] + - + bearerAuth: [] put: - summary: "Update Scenario" + summary: 'Update Scenario' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Governance OS" + - 'Governance OS' operationId: updateScenario security: - - bearerAuth: [] - description: "Requires role: Admin or Editor" + - + bearerAuth: [] + description: 'Requires role: Admin or Editor' delete: - summary: "Delete Scenario" + summary: 'Delete Scenario' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Governance OS" + - 'Governance OS' operationId: deleteScenario security: - - bearerAuth: [] - description: "Requires role: Admin" - "/governance-os/scenarios/{id}/activate": + - + bearerAuth: [] + description: 'Requires role: Admin' + '/governance-os/scenarios/{id}/activate': post: - summary: "Activate Scenario" + summary: 'Activate Scenario' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Governance OS" + - 'Governance OS' operationId: activateScenario security: - - bearerAuth: [] - description: "Requires role: Admin or Editor" + - + bearerAuth: [] + description: 'Requires role: Admin or Editor' /governance-os/scenarios/simulate: post: - summary: "Simulate Scenario" + summary: 'Simulate Scenario' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Governance OS" + - 'Governance OS' operationId: simulateScenario security: - - bearerAuth: [] + - + bearerAuth: [] /governance-os/activations: get: - summary: "Get Activation History" + summary: 'Get Activation History' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Governance OS" + - 'Governance OS' operationId: getActivationHistory security: - - bearerAuth: [] - "/governance-os/activations/{id}/deactivate": + - + bearerAuth: [] + '/governance-os/activations/{id}/deactivate': post: - summary: "Deactivate Scenario" + summary: 'Deactivate Scenario' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Governance OS" + - 'Governance OS' operationId: deactivateScenario security: - - bearerAuth: [] - description: "Requires role: Admin or Editor" - "/governance-os/activations/{id}/progress": + - + bearerAuth: [] + description: 'Requires role: Admin or Editor' + '/governance-os/activations/{id}/progress': get: - summary: "Get Scenario Progress" + summary: 'Get Scenario Progress' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Governance OS" + - 'Governance OS' operationId: getScenarioProgress security: - - bearerAuth: [] + - + bearerAuth: [] /governance-os/recommend: post: - summary: "Get Recommendations" + summary: 'Get Recommendations' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Governance OS" + - 'Governance OS' operationId: governance_os_getRecommendations security: - - bearerAuth: [] - "/governance-os/coverage/{projectId}": + - + bearerAuth: [] + '/governance-os/coverage/{projectId}': get: - summary: "Get Coverage" + summary: 'Get Coverage' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Governance OS" + - 'Governance OS' operationId: getCoverage security: - - bearerAuth: [] - "/governance-os/coverage/{projectId}/refresh": + - + bearerAuth: [] + '/governance-os/coverage/{projectId}/refresh': post: - summary: "Refresh Coverage" + summary: 'Refresh Coverage' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Governance OS" + - 'Governance OS' operationId: refreshCoverage security: - - bearerAuth: [] - description: "Requires role: Admin or Editor" - "/governance-os/unified-view/{projectId}": + - + bearerAuth: [] + description: 'Requires role: Admin or Editor' + '/governance-os/unified-view/{projectId}': get: - summary: "Get Unified View" + summary: 'Get Unified View' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Governance OS" + - 'Governance OS' operationId: getUnifiedView security: - - bearerAuth: [] + - + bearerAuth: [] /governance-os/eligibility: get: - summary: "Get Eligibility" + summary: 'Get Eligibility' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Governance OS" + - 'Governance OS' operationId: getEligibility security: - - bearerAuth: [] + - + bearerAuth: [] /governance-os/preferences: get: - summary: "Get Preferences" + summary: 'Get Preferences' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Governance OS" + - 'Governance OS' operationId: getPreferences security: - - bearerAuth: [] + - + bearerAuth: [] put: - summary: "Update Preferences" + summary: 'Update Preferences' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Governance OS" + - 'Governance OS' operationId: updatePreferences security: - - bearerAuth: [] - description: "Requires role: Admin" + - + bearerAuth: [] + description: 'Requires role: Admin' /risk-benchmarks: get: tags: - - "Risk Benchmarks" - summary: "Get All Benchmarks" + - 'Risk Benchmarks' + summary: 'Get All Benchmarks' operationId: getAllBenchmarks security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /risk-benchmarks/filters: get: tags: - - "Risk Benchmarks" - summary: "Get Benchmark Filters" + - 'Risk Benchmarks' + summary: 'Get Benchmark Filters' operationId: getBenchmarkFilters security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/risk-benchmarks/{id}": + '500': + description: 'Internal server error' + '/risk-benchmarks/{id}': get: tags: - - "Risk Benchmarks" - summary: "Get Benchmark By Id" + - 'Risk Benchmarks' + summary: 'Get Benchmark By Id' operationId: getBenchmarkById security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /quantitative-risks/portfolio/org: get: tags: - - "Quantitative Risks" - summary: "Get Org Portfolio" + - 'Quantitative Risks' + summary: 'Get Org Portfolio' operationId: getOrgPortfolio security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/quantitative-risks/portfolio/project/{projectId}": + '500': + description: 'Internal server error' + '/quantitative-risks/portfolio/project/{projectId}': get: tags: - - "Quantitative Risks" - summary: "Get Project Portfolio" + - 'Quantitative Risks' + summary: 'Get Project Portfolio' operationId: getProjectPortfolio security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: projectId + - + name: projectId in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /quantitative-risks/portfolio/trend: get: tags: - - "Quantitative Risks" - summary: "Get Portfolio Trend Handler" + - 'Quantitative Risks' + summary: 'Get Portfolio Trend Handler' operationId: getPortfolioTrendHandler security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/quantitative-risks/{riskId}/apply-benchmark/{benchmarkId}": + '500': + description: 'Internal server error' + '/quantitative-risks/{riskId}/apply-benchmark/{benchmarkId}': post: tags: - - "Quantitative Risks" - summary: "Apply Benchmark" + - 'Quantitative Risks' + summary: 'Apply Benchmark' operationId: applyBenchmark security: - - bearerAuth: [] + - + bearerAuth: [] parameters: - - name: riskId + - + name: riskId in: path required: true schema: type: integer - - name: benchmarkId + - + name: benchmarkId in: path required: true schema: @@ -21166,239 +22145,256 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' /quantitative-risks/assessment-mode: get: tags: - - "Quantitative Risks" - summary: "Get Risk Assessment Mode" + - 'Quantitative Risks' + summary: 'Get Risk Assessment Mode' operationId: getRiskAssessmentMode security: - - bearerAuth: [] + - + bearerAuth: [] responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" + '500': + description: 'Internal server error' put: tags: - - "Quantitative Risks" - summary: "Update Risk Assessment Mode" + - 'Quantitative Risks' + summary: 'Update Risk Assessment Mode' operationId: updateRiskAssessmentMode security: - - bearerAuth: [] + - + bearerAuth: [] requestBody: content: application/json: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "500": - description: "Internal server error" - "/custom-fields/definitions/by-id/{id}": + '500': + description: 'Internal server error' + '/custom-fields/definitions/by-id/{id}': get: - summary: "Get Custom Field Definition By Id" + summary: 'Get Custom Field Definition By Id' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Custom Fields" + - 'Custom Fields' operationId: getCustomFieldDefinitionById security: - - bearerAuth: [] - "/custom-fields/definitions/{entityType}": + - + bearerAuth: [] + '/custom-fields/definitions/{entityType}': get: - summary: "List Custom Field Definitions" + summary: 'List Custom Field Definitions' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Custom Fields" + - 'Custom Fields' operationId: listCustomFieldDefinitions security: - - bearerAuth: [] + - + bearerAuth: [] /custom-fields/definitions: post: - summary: "Create Custom Field Definition" + summary: 'Create Custom Field Definition' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Custom Fields" + - 'Custom Fields' operationId: createCustomFieldDefinition security: - - bearerAuth: [] - description: "Requires role: Admin" - "/custom-fields/definitions/{id}": + - + bearerAuth: [] + description: 'Requires role: Admin' + '/custom-fields/definitions/{id}': patch: - summary: "Update Custom Field Definition" + summary: 'Update Custom Field Definition' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Custom Fields" + - 'Custom Fields' operationId: updateCustomFieldDefinition security: - - bearerAuth: [] - description: "Requires role: Admin" + - + bearerAuth: [] + description: 'Requires role: Admin' delete: - summary: "Delete Custom Field Definition" + summary: 'Delete Custom Field Definition' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Custom Fields" + - 'Custom Fields' operationId: deleteCustomFieldDefinition security: - - bearerAuth: [] - description: "Requires role: Admin" - "/custom-fields/values/{entityType}/{entityId}/missing-required": + - + bearerAuth: [] + description: 'Requires role: Admin' + '/custom-fields/values/{entityType}/{entityId}/missing-required': get: - summary: "Get Missing Required Custom Fields" + summary: 'Get Missing Required Custom Fields' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Custom Fields" + - 'Custom Fields' operationId: getMissingRequiredCustomFields security: - - bearerAuth: [] - "/custom-fields/values/{entityType}/{entityId}": + - + bearerAuth: [] + '/custom-fields/values/{entityType}/{entityId}': get: - summary: "Get Custom Field Values For Entity" + summary: 'Get Custom Field Values For Entity' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Custom Fields" + - 'Custom Fields' operationId: getCustomFieldValuesForEntity security: - - bearerAuth: [] + - + bearerAuth: [] /custom-fields/values: put: - summary: "Set Custom Field Value" + summary: 'Set Custom Field Value' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Custom Fields" + - 'Custom Fields' operationId: setCustomFieldValue security: - - bearerAuth: [] - "/custom-fields/values/{definitionId}/{entityId}": + - + bearerAuth: [] + '/custom-fields/values/{definitionId}/{entityId}': delete: - summary: "Delete Custom Field Value" + summary: 'Delete Custom Field Value' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Custom Fields" + - 'Custom Fields' operationId: deleteCustomFieldValue security: - - bearerAuth: [] + - + bearerAuth: [] /super-admin/organizations: get: tags: - - "Super Admin" - summary: "List Organizations" + - 'Super Admin' + summary: 'List Organizations' operationId: listOrganizations security: - - bearerAuth: [] - description: "Requires role: Super Admin" + - + bearerAuth: [] + description: 'Requires role: Super Admin' responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' post: tags: - - "Super Admin" - summary: "Create Org" + - 'Super Admin' + summary: 'Create Org' operationId: createOrg security: - - bearerAuth: [] - description: "Requires role: Super Admin" + - + bearerAuth: [] + description: 'Requires role: Super Admin' requestBody: content: application/json: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" - "/super-admin/organizations/{id}": + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' + '/super-admin/organizations/{id}': delete: tags: - - "Super Admin" - summary: "Delete Org" + - 'Super Admin' + summary: 'Delete Org' operationId: deleteOrg security: - - bearerAuth: [] - description: "Requires role: Super Admin" + - + bearerAuth: [] + description: 'Requires role: Super Admin' parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' patch: tags: - - "Super Admin" - summary: "Update Org" + - 'Super Admin' + summary: 'Update Org' operationId: updateOrg security: - - bearerAuth: [] - description: "Requires role: Super Admin" + - + bearerAuth: [] + description: 'Requires role: Super Admin' parameters: - - name: id + - + name: id in: path required: true schema: @@ -21409,85 +22405,91 @@ paths: schema: type: object responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' /super-admin/users/count: get: tags: - - "Super Admin" - summary: "Get User Count" + - 'Super Admin' + summary: 'Get User Count' operationId: getUserCount security: - - bearerAuth: [] - description: "Requires role: Super Admin" + - + bearerAuth: [] + description: 'Requires role: Super Admin' responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' /super-admin/users: get: tags: - - "Super Admin" - summary: "List All Users" + - 'Super Admin' + summary: 'List All Users' operationId: listAllUsers security: - - bearerAuth: [] - description: "Requires role: Super Admin" + - + bearerAuth: [] + description: 'Requires role: Super Admin' responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" - "/super-admin/organizations/{id}/users": + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' + '/super-admin/organizations/{id}/users': get: tags: - - "Super Admin" - summary: "List Org Users" + - 'Super Admin' + summary: 'List Org Users' operationId: listOrgUsers security: - - bearerAuth: [] - description: "Requires role: Super Admin" + - + bearerAuth: [] + description: 'Requires role: Super Admin' parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": + '200': description: Success - "401": + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" - "/super-admin/organizations/{id}/invite": + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' + '/super-admin/organizations/{id}/invite': post: tags: - - "Super Admin" - summary: "Invite User To Org" + - 'Super Admin' + summary: 'Invite User To Org' operationId: inviteUserToOrg security: - - bearerAuth: [] - description: "Requires role: Super Admin" + - + bearerAuth: [] + description: 'Requires role: Super Admin' parameters: - - name: id + - + name: id in: path required: true schema: @@ -21498,56 +22500,59 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "401": + '201': + description: 'Created successfully' + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" - "/super-admin/users/{id}": + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' + '/super-admin/users/{id}': patch: - summary: "Update User" + summary: 'Update User' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Super Admin" + - 'Super Admin' operationId: updateUser security: - - bearerAuth: [] - description: "Requires role: Super Admin" + - + bearerAuth: [] + description: 'Requires role: Super Admin' delete: tags: - - "Super Admin" - summary: "Remove User" + - 'Super Admin' + summary: 'Remove User' operationId: removeUser security: - - bearerAuth: [] - description: "Requires role: Super Admin" + - + bearerAuth: [] + description: 'Requires role: Super Admin' parameters: - - name: id + - + name: id in: path required: true schema: type: integer responses: - "200": - description: "Deleted successfully" - "401": + '200': + description: 'Deleted successfully' + '401': description: Unauthorized - "403": - description: "Forbidden - insufficient role" - "500": - description: "Internal server error" + '403': + description: 'Forbidden - insufficient role' + '500': + description: 'Internal server error' /internal/ai-gateway/notify: post: tags: - Internal - summary: "AI Gateway notification callback" + summary: 'AI Gateway notification callback' operationId: internal_anonymous requestBody: content: @@ -21555,204 +22560,410 @@ paths: schema: type: object responses: - "201": - description: "Created successfully" - "500": - description: "Internal server error" + '201': + description: 'Created successfully' + '500': + description: 'Internal server error' /ssoConfig/feature: get: - summary: "Get S S O Feature Status" + summary: 'Get S S O Feature Status' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "SSO Config" + - 'SSO Config' operationId: getSSOFeatureStatus /ssoConfig/check-status: get: - summary: "Check S S O Status" + summary: 'Check S S O Status' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "SSO Config" + - 'SSO Config' operationId: checkSSOStatus /ssoConfig/orgs: get: - summary: "List S S O Orgs" + summary: 'List S S O Orgs' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "SSO Config" + - 'SSO Config' operationId: listSSOOrgs /ssoConfig: get: - summary: "Get S S O Config" + summary: 'Get S S O Config' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "SSO Config" + - 'SSO Config' operationId: getSSOConfig security: - - bearerAuth: [] + - + bearerAuth: [] put: - summary: "Save S S O Config" + summary: 'Save S S O Config' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "SSO Config" + - 'SSO Config' operationId: saveSSOConfig security: - - bearerAuth: [] + - + bearerAuth: [] /ssoConfig/enable: put: - summary: "Enable S S O" + summary: 'Enable S S O' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "SSO Config" + - 'SSO Config' operationId: enableSSO security: - - bearerAuth: [] + - + bearerAuth: [] /ssoConfig/disable: put: - summary: "Disable S S O" + summary: 'Disable S S O' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "SSO Config" + - 'SSO Config' operationId: disableSSO security: - - bearerAuth: [] + - + bearerAuth: [] /ai-trust-index/apps: get: - summary: "Get Apps" + summary: 'Get Apps' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Ai Trust Index" + - 'Ai Trust Index' operationId: getApps security: - - bearerAuth: [] - "/ai-trust-index/apps/{slug}": + - + bearerAuth: [] + '/ai-trust-index/apps/{slug}': get: - summary: "Get App" + summary: 'Get App' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Ai Trust Index" + - 'Ai Trust Index' operationId: getApp security: - - bearerAuth: [] + - + bearerAuth: [] /ai-trust-index/tracked: get: - summary: "Get Tracked" + summary: 'Get Tracked' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Ai Trust Index" + - 'Ai Trust Index' operationId: getTracked security: - - bearerAuth: [] + - + bearerAuth: [] post: - summary: "Track App" + summary: 'Track App' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Ai Trust Index" + - 'Ai Trust Index' operationId: trackApp security: - - bearerAuth: [] + - + bearerAuth: [] /ai-trust-index/tracked/bulk: post: - summary: "Track Apps Bulk" + summary: 'Track Apps Bulk' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Ai Trust Index" + - 'Ai Trust Index' operationId: trackAppsBulk security: - - bearerAuth: [] - "/ai-trust-index/tracked/{slug}": + - + bearerAuth: [] + '/ai-trust-index/tracked/{slug}': delete: - summary: "Untrack App" + summary: 'Untrack App' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Ai Trust Index" + - 'Ai Trust Index' operationId: untrackApp security: - - bearerAuth: [] + - + bearerAuth: [] /ai-trust-index/settings: get: - summary: "Get Settings" + summary: 'Get Settings' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Ai Trust Index" + - 'Ai Trust Index' operationId: ai_trust_index_getSettings security: - - bearerAuth: [] + - + bearerAuth: [] put: - summary: "Update Settings" + summary: 'Update Settings' responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' tags: - - "Ai Trust Index" + - 'Ai Trust Index' operationId: ai_trust_index_updateSettings security: - - bearerAuth: [] + - + bearerAuth: [] + /regulations-tracker/countries: + get: + summary: 'Get Countries' + responses: + '200': + description: Success + '500': + description: 'Internal server error' + tags: + - 'Regulations Tracker' + operationId: getCountries + security: + - + bearerAuth: [] + '/regulations-tracker/countries/{slug}/impact': + get: + summary: 'Get Impact Analysis' + responses: + '200': + description: Success + '500': + description: 'Internal server error' + tags: + - 'Regulations Tracker' + operationId: getImpactAnalysis + security: + - + bearerAuth: [] + '/regulations-tracker/countries/{slug}/impact/refresh': + post: + summary: 'Refresh Impact Analysis' + responses: + '200': + description: Success + '500': + description: 'Internal server error' + tags: + - 'Regulations Tracker' + operationId: refreshImpactAnalysis + security: + - + bearerAuth: [] + '/regulations-tracker/countries/{slug}': + get: + summary: 'Get Country Detail' + responses: + '200': + description: Success + '500': + description: 'Internal server error' + tags: + - 'Regulations Tracker' + operationId: getCountryDetail + security: + - + bearerAuth: [] + /regulations-tracker/tracked: + get: + summary: 'Get Tracked' + responses: + '200': + description: Success + '500': + description: 'Internal server error' + tags: + - 'Regulations Tracker' + operationId: regulations_tracker_getTracked + security: + - + bearerAuth: [] + post: + summary: 'Track Country Ctrl' + responses: + '200': + description: Success + '500': + description: 'Internal server error' + tags: + - 'Regulations Tracker' + operationId: trackCountryCtrl + security: + - + bearerAuth: [] + /regulations-tracker/tracked/bulk: + post: + summary: 'Track Bulk Ctrl' + responses: + '200': + description: Success + '500': + description: 'Internal server error' + tags: + - 'Regulations Tracker' + operationId: trackBulkCtrl + security: + - + bearerAuth: [] + '/regulations-tracker/tracked/{slug}': + delete: + summary: 'Untrack Country Ctrl' + responses: + '200': + description: Success + '500': + description: 'Internal server error' + tags: + - 'Regulations Tracker' + operationId: untrackCountryCtrl + security: + - + bearerAuth: [] + /regulations-tracker/settings: + get: + summary: 'Get Settings Ctrl' + responses: + '200': + description: Success + '500': + description: 'Internal server error' + tags: + - 'Regulations Tracker' + operationId: getSettingsCtrl + security: + - + bearerAuth: [] + put: + summary: 'Update Settings Ctrl' + responses: + '200': + description: Success + '500': + description: 'Internal server error' + tags: + - 'Regulations Tracker' + operationId: updateSettingsCtrl + security: + - + bearerAuth: [] + /regulations-tracker/horizon: + get: + summary: 'Get Horizon' + responses: + '200': + description: Success + '500': + description: 'Internal server error' + tags: + - 'Regulations Tracker' + operationId: getHorizon + security: + - + bearerAuth: [] + /regulations-tracker/deadlines: + get: + summary: 'Get Deadlines' + responses: + '200': + description: Success + '500': + description: 'Internal server error' + tags: + - 'Regulations Tracker' + operationId: getDeadlines + security: + - + bearerAuth: [] + /regulations-tracker/frameworks: + get: + summary: 'Get Frameworks' + responses: + '200': + description: Success + '500': + description: 'Internal server error' + tags: + - 'Regulations Tracker' + operationId: getFrameworks + security: + - + bearerAuth: [] + /regulations-tracker/sync: + post: + summary: 'Trigger Sync' + responses: + '200': + description: Success + '500': + description: 'Internal server error' + tags: + - 'Regulations Tracker' + operationId: regulations_tracker_triggerSync + security: + - + bearerAuth: [] /health: get: tags: - System - summary: "Health Check" + summary: 'Health Check' operationId: healthCheck responses: - "200": + '200': description: Success - "500": - description: "Internal server error" + '500': + description: 'Internal server error' diff --git a/Servers/templates/regulations-tracker-digest.mjml b/Servers/templates/regulations-tracker-digest.mjml new file mode 100644 index 0000000000..55e4178c99 --- /dev/null +++ b/Servers/templates/regulations-tracker-digest.mjml @@ -0,0 +1,35 @@ + + + + + Global AI regulations — update + + + + + + Regulations changed for countries your organization tracks. + + {{changedSection}} + {{removedSection}} + {{impactSection}} + + Browse + + + Or jump straight to tracked regulations + or manage your settings. + + + + + + + You receive this because you are a configured recipient for Regulations Tracker alerts. + An administrator can manage recipients in + Regulations Tracker settings. + + + + + diff --git a/Servers/utils/__tests__/regulationImpact.utils.test.ts b/Servers/utils/__tests__/regulationImpact.utils.test.ts new file mode 100644 index 0000000000..1cfad569f3 --- /dev/null +++ b/Servers/utils/__tests__/regulationImpact.utils.test.ts @@ -0,0 +1,548 @@ +jest.mock("../../database/db", () => ({ + sequelize: { query: jest.fn() }, +})); +jest.mock("../../advisor/aiSdkAgent", () => ({ runAdvisorAiSdk: jest.fn() })); +jest.mock("../logger/logHelper", () => ({ logFailure: jest.fn() })); +jest.mock("../llmKey.utils", () => ({ + getLLMKeysWithKeyQuery: jest.fn(), + getLLMProviderUrl: jest.fn().mockReturnValue("https://api.openai.com/v1/"), +})); +// BUG 3: normalizeSlug is now exported and used — mock the module, keeping it +// real so slug normalization happens correctly in these unit tests. +jest.mock("../regulationsTracker.utils", () => ({ + normalizeSlug: (s: string) => String(s).trim().toLowerCase(), +})); + +import { sequelize } from "../../database/db"; +import { runAdvisorAiSdk } from "../../advisor/aiSdkAgent"; +import { getCandidates } from "../regulationImpact.utils"; +import { + buildUserPrompt, + analyzeType, + SYSTEM_PROMPTS, + buildContext, +} from "../regulationImpact.utils"; + +import { + regionForCountry, + frameworksForRegulation, + validateVerdicts, +} from "../regulationImpact.utils"; + +import { getLLMKeysWithKeyQuery } from "../llmKey.utils"; +import { runImpactAnalysis } from "../regulationImpact.utils"; + +describe("regionForCountry", () => { + it("maps known European countries to 2", () => { + expect(regionForCountry("Germany")).toBe(2); + expect(regionForCountry("France")).toBe(2); + }); + it("maps the EU bloc entry to Europe", () => { + expect(regionForCountry("European Union")).toBe(2); + }); + it("maps the US to North America", () => { + expect(regionForCountry("United States")).toBe(3); + }); + it("returns null for an unknown country", () => { + expect(regionForCountry("Atlantis")).toBeNull(); + }); +}); + +describe("frameworksForRegulation", () => { + it("maps an EU AI Act regulation to the EU AI Act framework", () => { + expect(frameworksForRegulation({ type: "EU AI Act", country: "European Union" })).toContain( + "EU AI Act", + ); + }); + it("returns an empty array when no framework maps", () => { + expect(frameworksForRegulation({ type: "Local guidance", country: "Atlantis" })).toEqual([]); + }); +}); + +describe("validateVerdicts", () => { + const sent = [ + { type: "system" as const, id: 1, name: "A", description: "" }, + { type: "system" as const, id: 2, name: "B", description: "" }, + ]; + it("keeps valid entries that were sent", () => { + const raw = { results: [{ type: "system", id: 1, affected: true, why: "x" }] }; + expect(validateVerdicts(raw, sent)).toEqual([ + { type: "system", id: 1, affected: true, why: "x" }, + ]); + }); + it("drops hallucinated ids not in the sent set", () => { + const raw = { results: [{ type: "system", id: 99, affected: true, why: "x" }] }; + expect(validateVerdicts(raw, sent)).toEqual([]); + }); + it("drops entries with empty why", () => { + const raw = { results: [{ type: "system", id: 1, affected: true, why: "" }] }; + expect(validateVerdicts(raw, sent)).toEqual([]); + }); + it("drops entries with non-boolean affected", () => { + const raw = { results: [{ type: "system", id: 1, affected: "yes", why: "x" }] }; + expect(validateVerdicts(raw, sent)).toEqual([]); + }); + it("returns [] for malformed input", () => { + expect(validateVerdicts(null, sent)).toEqual([]); + expect(validateVerdicts({ nope: 1 }, sent)).toEqual([]); + }); + it("coerces numeric-string ids that some models emit", () => { + const raw = { results: [{ type: "system", id: "2", affected: false, why: "ok" }] }; + expect(validateVerdicts(raw, sent)).toEqual([ + { type: "system", id: 2, affected: false, why: "ok" }, + ]); + }); + it("still drops non-numeric string ids and hallucinated coerced ids", () => { + expect( + validateVerdicts( + { results: [{ type: "system", id: "abc", affected: true, why: "x" }] }, + sent, + ), + ).toEqual([]); + // "99" coerces to 99 but 99 was never sent → rejected by the sentKeys guard. + expect( + validateVerdicts({ results: [{ type: "system", id: "99", affected: true, why: "x" }] }, sent), + ).toEqual([]); + }); +}); + +describe("getCandidates", () => { + const q = sequelize.query as jest.Mock; + beforeEach(() => q.mockReset()); + + it("returns candidates grouped by type", async () => { + // 5 real queries fire when systems and controls are both non-empty: + // systems, controls, assessments (projectIds non-empty), vendors, policies (controlIds non-empty) + q.mockResolvedValueOnce([{ id: 1, name: "Resume Ranker", description: "hiring" }]); // systems + q.mockResolvedValueOnce([{ id: 7, name: "Human oversight", description: "" }]); // controls + q.mockResolvedValueOnce([]); // assessments + q.mockResolvedValueOnce([{ id: 3, name: "OpenAI", description: "vendor" }]); // vendors + q.mockResolvedValueOnce([]); // policies + + const out = await getCandidates(7, "European Union", { + type: "EU AI Act", + country: "European Union", + }); + + expect(out.system).toEqual([ + { type: "system", id: 1, name: "Resume Ranker", description: "hiring" }, + ]); + expect(out.control).toEqual([ + { type: "control", id: 7, name: "Human oversight", description: "" }, + ]); + expect(out.assessment).toEqual([]); + expect(out.vendor).toEqual([{ type: "vendor", id: 3, name: "OpenAI", description: "vendor" }]); + expect(out.policy).toEqual([]); + expect(q).toHaveBeenCalledTimes(5); + }); + + it("skips the assessments query and returns [] when systems is empty", async () => { + // systems empty → candidateProjectIds=[] → assessments branch skipped + // controls empty → policies branch skipped + // Only 3 real queries: systems, controls, vendors + q.mockResolvedValue([]); + const out = await getCandidates(5, "Germany", { type: "EU AI Act" }); + + expect(out.assessment).toEqual([]); + // No assessments query should have fired + const sqls = q.mock.calls.map((c: unknown[]) => c[0] as string); + expect(sqls.some((s) => /FROM assessments/.test(s))).toBe(false); + expect(q).toHaveBeenCalledTimes(3); // systems, controls, vendors only + }); + + it("skips the policies query and returns [] when controls is empty", async () => { + // systems non-empty → assessments query fires + // controls empty → policies branch skipped + // Total: 4 queries: systems, controls, assessments, vendors + q.mockResolvedValueOnce([{ id: 10, name: "Proj A", description: "" }]); // systems + q.mockResolvedValueOnce([]); // controls (empty) + q.mockResolvedValueOnce([]); // assessments + q.mockResolvedValueOnce([]); // vendors + + const out = await getCandidates(5, "European Union", { type: "EU AI Act" }); + + expect(out.policy).toEqual([]); + const sqls = q.mock.calls.map((c: unknown[]) => c[0] as string); + expect(sqls.some((s) => /policy_manager/.test(s))).toBe(false); + expect(q).toHaveBeenCalledTimes(4); // systems, controls, assessments, vendors — no policies + }); + + it("scopes every query to organization_id", async () => { + q.mockResolvedValue([]); + await getCandidates(42, "Germany", { type: "EU AI Act" }); + for (const call of q.mock.calls) { + expect(call[1].replacements.organizationId).toBe(42); + } + }); + + it("uses IN (:param) not = ANY(:param) for all array replacements", async () => { + q.mockResolvedValue([]); + await getCandidates(1, "European Union", { type: "EU AI Act", country: "European Union" }); + const sqls = q.mock.calls.map((c: unknown[]) => c[0] as string); + for (const sql of sqls) { + expect(sql).not.toMatch(/=\s*ANY\s*\(/); + if (/IN\s*\(/.test(sql)) { + expect(sql).toMatch(/IN\s*\(/); + } + } + }); +}); + +const ctx = { + name: "AI Act", + type: "EU AI Act", + status: "in force", + country: "European Union", + obligations: ["human oversight"], + maxPenalty: "€35M", + changeLines: ["status: draft → in force"], +}; + +describe("buildUserPrompt", () => { + it("includes regulation header, the change, and each candidate line", () => { + const p = buildUserPrompt("system", ctx, [ + { type: "system", id: 1, name: "Resume Ranker", description: "hiring tool" }, + ]); + expect(p).toContain("EU AI Act"); + expect(p).toContain("status: draft → in force"); + expect(p).toContain('id=1 "Resume Ranker"'); + }); +}); + +describe("SYSTEM_PROMPTS", () => { + it("has a prompt for every entity type with the conservative rule", () => { + for (const t of ["system", "control", "policy", "vendor", "assessment"] as const) { + expect(SYSTEM_PROMPTS[t]).toContain("conservative"); + } + }); +}); + +describe("buildContext", () => { + // Two regulations, each with its own obligations. Only the first changes. + const data = { + name: "Testland", + regulations: [ + { + name: "Alpha Act", + type: "law", + status: "in-force", + maxPenalty: "€10m", + obligations: ["Alpha obligation 1", "Alpha obligation 2"], + }, + { + name: "Beta Act", + type: "law", + status: "proposed", + obligations: ["Beta obligation 1", "Beta obligation 2", "Beta obligation 3"], + }, + ], + history: { + lastChange: { + date: "2026-07-15", + changes: [ + { + field: "regulation.status", + regulation: "Alpha Act", + from: "Proposed", + to: "Enacted", + }, + { + field: "regulation.effectiveDate", + regulation: "Alpha Act", + from: "2026-09-01", + to: "2026-02-01", + }, + ], + }, + }, + }; + + it("captures regulation.status and regulation.effectiveDate changes with the regulation name", () => { + const ctx = buildContext("testland", data); + expect(ctx.changeLines).toContain("Alpha Act: status Proposed → Enacted"); + expect(ctx.changeLines).toContain("Alpha Act: effective date 2026-09-01 → 2026-02-01"); + }); + + it("scopes obligations to the changed regulation only", () => { + const ctx = buildContext("testland", data); + // Only Alpha Act changed → only its two obligations, not Beta's three. + expect(ctx.obligations).toEqual(["Alpha obligation 1", "Alpha obligation 2"]); + expect(ctx.obligations).not.toContain("Beta obligation 1"); + }); + + it("falls back to all obligations when no specific regulation can be identified", () => { + const countOnly = { + ...data, + history: { + lastChange: { date: "x", changes: [{ field: "regulationCount", from: 2, to: 3 }] }, + }, + }; + const ctx = buildContext("testland", countOnly); + // regulationCount change names no regulation → keep all five obligations. + expect(ctx.obligations).toHaveLength(5); + expect(ctx.changeLines).toContain("regulation count 2 → 3"); + }); + + it("handles an added regulation and a missing history without throwing", () => { + const added = buildContext("testland", { + ...data, + history: { + lastChange: { + date: "x", + changes: [{ field: "regulation", change: "added", value: "Beta Act" }], + }, + }, + }); + expect(added.changeLines).toContain("regulation added: Beta Act"); + // "Beta Act" matches regs[1] → its three obligations are scoped in. + expect(added.obligations).toEqual([ + "Beta obligation 1", + "Beta obligation 2", + "Beta obligation 3", + ]); + + const noHistory = buildContext("testland", { ...data, history: null }); + expect(noHistory.changeLines).toEqual([]); + // No structured change → fall back to all obligations. + expect(noHistory.obligations).toHaveLength(5); + }); +}); + +describe("analyzeType", () => { + const creds = { apiKey: "k", baseURL: "u", model: "m", provider: "OpenAI" as const }; + const cands = [{ type: "system" as const, id: 1, name: "A", description: "" }]; + beforeEach(() => (runAdvisorAiSdk as jest.Mock).mockReset()); + + it("returns ok:true with verdicts for a good JSON response", async () => { + (runAdvisorAiSdk as jest.Mock).mockResolvedValue( + '{"results":[{"type":"system","id":1,"affected":true,"why":"in scope"}]}', + ); + const out = await analyzeType("system", ctx, cands, creds, 7); + expect(out.ok).toBe(true); + if (out.ok) { + expect(out.verdicts).toEqual([{ type: "system", id: 1, affected: true, why: "in scope" }]); + } + }); + + it("returns ok:false (not ok:true with []) when the LLM throws", async () => { + (runAdvisorAiSdk as jest.Mock).mockRejectedValue(new Error("provider down")); + const out = await analyzeType("system", ctx, cands, creds, 7); + expect(out.ok).toBe(false); + }); + + it("returns ok:false for non-JSON text (parse error is a failure, not empty success)", async () => { + (runAdvisorAiSdk as jest.Mock).mockResolvedValue("Sorry, I cannot help."); + const out = await analyzeType("system", ctx, cands, creds, 7); + expect(out.ok).toBe(false); + }); + + it("strips markdown fences and parses the inner JSON, returning ok:true", async () => { + (runAdvisorAiSdk as jest.Mock).mockResolvedValue( + '```json\n{"results":[{"type":"system","id":1,"affected":false,"why":"not in scope"}]}\n```', + ); + const out = await analyzeType("system", ctx, cands, creds, 7); + expect(out.ok).toBe(true); + if (out.ok) { + expect(out.verdicts).toEqual([ + { type: "system", id: 1, affected: false, why: "not in scope" }, + ]); + } + }); + + it("returns ok:true with empty verdicts when the LLM returns all not-affected (genuine empty)", async () => { + (runAdvisorAiSdk as jest.Mock).mockResolvedValue( + '{"results":[{"type":"system","id":1,"affected":false,"why":"not in scope"}]}', + ); + const out = await analyzeType("system", ctx, cands, creds, 7); + expect(out.ok).toBe(true); + if (out.ok) { + // validateVerdicts passes all entries through (including affected:false), so verdicts.length >= 0 + expect(Array.isArray(out.verdicts)).toBe(true); + } + }); +}); + +describe("runImpactAnalysis", () => { + const q = sequelize.query as jest.Mock; + beforeEach(() => { + q.mockReset(); + (getLLMKeysWithKeyQuery as jest.Mock).mockReset(); + (runAdvisorAiSdk as jest.Mock).mockReset(); + }); + + it("returns no_key and does not call the LLM when the org has no key", async () => { + (getLLMKeysWithKeyQuery as jest.Mock).mockResolvedValue([]); + // regulation_countries row lookup + q.mockResolvedValueOnce([ + { data: { name: "AI Act", regulations: [], history: null }, hash: "h1" }, + ]); + const out = await runImpactAnalysis(7, "eu"); + expect(out.status).toBe("no_key"); + expect(out.cached).toBe(false); + expect(runAdvisorAiSdk).not.toHaveBeenCalled(); + }); + + it("returns skipped_no_candidates when Stage A is empty for all types", async () => { + (getLLMKeysWithKeyQuery as jest.Mock).mockResolvedValue([ + { key: "k", name: "OpenAI", url: null, model: "gpt-4o" }, + ]); + q.mockResolvedValueOnce([ + { + data: { name: "AI Act", country: "European Union", regulations: [], history: null }, + hash: "h1", + }, + ]); // reg row + // no cached row + q.mockResolvedValueOnce([]); // getImpactRow + // Stage A: 5 queries all empty + q.mockResolvedValue([]); + const out = await runImpactAnalysis(7, "eu"); + expect(out.status).toBe("skipped_no_candidates"); + expect(out.cached).toBe(false); + expect(runAdvisorAiSdk).not.toHaveBeenCalled(); + }); + + it("does NOT use stale cache when regulation_hash differs — proceeds to Stage A and returns skipped_no_candidates", async () => { + (getLLMKeysWithKeyQuery as jest.Mock).mockResolvedValue([ + { key: "k", name: "OpenAI", url: null, model: "gpt-4o" }, + ]); + // regulation_countries returns hash "h2" (new hash) + q.mockResolvedValueOnce([ + { + data: { name: "AI Act", country: "European Union", regulations: [], history: null }, + hash: "h2", + }, + ]); // reg row + // getImpactRow returns a cached row with OLD hash "h1" and status "ok" + q.mockResolvedValueOnce([ + { + regulation_hash: "h1", + status: "ok", + result: { + systems: [], + controls: [], + policies: [], + vendors: [], + assessments: [], + generatedAt: "x", + }, + refreshed_at: "t", + }, + ]); // stale cached row + // Stage A: all candidate queries return empty + q.mockResolvedValue([]); + const out = await runImpactAnalysis(7, "eu"); + // Must NOT return cached "ok" — hash mismatch forces re-analysis + expect(out.status).toBe("skipped_no_candidates"); + expect(out.cached).toBe(false); + expect(runAdvisorAiSdk).not.toHaveBeenCalled(); + }); + + it("reuses a cached row when hash matches and returns cached:true", async () => { + (getLLMKeysWithKeyQuery as jest.Mock).mockResolvedValue([ + { key: "k", name: "OpenAI", url: null, model: "gpt-4o" }, + ]); + q.mockResolvedValueOnce([ + { data: { name: "AI Act", regulations: [], history: null }, hash: "h1" }, + ]); // reg row + q.mockResolvedValueOnce([ + { + regulation_hash: "h1", + status: "ok", + result: { + systems: [], + controls: [], + policies: [], + vendors: [], + assessments: [], + generatedAt: "x", + }, + refreshed_at: "t", + }, + ]); // cached, hash matches + const out = await runImpactAnalysis(7, "eu"); + expect(out.status).toBe("ok"); + // BUG 2 / BUG 5: cached:true signals that no LLM call was made + expect(out.cached).toBe(true); + expect(runAdvisorAiSdk).not.toHaveBeenCalled(); + }); + + // BUG 1: all analyzeType calls fail → status "error", NOT "ok" (no cache poisoning) + it("returns status:error (not ok) when every LLM type call fails", async () => { + (getLLMKeysWithKeyQuery as jest.Mock).mockResolvedValue([ + { key: "k", name: "OpenAI", url: null, model: "gpt-4o" }, + ]); + q.mockResolvedValueOnce([ + { + data: { name: "AI Act", country: "European Union", regulations: [], history: null }, + hash: "h1", + }, + ]); // reg row + q.mockResolvedValueOnce([]); // getImpactRow — no cache + // Stage A: one non-empty type so Stage B fires + q.mockResolvedValueOnce([{ id: 1, name: "Sys", description: "" }]); // systems + q.mockResolvedValue([]); // controls, vendors, (no assessments/policies branches) + // Every LLM call throws + (runAdvisorAiSdk as jest.Mock).mockRejectedValue(new Error("provider down")); + // upsertImpactRow call + q.mockResolvedValue([]); + const out = await runImpactAnalysis(7, "eu"); + expect(out.status).toBe("error"); + expect(out.result).toBeNull(); + expect(out.cached).toBe(false); + }); + + // BUG 2: force=true bypasses the cache even when hash matches + it("bypasses cache and re-runs LLM when force=true", async () => { + (getLLMKeysWithKeyQuery as jest.Mock).mockResolvedValue([ + { key: "k", name: "OpenAI", url: null, model: "gpt-4o" }, + ]); + q.mockResolvedValueOnce([ + { + data: { name: "AI Act", country: "European Union", regulations: [], history: null }, + hash: "h1", + }, + ]); // reg row + // No getImpactRow call expected when force=true (cache is skipped entirely) + // Stage A: all empty → skipped_no_candidates (no LLM call needed, just prove cache bypassed) + q.mockResolvedValue([]); + const out = await runImpactAnalysis(7, "eu", true); + // force=true skipped the cache — result is from a fresh run + expect(out.status).toBe("skipped_no_candidates"); + expect(out.cached).toBe(false); + }); + + // BUG 3: mixed-case / whitespace slug is normalized and resolves to the same row + it("normalizes a mixed-case slug so it resolves to the same cached row as lowercase", async () => { + (getLLMKeysWithKeyQuery as jest.Mock).mockResolvedValue([ + { key: "k", name: "OpenAI", url: null, model: "gpt-4o" }, + ]); + // Catalog row stored under normalized "eu" + q.mockResolvedValueOnce([ + { data: { name: "AI Act", regulations: [], history: null }, hash: "h1" }, + ]); // reg row + // Cache row also stored under "eu" (normalized) + q.mockResolvedValueOnce([ + { + regulation_hash: "h1", + status: "ok", + result: { + systems: [], + controls: [], + policies: [], + vendors: [], + assessments: [], + generatedAt: "x", + }, + refreshed_at: "t", + }, + ]); + // Pass " EU " — should normalize to "eu" and hit the cache + const out = await runImpactAnalysis(7, " EU "); + expect(out.status).toBe("ok"); + expect(out.cached).toBe(true); + // Verify the normalized slug was used in the DB query + const firstQueryReplacements = (q.mock.calls[0][1] as any).replacements; + expect(firstQueryReplacements.slug).toBe("eu"); + }); +}); diff --git a/Servers/utils/__tests__/regulationsTracker.utils.test.ts b/Servers/utils/__tests__/regulationsTracker.utils.test.ts new file mode 100644 index 0000000000..bdf86717cb --- /dev/null +++ b/Servers/utils/__tests__/regulationsTracker.utils.test.ts @@ -0,0 +1,118 @@ +import { + renderChangeLine, + currentIsoWeek, + currentIsoDay, + escapeHtml, + countChangesSince, +} from "../regulationsTracker.utils"; + +describe("renderChangeLine", () => { + it("renders status change", () => { + expect( + renderChangeLine({ + field: "regulation.status", + regulation: "EU AI Act", + from: "proposed", + to: "in-force", + }), + ).toBe("EU AI Act: status proposed → in-force"); + }); + it("renders effective date change", () => { + expect( + renderChangeLine({ + field: "regulation.effectiveDate", + regulation: "X", + from: "2024", + to: "2026", + }), + ).toBe("X: effective date 2024 → 2026"); + }); + it("renders added/removed", () => { + expect(renderChangeLine({ field: "regulation", change: "added", value: "New Bill" })).toBe( + "Added: New Bill", + ); + expect(renderChangeLine({ field: "regulation", change: "removed", value: "Old Bill" })).toBe( + "Removed: Old Bill", + ); + }); +}); + +describe("currentIsoWeek", () => { + it("returns YYYY-Www format", () => { + expect(currentIsoWeek(new Date("2026-06-25T00:00:00Z"))).toMatch(/^\d{4}-W\d{2}$/); + }); +}); + +describe("currentIsoDay", () => { + it("returns the UTC calendar day as YYYY-MM-DD", () => { + expect(currentIsoDay(new Date("2026-06-29T13:45:00Z"))).toBe("2026-06-29"); + }); + it("uses UTC, not local time, near a day boundary", () => { + expect(currentIsoDay(new Date("2026-06-29T23:59:59Z"))).toBe("2026-06-29"); + }); + it("fits the legacy last_run_week VARCHAR(10) width", () => { + expect(currentIsoDay(new Date("2026-06-29T00:00:00Z")).length).toBe(10); + }); +}); + +describe("escapeHtml", () => { + it("escapes HTML metacharacters", () => { + expect(escapeHtml("\"&'")).toBe("<b>"&'"); + }); +}); + +describe("countChangesSince", () => { + const hist = (entries: { date: string; hash: string }[]) => + ({ + firstAssessed: "", + lastChanged: "", + lastChecked: "", + assessmentCount: entries.length, + hashHistory: entries.map((e) => ({ ...e, regulationCount: 0 })), + lastChange: null, + }) as any; + + it("returns count 1 and no dates when there is no hashHistory", () => { + expect(countChangesSince(null, "h1")).toEqual({ count: 1, dates: [] }); + expect(countChangesSince(hist([]), "h1")).toEqual({ count: 1, dates: [] }); + }); + + it("counts only assessments newer than the stored hash, newest date first", () => { + const h = hist([ + { date: "2026-01-01", hash: "h1" }, + { date: "2026-02-01", hash: "h2" }, + { date: "2026-03-01", hash: "h3" }, + { date: "2026-04-01", hash: "h4" }, + ]); + // Stored at h2 -> h3 and h4 are newer (2 changes), newest first. + expect(countChangesSince(h, "h2")).toEqual({ + count: 2, + dates: ["2026-04-01", "2026-03-01"], + }); + }); + + it("treats a single change since stored hash as count 1", () => { + const h = hist([ + { date: "2026-01-01", hash: "h1" }, + { date: "2026-02-01", hash: "h2" }, + ]); + expect(countChangesSince(h, "h1")).toEqual({ count: 1, dates: ["2026-02-01"] }); + }); + + it("falls back to the latest entry when the stored hash is not found", () => { + const h = hist([ + { date: "2026-01-01", hash: "h1" }, + { date: "2026-02-01", hash: "h2" }, + ]); + // Unknown stored hash (e.g. our row predates this history) -> report the latest. + expect(countChangesSince(h, "stale-unknown")).toEqual({ count: 1, dates: ["2026-02-01"] }); + }); + + it("falls back to the latest entry when there is no stored hash", () => { + const h = hist([ + { date: "2026-01-01", hash: "h1" }, + { date: "2026-02-01", hash: "h2" }, + ]); + expect(countChangesSince(h, undefined)).toEqual({ count: 1, dates: ["2026-02-01"] }); + }); +}); diff --git a/Servers/utils/__tests__/regulationsTrackerFeed.test.ts b/Servers/utils/__tests__/regulationsTrackerFeed.test.ts new file mode 100644 index 0000000000..03b5a33d23 --- /dev/null +++ b/Servers/utils/__tests__/regulationsTrackerFeed.test.ts @@ -0,0 +1,92 @@ +import { validateManifest, ABSOLUTE_FLOOR } from "../regulationsTrackerFeed"; + +function makeCountry(slug: string) { + return { + slug, + name: slug, + region: "europe", + regulationCount: 1, + hash: "sha256-x", + history: null, + url: `/c/${slug}`, + }; +} +function manifest(n: number, extra: Record = {}) { + return { + feedVersion: 1, + generatedAt: "2026-06-25T00:00:00Z", + counts: { countries: n }, + countries: Array.from({ length: n }, (_, i) => makeCountry("c" + i)), + ...extra, + }; +} + +describe("validateManifest", () => { + it("rejects wrong feedVersion", () => { + const r = validateManifest(manifest(30, { feedVersion: 2 }), null); + expect(r.ok).toBe(false); + }); + + it("rejects when valid count is below absolute floor", () => { + // ABSOLUTE_FLOOR - 1 valid entries → rejected (gates on valid count now) + const r = validateManifest(manifest(ABSOLUTE_FLOOR - 1), null); + expect(r.ok).toBe(false); + }); + + it("rejects when valid count is below 50% of last good count", () => { + // 30 valid entries against lastGoodCount 100 → 30 < 50 → rejected + const r = validateManifest(manifest(30), 100); + expect(r.ok).toBe(false); + }); + + it("accepts a healthy feed and returns presentSlugs + rawCount", () => { + const r = validateManifest(manifest(30), 40); + expect(r.ok).toBe(true); + if (r.ok) { + expect(r.countries.length).toBe(30); + expect(r.presentSlugs.length).toBe(30); + expect(r.rawCount).toBe(30); + } + }); + + it("keeps a present-but-malformed country in presentSlugs but not in valid countries", () => { + const m = manifest(25); + (m.countries as any[]).push({ slug: "broken" }); // missing hash/name + m.counts.countries = m.countries.length; + const r = validateManifest(m, null); + expect(r.ok).toBe(true); + if (r.ok) { + expect(r.presentSlugs).toContain("broken"); + expect(r.countries.find((c) => c.slug === "broken")).toBeUndefined(); + } + }); + + it("rejects a feed with healthy rawCount but >50% malformed entries (gates on valid count)", () => { + // 60 raw entries: 25 valid (above ABSOLUTE_FLOOR=20) + 35 malformed. + // lastGoodCount = 60 → need valid >= 30 to pass 50% gate; 25 < 30 → rejected on 50% gate. + const m = manifest(25); // 25 valid entries + const malformed = Array.from({ length: 35 }, (_, i) => ({ slug: `bad-${i}` })); // missing hash/name/region + (m.countries as any[]).push(...malformed); + m.counts.countries = m.countries.length; // 60 raw entries + const r = validateManifest(m, 60); + expect(r.ok).toBe(false); + if (!r.ok) { + expect(r.reason).toMatch(/below 50%/); + } + }); + + it("rejects a feed whose valid count is below the absolute floor even if rawCount is above it", () => { + // 30 raw entries but only ABSOLUTE_FLOOR - 1 valid → should reject + const m = manifest(ABSOLUTE_FLOOR - 1); // valid entries + const malformed = Array.from({ length: 30 - (ABSOLUTE_FLOOR - 1) }, (_, i) => ({ + slug: `bad-${i}`, + })); + (m.countries as any[]).push(...malformed); + m.counts.countries = m.countries.length; + const r = validateManifest(m, null); + expect(r.ok).toBe(false); + if (!r.ok) { + expect(r.reason).toMatch(/absolute floor/); + } + }); +}); diff --git a/Servers/utils/regulationImpact.utils.ts b/Servers/utils/regulationImpact.utils.ts new file mode 100644 index 0000000000..d3cd462bd8 --- /dev/null +++ b/Servers/utils/regulationImpact.utils.ts @@ -0,0 +1,640 @@ +import { sequelize } from "../database/db"; +import { QueryTypes } from "sequelize"; +import { runAdvisorAiSdk } from "../advisor/aiSdkAgent"; +import { logFailure } from "./logger/logHelper"; +import { getLLMKeysWithKeyQuery, getLLMProviderUrl } from "./llmKey.utils"; +import { normalizeSlug } from "./regulationsTracker.utils"; + +export type EntityType = "system" | "control" | "policy" | "vendor" | "assessment"; + +export interface Candidate { + type: EntityType; + id: number; + name: string; + description: string; +} + +export interface LlmVerdict { + type: EntityType; + id: number; + affected: boolean; + why: string; +} + +// geography enum: 1 Global, 2 Europe, 3 North America, 4 South America, 5 Asia, 6 Africa +const REGION_BY_COUNTRY: Record = { + "european union": 2, + germany: 2, + france: 2, + italy: 2, + spain: 2, + netherlands: 2, + "united kingdom": 2, + ireland: 2, + poland: 2, + sweden: 2, + "united states": 3, + canada: 3, + mexico: 3, + brazil: 4, + argentina: 4, + chile: 4, + china: 5, + japan: 5, + "south korea": 5, + india: 5, + singapore: 5, + "south africa": 6, + nigeria: 6, + kenya: 6, + egypt: 6, +}; + +export function regionForCountry(countryName: string): number | null { + if (!countryName) return null; + const key = countryName.trim().toLowerCase(); + return REGION_BY_COUNTRY[key] ?? null; +} + +const FRAMEWORK_BY_TYPE: Record = { + "eu ai act": ["EU AI Act"], + "iso 42001": ["ISO 42001"], + "iso/iec 42001": ["ISO 42001"], + "iso 27001": ["ISO 27001"], + "iso/iec 27001": ["ISO 27001"], + "nist ai rmf": ["NIST AI RMF"], +}; + +export function frameworksForRegulation(reg: { type?: string; country?: string }): string[] { + const t = (reg.type ?? "").trim().toLowerCase(); + if (FRAMEWORK_BY_TYPE[t]) return FRAMEWORK_BY_TYPE[t]; + // EU-bloc regulations imply the EU AI Act framework even when type is free-text. + if ((reg.country ?? "").trim().toLowerCase() === "european union") return ["EU AI Act"]; + return []; +} + +export function validateVerdicts(raw: unknown, sent: Candidate[]): LlmVerdict[] { + if (!raw || typeof raw !== "object") return []; + const results = (raw as { results?: unknown }).results; + if (!Array.isArray(results)) return []; + const sentKeys = new Set(sent.map((c) => `${c.type}:${c.id}`)); + const out: LlmVerdict[] = []; + for (const r of results) { + if (!r || typeof r !== "object") continue; + const { type, id: rawId, affected, why } = r as Record; + if (typeof type !== "string") continue; + // Some models serialize numeric ids as JSON strings ("id": "42"). Coerce + // those rather than silently dropping the verdict. The sentKeys check below + // still rejects any id that wasn't actually sent (hallucination guard). + const id = + typeof rawId === "number" + ? rawId + : typeof rawId === "string" && /^\d+$/.test(rawId.trim()) + ? parseInt(rawId, 10) + : NaN; + if (!Number.isInteger(id)) continue; + if (!sentKeys.has(`${type}:${id}`)) continue; + if (typeof affected !== "boolean") continue; + if (typeof why !== "string" || why.trim() === "") continue; + out.push({ type: type as EntityType, id, affected, why: why.trim() }); + } + return out; +} + +const EMPTY_BY_TYPE = (): Record => ({ + system: [], + control: [], + policy: [], + vendor: [], + assessment: [], +}); + +export async function getCandidates( + organizationId: number, + countryName: string, + regulation: { type?: string; country?: string }, +): Promise> { + const region = regionForCountry(countryName); + const frameworks = frameworksForRegulation({ type: regulation.type, country: countryName }); + const out = EMPTY_BY_TYPE(); + + // --- systems (projects): geography region match OR framework match via projects_frameworks --- + const systems = (await sequelize.query( + `SELECT DISTINCT p.id, p.project_title AS name, + COALESCE(p.goal, '') AS description + FROM projects p + LEFT JOIN projects_frameworks pf ON pf.project_id = p.id + LEFT JOIN frameworks f ON f.id = pf.framework_id + WHERE p.organization_id = :organizationId + AND ( (:region IS NOT NULL AND p.geography = :region) + OR f.name IN (:frameworks) )`, + { + replacements: { + organizationId, + region, + frameworks: frameworks.length ? frameworks : ["__none__"], + }, + type: QueryTypes.SELECT, + }, + )) as { id: number; name: string; description: string }[]; + out.system = systems.map((r) => ({ + type: "system", + id: r.id, + name: r.name, + description: r.description, + })); + + const candidateProjectIds = systems.map((s) => s.id); + + // --- controls: belong to a project whose framework matches (3-hop) --- + const controls = (await sequelize.query( + `SELECT DISTINCT c.id, c.title AS name, COALESCE(c.description, '') AS description + FROM controls c + JOIN control_categories cc ON cc.id = c.control_category_id + JOIN projects_frameworks pf ON pf.project_id = cc.project_id + JOIN frameworks f ON f.id = pf.framework_id + JOIN projects p ON p.id = cc.project_id + WHERE p.organization_id = :organizationId + AND f.name IN (:frameworks)`, + { + replacements: { organizationId, frameworks: frameworks.length ? frameworks : ["__none__"] }, + type: QueryTypes.SELECT, + }, + )) as { id: number; name: string; description: string }[]; + out.control = controls.map((r) => ({ + type: "control", + id: r.id, + name: r.name, + description: r.description, + })); + + // --- assessments: project_id in candidate projects --- + if (candidateProjectIds.length) { + const assessments = (await sequelize.query( + `SELECT a.id, COALESCE(p.project_title, 'Assessment') AS name, '' AS description + FROM assessments a + JOIN projects p ON p.id = a.project_id + WHERE p.organization_id = :organizationId + AND a.project_id IN (:projectIds)`, + { + replacements: { organizationId, projectIds: candidateProjectIds }, + type: QueryTypes.SELECT, + }, + )) as { id: number; name: string; description: string }[]; + out.assessment = assessments.map((r) => ({ + type: "assessment", + id: r.id, + name: r.name, + description: r.description, + })); + } + + // --- vendors: regulatory_exposure maps to framework OR linked to a candidate project --- + const vendors = (await sequelize.query( + `SELECT DISTINCT v.id, v.vendor_name AS name, COALESCE(v.vendor_provides, '') AS description + FROM vendors v + LEFT JOIN vendors_projects vp ON vp.vendor_id = v.id + WHERE v.organization_id = :organizationId + AND ( v.regulatory_exposure IN (:frameworkExposure) + OR (:hasProjects AND vp.project_id IN (:projectIds)) )`, + { + replacements: { + organizationId, + frameworkExposure: mapFrameworksToExposure(frameworks), + hasProjects: candidateProjectIds.length > 0, + projectIds: candidateProjectIds.length > 0 ? candidateProjectIds : [-1], + }, + type: QueryTypes.SELECT, + }, + )) as { id: number; name: string; description: string }[]; + out.vendor = vendors.map((r) => ({ + type: "vendor", + id: r.id, + name: r.name, + description: r.description, + })); + + // --- policies: linked to a candidate control via policy_linked_objects --- + const controlIds = controls.map((c) => c.id); + if (controlIds.length) { + const policies = (await sequelize.query( + `SELECT DISTINCT pm.id, pm.title AS name, '' AS description + FROM policy_manager pm + JOIN policy_linked_objects plo ON plo.policy_id = pm.id + WHERE pm.organization_id = :organizationId + AND plo.object_type = 'control' + AND plo.object_id IN (:controlIds)`, + { replacements: { organizationId, controlIds }, type: QueryTypes.SELECT }, + )) as { id: number; name: string; description: string }[]; + out.policy = policies.map((r) => ({ + type: "policy", + id: r.id, + name: r.name, + description: r.description, + })); + } + + return out; +} + +// Maps regulation framework names to the vendor regulatory_exposure enum values. +// NOTE: ISO 42001 and NIST AI RMF are intentionally NOT mapped here — the +// vendor.regulatory_exposure enum (vendor.model.ts) only includes: +// "GDPR (EU)", "HIPAA (US)", "SOC 2", "ISO 27001", "EU AI act", "CCPA (california)" +// There is no "ISO 42001" or "NIST AI RMF" exposure value, so mapping them +// would be incorrect. Vendors relevant to those frameworks are found via the +// project-link path, not the regulatory_exposure column. +// Returns ["__none__"] when no mapping exists so the caller's IN-clause never +// matches real data (safe sentinel, not a bug). +function mapFrameworksToExposure(frameworks: string[]): string[] { + const m: Record = { + "EU AI Act": "EU AI act", + "ISO 27001": "ISO 27001", + }; + const mapped = frameworks.map((f) => m[f]).filter(Boolean); + return mapped.length ? mapped : ["__none__"]; +} + +// ─── Stage B: prompt assembly + per-type LLM call ─────────────────────────── + +export interface RegulationContext { + name: string; + type: string; + status: string; + country: string; + obligations: string[]; + maxPenalty: string; + changeLines: string[]; +} + +export interface LlmCreds { + apiKey: string; + baseURL: string; + model: string; + provider: "Anthropic" | "OpenAI" | "OpenRouter" | "Custom"; +} + +const TYPE_NOUN: Record = { + system: "AI systems", + control: "controls", + policy: "policies", + vendor: "vendors", + assessment: "assessments", +}; + +function systemPrompt(noun: string): string { + return [ + `You are a compliance analyst assessing how a specific change to an AI regulation affects a list of an organisation's ${noun}.`, + `You will be given: the regulation's identity and country, the specific change that just occurred (not the whole regulation), and a numbered list of candidate entities, each with a type, id, name and description.`, + `For each candidate, decide whether this specific change plausibly creates new or altered obligations for that entity.`, + `Rules you must follow:`, + `1. Judge the change, not the regulation in general. An entity is "affected" only if the described change alters what the organisation must do about it.`, + `2. Be conservative — when unsure, mark not affected. A false "affected" wastes the team's time and erodes trust.`, + `3. Use only the information given. Do not assume facts about an entity beyond its description. Do not infer geography, sector or framework that isn't stated.`, + `4. Only reason about entities in the provided list. Never introduce an entity, id or name that was not given to you.`, + `5. For each affected entity, give one sentence stating the concrete reason, citing the specific obligation or change. No generic statements.`, + `6. If a candidate is not affected, still return it with affected:false and a short reason.`, + `Return ONLY valid JSON of the form {"results":[{"type":"...","id":N,"affected":true|false,"why":"..."}]}. No prose outside the JSON.`, + ].join("\n"); +} + +export const SYSTEM_PROMPTS: Record = { + system: systemPrompt(TYPE_NOUN.system), + control: systemPrompt(TYPE_NOUN.control), + policy: systemPrompt(TYPE_NOUN.policy), + vendor: systemPrompt(TYPE_NOUN.vendor), + assessment: systemPrompt(TYPE_NOUN.assessment), +}; + +export function buildUserPrompt( + _type: EntityType, + ctx: RegulationContext, + candidates: Candidate[], +): string { + const change = ctx.changeLines.length + ? ctx.changeLines.map((l) => `- ${l}`).join("\n") + : "- (no structured diff available)"; + const cands = candidates + .map((c) => `[${c.type}] id=${c.id} "${c.name}" — ${c.description || "(no description)"}`) + .join("\n"); + return [ + `REGULATION: ${ctx.name} (${ctx.type}, ${ctx.status}) — ${ctx.country}`, + `THE CHANGE:\n${change}`, + `KEY OBLIGATIONS: ${ctx.obligations.join("; ") || "(none listed)"}`, + `MAX PENALTY: ${ctx.maxPenalty || "(not specified)"}`, + ``, + `CANDIDATE ENTITIES:\n${cands}`, + ].join("\n"); +} + +function parseJsonLoose(text: string): unknown { + const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i); + const body = fenced ? fenced[1] : text; + const start = body.indexOf("{"); + if (start === -1) throw new Error("no JSON object in response"); + // Scan brace depth to find the matching close of the FIRST top-level object, + // rather than lastIndexOf("}"). The latter mis-bounds when the model appends + // prose containing a "}" after the JSON (e.g. "... the {control 7} item."). + let depth = 0; + let end = -1; + for (let i = start; i < body.length; i++) { + const ch = body[i]; + if (ch === "{") depth++; + else if (ch === "}") { + depth--; + if (depth === 0) { + end = i; + break; + } + } + } + if (end === -1) throw new Error("unterminated JSON object in response"); + return JSON.parse(body.slice(start, end + 1)); +} + +export type AnalyzeTypeResult = { ok: true; verdicts: LlmVerdict[] } | { ok: false }; + +export async function analyzeType( + type: EntityType, + ctx: RegulationContext, + candidates: Candidate[], + creds: LlmCreds, + tenant: number, +): Promise { + try { + const text = await runAdvisorAiSdk({ + apiKey: creds.apiKey, + baseURL: creds.baseURL, + model: creds.model, + provider: creds.provider, + tenant, + userPrompt: `${SYSTEM_PROMPTS[type]}\n\n${buildUserPrompt(type, ctx, candidates)}`, + availableTools: {}, + toolsDefinition: [], + enableToolSubsetting: false, + } as any); + return { ok: true, verdicts: validateVerdicts(parseJsonLoose(text), candidates) }; + } catch (err) { + const error = err instanceof Error ? err : new Error(String(err)); + await logFailure({ + eventType: "Error", + description: `impact analysis ${type} call failed: ${error.message}`, + functionName: "analyzeType", + fileName: "regulationImpact.utils.ts", + error, + userId: 0, // background job — no user context + }); + return { ok: false }; + } +} + +// ─── Persistence types ──────────────────────────────────────────────────────── + +export interface AffectedEntity { + id: number; + name: string; + why: string; +} +export interface ImpactResult { + systems: AffectedEntity[]; + controls: AffectedEntity[]; + policies: AffectedEntity[]; + vendors: AffectedEntity[]; + assessments: AffectedEntity[]; + generatedAt: string; +} + +const RESULT_KEY: Record> = { + system: "systems", + control: "controls", + policy: "policies", + vendor: "vendors", + assessment: "assessments", +}; + +// ─── Persistence helpers ────────────────────────────────────────────────────── + +export async function getImpactRow(organizationId: number, slug: string) { + const normalizedSlug = normalizeSlug(slug); + const rows = (await sequelize.query( + `SELECT regulation_hash, status, result, refreshed_at + FROM regulation_impact_analysis + WHERE organization_id = :organizationId AND country_slug = :slug + LIMIT 1`, + { replacements: { organizationId, slug: normalizedSlug }, type: QueryTypes.SELECT }, + )) as { + regulation_hash: string; + status: string; + result: ImpactResult | null; + refreshed_at: string; + }[]; + return rows[0] ?? null; +} + +async function upsertImpactRow( + organizationId: number, + slug: string, + hash: string, + status: string, + result: ImpactResult | null, + model: string | null, +) { + await sequelize.query( + `INSERT INTO regulation_impact_analysis + (organization_id, country_slug, regulation_hash, status, result, model, refreshed_at) + VALUES (:organizationId, :slug, :hash, :status, :result::jsonb, :model, NOW()) + ON CONFLICT (organization_id, country_slug) DO UPDATE + SET regulation_hash = EXCLUDED.regulation_hash, + status = EXCLUDED.status, + result = EXCLUDED.result, + model = EXCLUDED.model, + refreshed_at = NOW()`, + { + replacements: { + organizationId, + slug, + hash, + status, + model, + result: result ? JSON.stringify(result) : null, + }, + }, + ); +} + +export function buildContext(slug: string, data: any): RegulationContext { + const regs = Array.isArray(data?.regulations) ? data.regulations : []; + const first = regs[0] ?? {}; + + // Build the change lines from the feed's structured diff. The field names + // must match the feed's RegulationChange shape exactly (regulation.status / + // regulation.effectiveDate / regulation / regulationCount); the status and + // effective-date variants carry the specific regulation's name, which we both + // surface in the line and use to scope obligations below. + const changeLines: string[] = []; + const changedRegNames = new Set(); + const history = data?.history ?? null; + if (Array.isArray(history?.lastChange?.changes)) { + for (const ch of history.lastChange.changes) { + if (ch.field === "regulation.status") { + changeLines.push(`${ch.regulation}: status ${ch.from} → ${ch.to}`); + if (ch.regulation) changedRegNames.add(ch.regulation); + } else if (ch.field === "regulation.effectiveDate") { + changeLines.push(`${ch.regulation}: effective date ${ch.from} → ${ch.to}`); + if (ch.regulation) changedRegNames.add(ch.regulation); + } else if (ch.field === "regulation") { + changeLines.push(`regulation ${ch.change}: ${ch.value}`); + if (ch.value) changedRegNames.add(ch.value); + } else if (ch.field === "regulationCount") { + changeLines.push(`regulation count ${ch.from} → ${ch.to}`); + } + } + } + + // Scope obligations to the regulation(s) that actually changed, so the prompt + // foregrounds the new/altered duties instead of drowning them in the whole + // country's regulatory baseline. Match a changed regulation name against a + // regs[] entry's name (a "regulation: added" value may be free text that + // doesn't exactly match, so matching is best-effort). If nothing matches — + // e.g. only a regulationCount change, or an added regulation we can't line up + // — fall back to all obligations rather than sending the LLM none. + const matchedRegs = changedRegNames.size + ? regs.filter((r: any) => typeof r?.name === "string" && changedRegNames.has(r.name)) + : []; + const obligationSource = matchedRegs.length ? matchedRegs : regs; + const obligations: string[] = []; + for (const r of obligationSource) + if (Array.isArray(r.obligations)) obligations.push(...r.obligations); + + return { + name: data?.name ?? slug, + type: first.type ?? "", + status: first.status ?? "", + country: data?.name ?? "", + obligations, + maxPenalty: first.maxPenalty ?? "", + changeLines, + }; +} + +// ─── Orchestrator ───────────────────────────────────────────────────────────── + +export async function runImpactAnalysis( + organizationId: number, + slug: string, + force = false, +): Promise<{ + status: string; + result: ImpactResult | null; + counts: Record; + cached: boolean; +}> { + const zeroCounts = (): Record => ({ + system: 0, + control: 0, + policy: 0, + vendor: 0, + assessment: 0, + }); + + // BUG 3: Normalize slug at the top so reads and writes always agree. + const normalizedSlug = normalizeSlug(slug); + + // load the global catalog row + const regRows = (await sequelize.query( + `SELECT data, hash FROM regulation_countries WHERE slug = :slug LIMIT 1`, + { replacements: { slug: normalizedSlug }, type: QueryTypes.SELECT }, + )) as { data: any; hash: string }[]; + if (!regRows.length) + return { status: "error", result: null, counts: zeroCounts(), cached: false }; + const { data, hash } = regRows[0]; + + // key gate + const keys = await getLLMKeysWithKeyQuery(organizationId); + if (!keys.length) return { status: "no_key", result: null, counts: zeroCounts(), cached: false }; + const k = keys[0]; + const creds: LlmCreds = { + apiKey: k.key, + baseURL: k.url || getLLMProviderUrl(k.name), + model: k.model, + provider: k.name, + }; + + // BUG 2: cache check — skipped when force=true (admin forced re-analysis). + if (!force) { + const cachedRow = await getImpactRow(organizationId, normalizedSlug); + if (cachedRow && cachedRow.regulation_hash === hash && cachedRow.status === "ok") { + return { + status: "ok", + result: cachedRow.result, + counts: countsFromResult(cachedRow.result), + cached: true, + }; + } + } + + const ctx = buildContext(normalizedSlug, data); + const candidates = await getCandidates(organizationId, ctx.country, { + type: ctx.type, + country: ctx.country, + }); + + const nonEmpty = (Object.keys(candidates) as EntityType[]).filter( + (t) => candidates[t].length > 0, + ); + if (!nonEmpty.length) { + await upsertImpactRow( + organizationId, + normalizedSlug, + hash, + "skipped_no_candidates", + null, + null, + ); + return { status: "skipped_no_candidates", result: null, counts: zeroCounts(), cached: false }; + } + + const verdictsByType = await Promise.all( + nonEmpty.map((t) => + analyzeType(t, ctx, candidates[t], creds, organizationId).then((r) => [t, r] as const), + ), + ); + + // BUG 1: Only cache as "ok" if at least one type's LLM call actually succeeded. + // If every analyzeType returned { ok: false }, the result is all-empty due to + // LLM failures — cache as "error" rather than poisoning with a false "ok". + const allFailed = verdictsByType.every(([, r]) => !r.ok); + if (allFailed) { + await upsertImpactRow(organizationId, normalizedSlug, hash, "error", null, null); + return { status: "error", result: null, counts: zeroCounts(), cached: false }; + } + + const result: ImpactResult = { + systems: [], + controls: [], + policies: [], + vendors: [], + assessments: [], + generatedAt: new Date().toISOString(), + }; + for (const [t, r] of verdictsByType) { + if (!r.ok) continue; // skip failed types; partial success is acceptable + const byId = new Map(candidates[t].map((c) => [c.id, c.name])); + for (const v of r.verdicts) { + if (v.affected) + result[RESULT_KEY[t]].push({ id: v.id, name: byId.get(v.id) ?? String(v.id), why: v.why }); + } + } + await upsertImpactRow(organizationId, normalizedSlug, hash, "ok", result, creds.model); + return { status: "ok", result, counts: countsFromResult(result), cached: false }; +} + +function countsFromResult(result: ImpactResult | null): Record { + return { + system: result?.systems.length ?? 0, + control: result?.controls.length ?? 0, + policy: result?.policies.length ?? 0, + vendor: result?.vendors.length ?? 0, + assessment: result?.assessments.length ?? 0, + }; +} diff --git a/Servers/utils/regulationsTracker.utils.ts b/Servers/utils/regulationsTracker.utils.ts new file mode 100644 index 0000000000..9aead5cefa --- /dev/null +++ b/Servers/utils/regulationsTracker.utils.ts @@ -0,0 +1,698 @@ +import path from "path"; +import fs from "fs"; +import { QueryTypes } from "sequelize"; +import { sequelize } from "../database/db"; +import logger from "./logger/fileLogger"; +import { + IManifestCountry, + RegulationChange, +} from "../domain.layer/interfaces/i.regulationsTracker"; + +// --------------------------------------------------------------------------- +// Country flags +// --------------------------------------------------------------------------- +// The public manifest feed does NOT carry a per-country `flag` (only the +// per-country detail endpoint does, inconsistently). So every sync would +// otherwise store flag-less rows and the Tracked/Browse/Deadlines flags would +// vanish on the next sync. We treat the committed seed snapshot as the durable +// source of truth for flags (a static, presentation-only slug -> emoji map) and +// re-inject the flag into every row we store. Loaded once, lazily. +let _flagMap: Map | null = null; + +function flagBySlug(): Map { + if (_flagMap) return _flagMap; + const map = new Map(); + try { + const snapshotPath = path.join( + __dirname, + "../database/seeds/regulations-tracker-snapshot.json", + ); + const snapshot = JSON.parse(fs.readFileSync(snapshotPath, "utf8")); + for (const c of snapshot.countries ?? []) { + if (c?.slug && c?.flag) map.set(String(c.slug).trim().toLowerCase(), c.flag); + } + } catch (e) { + logger.warn(`[regulations-tracker] could not load flag map: ${(e as Error).message}`); + } + _flagMap = map; + return map; +} + +/** + * Returns `data` with a top-level `flag` guaranteed: keeps any flag the feed + * supplied, otherwise fills it from the static seed-snapshot map keyed by slug. + * Presentation-only — never part of the change-detection hash. + */ +export function withFlag(slug: string, data: any): any { + const existing = data && typeof data === "object" ? (data as Record).flag : null; + if (typeof existing === "string" && existing) return data; + const flag = flagBySlug().get(slug.trim().toLowerCase()); + if (!flag) return data; + return { ...(data && typeof data === "object" ? data : {}), flag }; +} + +export function renderChangeLine(c: RegulationChange): string { + switch (c.field) { + case "regulation.status": + return `${c.regulation}: status ${c.from} → ${c.to}`; + case "regulation.effectiveDate": + return `${c.regulation}: effective date ${c.from} → ${c.to}`; + case "regulation": + return c.change === "added" ? `Added: ${c.value}` : `Removed: ${c.value}`; + case "regulationCount": + return `Regulation count ${c.from} → ${c.to}`; + default: + return JSON.stringify(c); + } +} + +export function escapeHtml(s: string): string { + return String(s) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +// ISO-8601 week, e.g. "2026-W26". Matches the AI Trust Index week-idempotency key. +export function currentIsoWeek(date: Date): string { + const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); + const dayNum = d.getUTCDay() === 0 ? 7 : d.getUTCDay(); + d.setUTCDate(d.getUTCDate() + 4 - dayNum); + const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + const week = Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7); + return `${d.getUTCFullYear()}-W${String(week).padStart(2, "0")}`; +} + +// UTC calendar day, e.g. "2026-06-29". The regulations sync runs daily (every +// morning at 06:00 UTC) so customers tracking a country are alerted the day a +// change lands rather than waiting for the next Monday. We reuse the existing +// last_run_week VARCHAR(10) column to hold this day key (no migration); the +// 10-char width fits "YYYY-MM-DD" exactly. +export function currentIsoDay(date: Date): string { + return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())) + .toISOString() + .slice(0, 10); +} + +export function normalizeSlug(s: string): string { + return String(s).trim().toLowerCase(); +} + +export async function getMetaQuery(): Promise<{ + seeded_at: Date | null; + last_good_count: number | null; + last_run_week: string | null; + last_run_at: Date | null; + last_run_status: string | null; +}> { + const rows = (await sequelize.query( + `SELECT seeded_at, last_good_count, last_run_week, last_run_at, last_run_status + FROM regulation_tracker_meta WHERE id = 1;`, + { type: QueryTypes.SELECT }, + )) as any[]; + return ( + rows[0] ?? { + seeded_at: null, + last_good_count: null, + last_run_week: null, + last_run_at: null, + last_run_status: null, + } + ); +} + +// Clears the day-idempotency watermark so the very next sync run actually +// fetches + diffs (used by the admin "check for updates now" trigger). The +// watermark lives in the legacy-named last_run_week column (see currentIsoDay). +export async function clearLastRunDay(): Promise { + await sequelize.query(`UPDATE regulation_tracker_meta SET last_run_week = NULL WHERE id = 1;`); +} + +// Stamps the most recent sync attempt's time + outcome on the meta singleton. +// Called at every exit point of the daily sync job (skip / fail / success) so the +// app can surface freshness and failures. +export async function recordRunStatus(status: string): Promise { + await sequelize.query( + `UPDATE regulation_tracker_meta + SET last_run_at = NOW(), last_run_status = :status + WHERE id = 1;`, + { replacements: { status: status.slice(0, 120) } }, + ); +} + +// --------------------------------------------------------------------------- +// Cross-process sync lock +// --------------------------------------------------------------------------- + +// Arbitrary, stable 64-bit key identifying the regulations-tracker sync lock. +// Chosen once; must stay constant so every process contends on the same key. +const SYNC_ADVISORY_LOCK_KEY = 4216_2026; + +/** + * Try to acquire the cross-process regulations-tracker sync lock. + * + * Returns a release function on success, or `null` if another process/connection + * already holds it. Uses a Postgres session-level advisory lock pinned to a + * single pooled connection via a dedicated transaction, so acquire and release + * always run on the same backend (required for pg_advisory_unlock to match). + * + * Unlike a module-level boolean, this is effective across multiple worker + * processes / pods — the real concurrency boundary for a horizontally-scaled + * BullMQ deployment. + */ +export async function acquireSyncLock(): Promise<(() => Promise) | null> { + const tx = await sequelize.transaction(); + try { + const rows = (await sequelize.query(`SELECT pg_try_advisory_lock(:key) AS acquired`, { + replacements: { key: SYNC_ADVISORY_LOCK_KEY }, + type: QueryTypes.SELECT, + transaction: tx, + })) as { acquired: boolean }[]; + if (!rows[0]?.acquired) { + // Didn't get the lock — close the holding transaction and report contention. + await tx.commit(); + return null; + } + // Hold the lock on this transaction's connection. The returned release + // function unlocks on the SAME connection, then commits to free it back to + // the pool. Best-effort: never throws out of release. + return async () => { + try { + await sequelize.query(`SELECT pg_advisory_unlock(:key)`, { + replacements: { key: SYNC_ADVISORY_LOCK_KEY }, + type: QueryTypes.SELECT, + transaction: tx, + }); + } catch (e) { + logger.warn(`[regulations-tracker] advisory unlock failed: ${(e as Error).message}`); + } finally { + await tx.commit().catch(() => undefined); + } + }; + } catch (e) { + await tx.rollback().catch(() => undefined); + logger.warn(`[regulations-tracker] advisory lock acquire failed: ${(e as Error).message}`); + // Fail open: if the lock machinery itself errors, let the sync proceed + // rather than silently never running. The day-key guard still prevents + // duplicate same-day work in the common case. + return async () => undefined; + } +} + +export interface CountryChange { + slug: string; + name: string; + lines: string[]; + unstructured: boolean; + /** + * Number of distinct assessments recorded in the feed's hashHistory since our + * previously-stored hash. 1 = a single change since last check; >1 means the + * country changed multiple times between our runs and only the latest change's + * detail is available from the feed. + */ + changeCount: number; + /** ISO dates of those intervening assessments (newest first), for a timeline note. */ + changeDates: string[]; +} + +// Given the feed country's hashHistory and our previously-stored hash, count how +// many assessments are newer than the stored one and collect their dates. The +// feed only carries structured change detail for the latest change, so this lets +// us tell the user "changed N times since last check" with the dates, even +// though we can only show the most recent change's specifics. +export function countChangesSince( + history: IManifestCountry["history"], + storedHash: string | undefined, +): { count: number; dates: string[] } { + const hh = history?.hashHistory ?? []; + if (!hh.length) return { count: 1, dates: [] }; + // hashHistory is chronological (oldest first). Find the stored hash; everything + // after it is new. If not found (or no stored hash), treat the latest entry as + // the single change we're reporting. + const idx = storedHash ? hh.findIndex((h) => h.hash === storedHash) : -1; + const newer = idx >= 0 ? hh.slice(idx + 1) : hh.slice(-1); + const dates = newer + .map((h) => h.date) + .filter((d): d is string => !!d) + .reverse(); // newest first + return { count: Math.max(newer.length, 1), dates }; +} + +// --------------------------------------------------------------------------- +// Global feeds (changelog / deadlines / frameworks) cached on the meta singleton +// --------------------------------------------------------------------------- + +export type GlobalFeedColumn = "horizon" | "deadlines" | "frameworks"; + +// Returns the cached JSONB for one global feed column (parsed object/array), or +// null if never stored. Column name is from a fixed union — never user input — +// so the interpolation is safe. +export async function getGlobalFeed(column: GlobalFeedColumn): Promise { + const rows = (await sequelize.query( + `SELECT ${column} AS v FROM regulation_tracker_meta WHERE id = 1;`, + { type: QueryTypes.SELECT }, + )) as { v: unknown }[]; + return rows[0]?.v ?? null; +} + +// Persists the three global feed blobs on the meta singleton in one update. +// Pass undefined for a feed to leave it unchanged. +export async function setGlobalFeeds(feeds: { + horizon?: unknown; + deadlines?: unknown; + frameworks?: unknown; +}): Promise { + const sets: string[] = []; + const repl: Record = {}; + if (feeds.horizon !== undefined) { + sets.push("horizon = :horizon::jsonb"); + repl.horizon = JSON.stringify(feeds.horizon); + } + if (feeds.deadlines !== undefined) { + sets.push("deadlines = :deadlines::jsonb"); + repl.deadlines = JSON.stringify(feeds.deadlines); + } + if (feeds.frameworks !== undefined) { + sets.push("frameworks = :frameworks::jsonb"); + repl.frameworks = JSON.stringify(feeds.frameworks); + } + if (!sets.length) return; + await sequelize.query(`UPDATE regulation_tracker_meta SET ${sets.join(", ")} WHERE id = 1;`, { + replacements: repl, + }); +} + +// Returns a map of normalized slug -> stored hash for the given slugs. Used by +// the daily sync to decide which countries' full detail needs re-fetching +// (new or hash-changed) before upserting. +export async function getStoredHashes(slugs: string[]): Promise> { + if (!slugs.length) return new Map(); + const normalized = slugs.map(normalizeSlug); + const rows = (await sequelize.query( + `SELECT slug, hash FROM regulation_countries WHERE slug = ANY(ARRAY[:slugs]::varchar[]);`, + { replacements: { slugs: normalized }, type: QueryTypes.SELECT }, + )) as { slug: string; hash: string }[]; + return new Map(rows.map((r) => [r.slug, r.hash])); +} + +export async function upsertFeedTx( + countries: IManifestCountry[], + presentSlugs?: string[], + rawCount?: number, + // Optional full per-country detail (regulations/timeline/meta), keyed by + // normalized slug. When present for a slug, the row's `data` stores the full + // detail so the detail page renders complete content from our DB; otherwise it + // falls back to the manifest summary entry. Lets a fresh install / sync mirror + // the website's full data instead of summary-only. + detailBySlug?: Map, +): Promise<{ + changed: CountryChange[]; + newlyAdded: string[]; + newlyRemoved: string[]; + wasFirstSeed: boolean; +}> { + if (!countries.length) + return { changed: [], newlyAdded: [], newlyRemoved: [], wasFirstSeed: false }; + + const changed: CountryChange[] = []; + const newlyAdded: string[] = []; + const newlyRemoved: string[] = []; + let wasFirstSeed = false; + + await sequelize.transaction(async (transaction) => { + const metaRows = (await sequelize.query( + `SELECT seeded_at FROM regulation_tracker_meta WHERE id = 1 FOR UPDATE;`, + { type: QueryTypes.SELECT, transaction }, + )) as any[]; + wasFirstSeed = !metaRows[0]?.seeded_at; + + // Prefetch all existing slugs + hashes in a single query to avoid N+1 SELECTs. + const normalizedSlugs = countries.map((c) => normalizeSlug(c.slug)); + const prefetchRows = (await sequelize.query( + `SELECT slug, hash FROM regulation_countries WHERE slug = ANY(ARRAY[:slugs]::varchar[]);`, + { replacements: { slugs: normalizedSlugs }, type: QueryTypes.SELECT, transaction }, + )) as { slug: string; hash: string }[]; + const existingMap = new Map(prefetchRows.map((r) => [r.slug, r.hash])); + + const upsertedSlugs: string[] = []; + for (const c of countries) { + const slug = normalizeSlug(c.slug); + upsertedSlugs.push(slug); + const existingHash = existingMap.get(slug); + // Prefer the full detail object (regulations/timeline/meta) when the caller + // supplied it; otherwise store the manifest summary entry. Always re-inject + // the flag (the feed doesn't carry one) so it survives every sync. + const storedData = withFlag(slug, detailBySlug?.get(slug) ?? c); + + if (existingHash !== undefined) { + const hashMoved = existingHash !== c.hash; + if (hashMoved) { + const lc = c.history?.lastChange ?? null; + const lines = (lc?.changes ?? []).map(renderChangeLine); + const { count, dates } = countChangesSince(c.history, existingHash); + changed.push({ + slug, + name: c.name, + lines: lines.length ? lines : ["Updated — see source"], + unstructured: lines.length === 0, + changeCount: count, + changeDates: dates, + }); + } + await sequelize.query( + `UPDATE regulation_countries SET + name = :name, region = :region, regulation_count = :rc, + data = :data::jsonb, hash = :hash, is_active = TRUE, removed_at = NULL, + last_fetched_at = NOW() ${hashMoved ? ", last_changed_at = NOW()" : ""} + WHERE slug = :slug;`, + { + replacements: { + slug, + name: c.name, + region: c.region ?? null, + rc: c.regulationCount ?? null, + data: JSON.stringify(storedData), + hash: c.hash, + }, + transaction, + }, + ); + } else { + newlyAdded.push(slug); + await sequelize.query( + `INSERT INTO regulation_countries + (slug, name, region, regulation_count, data, hash, is_active, last_changed_at, last_fetched_at) + VALUES (:slug, :name, :region, :rc, :data::jsonb, :hash, TRUE, NOW(), NOW());`, + { + replacements: { + slug, + name: c.name, + region: c.region ?? null, + rc: c.regulationCount ?? null, + data: JSON.stringify(storedData), + hash: c.hash, + }, + transaction, + }, + ); + } + } + + const seenSlugs = Array.from( + new Set([...upsertedSlugs, ...(presentSlugs ?? []).map(normalizeSlug)]), + ); + const removedRows = (await sequelize.query( + `UPDATE regulation_countries + SET is_active = FALSE, removed_at = NOW() + WHERE is_active = TRUE AND slug <> ALL(ARRAY[:seen]::varchar[]) + RETURNING slug;`, + { replacements: { seen: seenSlugs }, type: QueryTypes.SELECT, transaction }, + )) as any[]; + for (const r of removedRows) newlyRemoved.push(r.slug); + + await sequelize.query( + `UPDATE regulation_tracker_meta + SET last_good_count = :count, last_run_week = :week + ${wasFirstSeed ? ", seeded_at = NOW()" : ""} + WHERE id = 1;`, + { + replacements: { count: rawCount ?? countries.length, week: currentIsoDay(new Date()) }, + transaction, + }, + ); + }); + + logger.info( + `[regulationsTracker] upsertFeedTx complete: changed=${changed.length}, removed=${newlyRemoved.length}, firstSeed=${wasFirstSeed}`, + ); + + return { changed, newlyAdded, newlyRemoved, wasFirstSeed }; +} + +// --------------------------------------------------------------------------- +// CRUD: country catalogue +// --------------------------------------------------------------------------- + +export async function listCountries( + organizationId: number, + filters: { region?: string; q?: string } = {}, +) { + const where: string[] = ["c.is_active = TRUE"]; + const repl: Record = { organizationId }; + if (filters.region) { + where.push("c.region = :region"); + repl.region = filters.region; + } + if (filters.q) { + where.push("c.name ILIKE :q"); + repl.q = `%${filters.q}%`; + } + return sequelize.query( + `SELECT c.slug, c.name, c.region, c.regulation_count, c.hash, c.last_changed_at, + c.data->>'flag' AS flag, + (t.id IS NOT NULL) AS is_tracked + FROM regulation_countries c + LEFT JOIN regulation_tracked_countries t + ON t.country_slug = c.slug AND t.organization_id = :organizationId + WHERE ${where.join(" AND ")} ORDER BY c.name ASC;`, + { replacements: repl, type: QueryTypes.SELECT }, + ); +} + +export async function getCountryRow(slug: string, organizationId: number) { + const rows = (await sequelize.query( + `SELECT c.slug, c.name, c.region, c.regulation_count, c.data, c.hash, c.is_active, c.last_changed_at, + (t.id IS NOT NULL) AS is_tracked + FROM regulation_countries c + LEFT JOIN regulation_tracked_countries t + ON t.country_slug = c.slug AND t.organization_id = :organizationId + WHERE c.slug = :slug;`, + { replacements: { slug: normalizeSlug(slug), organizationId }, type: QueryTypes.SELECT }, + )) as any[]; + return rows[0] ?? null; +} + +// --------------------------------------------------------------------------- +// CRUD: tracked countries (per-org) +// --------------------------------------------------------------------------- + +export async function listTracked(organizationId: number) { + return sequelize.query( + `SELECT t.country_slug, t.country_slug AS slug, t.created_at, + c.name, c.region, c.regulation_count, c.is_active, c.last_changed_at, + c.data->>'flag' AS flag + FROM regulation_tracked_countries t + LEFT JOIN regulation_countries c ON c.slug = t.country_slug + WHERE t.organization_id = :organizationId ORDER BY c.name ASC;`, + { replacements: { organizationId }, type: QueryTypes.SELECT }, + ); +} + +export async function trackCountry(organizationId: number, slug: string, userId: number) { + await sequelize.query( + `INSERT INTO regulation_tracked_countries (organization_id, country_slug, tracked_by, created_at) + VALUES (:organizationId, :slug, :userId, NOW()) + ON CONFLICT (organization_id, country_slug) DO NOTHING;`, + { replacements: { organizationId, slug: normalizeSlug(slug), userId } }, + ); + return { tracked: true }; +} + +export async function trackCountriesBulk(organizationId: number, slugs: string[], userId: number) { + for (const s of slugs) await trackCountry(organizationId, s, userId); + return { tracked: slugs.length }; +} + +export async function untrackCountry(organizationId: number, slug: string) { + await sequelize.query( + `DELETE FROM regulation_tracked_countries + WHERE organization_id = :organizationId AND country_slug = :slug;`, + { replacements: { organizationId, slug: normalizeSlug(slug) } }, + ); + return { untracked: true }; +} + +// --------------------------------------------------------------------------- +// CRUD: notification settings (per-org) +// --------------------------------------------------------------------------- + +export async function getSettings(organizationId: number) { + const rows = (await sequelize.query( + `SELECT recipient_user_ids, recipient_emails, updated_by, updated_at, + impact_enabled, last_impact_run_at + FROM regulation_tracker_settings WHERE organization_id = :organizationId;`, + { replacements: { organizationId }, type: QueryTypes.SELECT }, + )) as { + recipient_user_ids: number[] | null; + recipient_emails: string[] | null; + updated_by: number | null; + updated_at: Date | null; + impact_enabled: boolean | null; + last_impact_run_at: Date | null; + }[]; + return ( + rows[0] ?? { + recipient_user_ids: [], + recipient_emails: [], + updated_by: null, + updated_at: null, + impact_enabled: true, + last_impact_run_at: null, + } + ); +} + +export async function upsertSettings( + organizationId: number, + userIds: number[], + emails: string[], + userId: number, + impactEnabled?: boolean, +) { + await sequelize.query( + `INSERT INTO regulation_tracker_settings + (organization_id, recipient_user_ids, recipient_emails, updated_by, updated_at, impact_enabled) + VALUES (:organizationId, :userIds::jsonb, :emails::jsonb, :userId, NOW(), COALESCE(:impactEnabled, true)) + ON CONFLICT (organization_id) DO UPDATE SET + recipient_user_ids = :userIds::jsonb, recipient_emails = :emails::jsonb, + updated_by = :userId, updated_at = NOW(), + impact_enabled = COALESCE(:impactEnabled, regulation_tracker_settings.impact_enabled);`, + { + replacements: { + organizationId, + userId, + userIds: JSON.stringify(userIds ?? []), + emails: JSON.stringify(emails ?? []), + impactEnabled: impactEnabled === undefined ? null : impactEnabled, + }, + }, + ); + return getSettings(organizationId); +} + +export async function setLastImpactRunAt(organizationId: number): Promise { + await sequelize.query( + `INSERT INTO regulation_tracker_settings (organization_id, last_impact_run_at, updated_at) + VALUES (:organizationId, NOW(), NOW()) + ON CONFLICT (organization_id) DO UPDATE SET last_impact_run_at = NOW();`, + { replacements: { organizationId } }, + ); +} + +// --------------------------------------------------------------------------- +// Query: orgs tracking any of the given slugs (used by the daily sync job) +// --------------------------------------------------------------------------- + +export async function getAffectedOrgsBySlugs( + slugs: string[], +): Promise<{ organization_id: number; country_slug: string; name: string | null }[]> { + if (!slugs.length) return []; + // NOTE: intentionally NO `c.is_active = TRUE` filter. This is called with both + // CHANGED and newly-REMOVED slugs; a removed country has is_active = FALSE but + // its org still needs the "removed from the feed" notification. The LEFT JOIN + // also tolerates a truly-orphaned tracking row (country gone from the catalog + // entirely) by returning name = NULL, which the caller resolves to the slug. + return (await sequelize.query( + `SELECT DISTINCT t.organization_id, t.country_slug, c.name + FROM regulation_tracked_countries t + LEFT JOIN regulation_countries c ON c.slug = t.country_slug + WHERE t.country_slug = ANY(ARRAY[:slugs]::varchar[]);`, + { replacements: { slugs }, type: QueryTypes.SELECT }, + )) as { organization_id: number; country_slug: string; name: string | null }[]; +} + +// --------------------------------------------------------------------------- +// Recipient resolution +// --------------------------------------------------------------------------- + +// EMAIL recipients: configured only, NO admin fallback (matches AI Trust Index pattern). +export async function resolveEmailRecipients(organizationId: number): Promise { + const s = await getSettings(organizationId); + const userIds: number[] = s.recipient_user_ids ?? []; + const freeText: string[] = s.recipient_emails ?? []; + let userEmails: string[] = []; + if (userIds.length) { + const rows = (await sequelize.query( + `SELECT email FROM users WHERE organization_id = :organizationId AND id = ANY(ARRAY[:ids]::int[]);`, + { replacements: { organizationId, ids: userIds }, type: QueryTypes.SELECT }, + )) as { email: string }[]; + userEmails = rows.map((r) => r.email); + } + const recipients = Array.from( + new Set([...userEmails, ...freeText].map((e) => e.trim().toLowerCase()).filter(Boolean)), + ); + if (!recipients.length) { + logger.info( + `[regulations-tracker] org ${organizationId} changed but no email recipients configured; skipped`, + ); + } + return recipients; +} + +// IN-APP recipients: org Admins ∪ configured recipient_user_ids (deduped). +// Uses JOIN roles r ON r.id = u.role_id — confirmed pattern from invitation.utils.ts / user.utils.ts. +export async function resolveInAppUserIds(organizationId: number): Promise { + const s = await getSettings(organizationId); + const configured: number[] = s.recipient_user_ids ?? []; + const admins = (await sequelize.query( + `SELECT u.id FROM users u + JOIN roles r ON r.id = u.role_id + WHERE u.organization_id = :organizationId AND r.name IN ('Admin', 'SuperAdmin');`, + { replacements: { organizationId }, type: QueryTypes.SELECT }, + )) as { id: number }[]; + return Array.from(new Set([...admins.map((a) => a.id), ...configured])); +} + +// Every (org, admin user) pair across all organizations. Used to notify admins +// when brand-new countries appear in the feed — those aren't tracked by anyone +// yet, so the alert is org-agnostic (goes to each org's admins). +export async function getAllOrgAdmins(): Promise<{ organization_id: number; user_id: number }[]> { + return (await sequelize.query( + `SELECT u.organization_id, u.id AS user_id + FROM users u + JOIN roles r ON r.id = u.role_id + WHERE r.name IN ('Admin', 'SuperAdmin');`, + { type: QueryTypes.SELECT }, + )) as { organization_id: number; user_id: number }[]; +} + +// --------------------------------------------------------------------------- +// Deadline flag enrichment +// --------------------------------------------------------------------------- + +/** + * Enriches a deadlines/unscheduled array with `countryFlag` fetched from the + * regulation_countries catalog. Uses a single batched query for all distinct + * slugs. Best-effort: never throws — if the query fails the original items are + * returned unchanged. + * + * Moved here from the controller per the thin-controller convention (raw SQL + * belongs in utils, not controllers). + */ +export async function enrichWithFlags(items: unknown[]): Promise { + if (!items.length) return items; + try { + const slugs = [ + ...new Set( + items + .map((i) => (i as Record).countrySlug) + .filter((s) => typeof s === "string"), + ), + ] as string[]; + if (!slugs.length) return items; + const rows = (await sequelize.query( + `SELECT slug, data->>'flag' AS flag FROM regulation_countries WHERE slug IN (:slugs)`, + { replacements: { slugs }, type: QueryTypes.SELECT }, + )) as { slug: string; flag: string | null }[]; + const flagMap = new Map(rows.map((r) => [r.slug, r.flag ?? undefined])); + return items.map((item) => { + const it = item as Record; + const flag = flagMap.get(it.countrySlug as string); + return flag !== undefined ? { ...it, countryFlag: flag } : it; + }); + } catch { + return items; + } +} diff --git a/Servers/utils/regulationsTrackerFeed.ts b/Servers/utils/regulationsTrackerFeed.ts new file mode 100644 index 0000000000..5f9737eb16 --- /dev/null +++ b/Servers/utils/regulationsTrackerFeed.ts @@ -0,0 +1,123 @@ +import axios from "axios"; +import { IManifestCountry } from "../domain.layer/interfaces/i.regulationsTracker"; + +export const FEED_ORIGIN = "https://verifywise.ai"; +export const MANIFEST_URL = `${FEED_ORIGIN}/api/regulations`; +export const EXPECTED_FEED_VERSION = 1; +export const ABSOLUTE_FLOOR = 20; + +const REQUIRED_KEYS: (keyof IManifestCountry)[] = ["slug", "name", "region", "hash"]; + +function hasRequired(c: any): c is IManifestCountry { + return ( + c && typeof c === "object" && REQUIRED_KEYS.every((k) => c[k] !== undefined && c[k] !== null) + ); +} + +function normalizeSlug(s: string): string { + return String(s).trim().toLowerCase(); +} + +export type ValidateResult = + | { + ok: true; + countries: IManifestCountry[]; + presentSlugs: string[]; + rawCount: number; + generatedAt: string; + } + | { ok: false; reason: string }; + +export function validateManifest(raw: unknown, lastGoodCount: number | null): ValidateResult { + if (!raw || typeof raw !== "object") return { ok: false, reason: "feed is not an object" }; + const f = raw as Record; + if (f.feedVersion !== EXPECTED_FEED_VERSION) + return { ok: false, reason: `unsupported feedVersion ${String(f.feedVersion)}` }; + if (!Array.isArray(f.countries)) return { ok: false, reason: "countries is not an array" }; + const counts = (f.counts as Record) ?? {}; + if (typeof counts.countries === "number" && counts.countries !== f.countries.length) + return { + ok: false, + reason: `counts.countries (${counts.countries}) != length (${f.countries.length})`, + }; + + // Filter to valid entries first, then gate on the VALID count so a feed with many + // malformed entries doesn't pass the floor/50%-drop guards while silently losing data. + const countries = (f.countries as unknown[]).filter(hasRequired) as IManifestCountry[]; + const validCount = countries.length; + + if (validCount < ABSOLUTE_FLOOR) + return { ok: false, reason: `below absolute floor (${validCount} valid < ${ABSOLUTE_FLOOR})` }; + if (lastGoodCount != null && validCount < lastGoodCount * 0.5) + return { + ok: false, + reason: `below 50% of last good count (${validCount} valid < ${lastGoodCount})`, + }; + + const presentSlugs = (f.countries as unknown[]) + .map((c) => + c && typeof c === "object" && typeof (c as Record).slug === "string" + ? normalizeSlug((c as Record).slug as string) + : null, + ) + .filter((s): s is string => !!s); + return { + ok: true, + countries, + presentSlugs, + rawCount: f.countries.length, + generatedAt: typeof f.generatedAt === "string" ? f.generatedAt : new Date().toISOString(), + }; +} + +export async function fetchManifest(deps?: { + get?: (url: string) => Promise<{ status: number; data: unknown }>; +}): Promise { + const get = deps?.get ?? ((url: string) => axios.get(url, { timeout: 20000 })); + const res = await get(MANIFEST_URL); + if (res.status !== 200) throw new Error(`manifest HTTP ${res.status}`); + return res.data; +} + +export async function fetchCountryDetail( + slug: string, + deps?: { get?: (url: string) => Promise<{ status: number; data: unknown }> }, +): Promise { + const get = deps?.get ?? ((url: string) => axios.get(url, { timeout: 10000 })); + const res = await get(`${FEED_ORIGIN}/api/regulations/country/${encodeURIComponent(slug)}`); + if (res.status !== 200) throw new Error(`country detail HTTP ${res.status}`); + return res.data; +} + +// --------------------------------------------------------------------------- +// Global, non-tenant feeds (changelog / deadlines / international frameworks). +// Each returns the raw feed object; callers extract the array(s) they need. +// --------------------------------------------------------------------------- + +async function fetchJson( + url: string, + deps?: { get?: (url: string) => Promise<{ status: number; data: unknown }> }, +): Promise { + const get = deps?.get ?? ((u: string) => axios.get(u, { timeout: 15000 })); + const res = await get(url); + if (res.status !== 200) throw new Error(`feed HTTP ${res.status} for ${url}`); + return res.data; +} + +export function fetchHorizon(deps?: { + get?: (url: string) => Promise<{ status: number; data: unknown }>; +}): Promise { + return fetchJson(`${FEED_ORIGIN}/api/regulations/horizon`, deps); +} + +export function fetchDeadlines(deps?: { + get?: (url: string) => Promise<{ status: number; data: unknown }>; +}): Promise { + return fetchJson(`${FEED_ORIGIN}/api/regulations/deadlines`, deps); +} + +export function fetchSnapshot(deps?: { + get?: (url: string) => Promise<{ status: number; data: unknown }>; +}): Promise { + return fetchJson(`${FEED_ORIGIN}/api/regulations/snapshot`, deps); +} diff --git a/docs/api-docs/src/config/endpoints.ts b/docs/api-docs/src/config/endpoints.ts index 40d348dd6c..3bd668fd3e 100644 --- a/docs/api-docs/src/config/endpoints.ts +++ b/docs/api-docs/src/config/endpoints.ts @@ -4,7 +4,7 @@ export interface Parameter { name: string; - in: "path" | "query" | "header"; + in: 'path' | 'query' | 'header'; type: string; required: boolean; description: string; @@ -16,7 +16,7 @@ export interface Response { } export interface Endpoint { - method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; + method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; path: string; summary: string; description?: string; @@ -30,8 +30,8 @@ export interface Endpoint { // Agent Discovery endpoints export const agentDiscoveryEndpoints: Endpoint[] = [ { - method: "GET", - path: "/agent-primitives", + method: 'GET', + path: '/agent-primitives', summary: "Get All Agent Primitives", requiresAuth: true, responses: [ @@ -42,8 +42,8 @@ export const agentDiscoveryEndpoints: Endpoint[] = [ tag: "Agent Discovery", }, { - method: "POST", - path: "/agent-primitives", + method: 'POST', + path: '/agent-primitives', summary: "Create Agent Primitive", requiresAuth: true, responses: [ @@ -54,8 +54,8 @@ export const agentDiscoveryEndpoints: Endpoint[] = [ tag: "Agent Discovery", }, { - method: "GET", - path: "/agent-primitives/stats", + method: 'GET', + path: '/agent-primitives/stats', summary: "Get Agent Stats", requiresAuth: true, responses: [ @@ -66,8 +66,8 @@ export const agentDiscoveryEndpoints: Endpoint[] = [ tag: "Agent Discovery", }, { - method: "GET", - path: "/agent-primitives/sync/logs", + method: 'GET', + path: '/agent-primitives/sync/logs', summary: "Get Sync Logs", requiresAuth: true, responses: [ @@ -78,8 +78,8 @@ export const agentDiscoveryEndpoints: Endpoint[] = [ tag: "Agent Discovery", }, { - method: "GET", - path: "/agent-primitives/sync/status", + method: 'GET', + path: '/agent-primitives/sync/status', summary: "Get Sync Status", requiresAuth: true, responses: [ @@ -90,18 +90,12 @@ export const agentDiscoveryEndpoints: Endpoint[] = [ tag: "Agent Discovery", }, { - method: "GET", - path: "/agent-primitives/{id}", + method: 'GET', + path: '/agent-primitives/{id}', summary: "Get Agent Primitive By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -111,18 +105,12 @@ export const agentDiscoveryEndpoints: Endpoint[] = [ tag: "Agent Discovery", }, { - method: "PATCH", - path: "/agent-primitives/{id}", + method: 'PATCH', + path: '/agent-primitives/{id}', summary: "Update Agent Primitive", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -132,18 +120,12 @@ export const agentDiscoveryEndpoints: Endpoint[] = [ tag: "Agent Discovery", }, { - method: "DELETE", - path: "/agent-primitives/{id}", + method: 'DELETE', + path: '/agent-primitives/{id}', summary: "Delete Agent Primitive By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -153,8 +135,8 @@ export const agentDiscoveryEndpoints: Endpoint[] = [ tag: "Agent Discovery", }, { - method: "POST", - path: "/agent-primitives/sync", + method: 'POST', + path: '/agent-primitives/sync', summary: "Trigger Sync", requiresAuth: true, responses: [ @@ -165,18 +147,12 @@ export const agentDiscoveryEndpoints: Endpoint[] = [ tag: "Agent Discovery", }, { - method: "PATCH", - path: "/agent-primitives/{id}/review", + method: 'PATCH', + path: '/agent-primitives/{id}/review', summary: "Review Agent Primitive", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -186,18 +162,12 @@ export const agentDiscoveryEndpoints: Endpoint[] = [ tag: "Agent Discovery", }, { - method: "PATCH", - path: "/agent-primitives/{id}/link-model", + method: 'PATCH', + path: '/agent-primitives/{id}/link-model', summary: "Link Model To Agent", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -207,18 +177,12 @@ export const agentDiscoveryEndpoints: Endpoint[] = [ tag: "Agent Discovery", }, { - method: "PATCH", - path: "/agent-primitives/{id}/unlink-model", + method: 'PATCH', + path: '/agent-primitives/{id}/unlink-model', summary: "Unlink Model From Agent", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -228,18 +192,12 @@ export const agentDiscoveryEndpoints: Endpoint[] = [ tag: "Agent Discovery", }, { - method: "GET", - path: "/agent-primitives/{id}/audit-logs", + method: 'GET', + path: '/agent-primitives/{id}/audit-logs', summary: "Get Agent Audit Logs", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -253,8 +211,8 @@ export const agentDiscoveryEndpoints: Endpoint[] = [ // AI Advisor endpoints export const aiAdvisorEndpoints: Endpoint[] = [ { - method: "POST", - path: "/advisor", + method: 'POST', + path: '/advisor', summary: "Run Advisor", requiresAuth: true, responses: [ @@ -265,8 +223,8 @@ export const aiAdvisorEndpoints: Endpoint[] = [ tag: "AI Advisor", }, { - method: "POST", - path: "/advisor/stream", + method: 'POST', + path: '/advisor/stream', summary: "Stream Advisor", requiresAuth: true, responses: [ @@ -277,8 +235,8 @@ export const aiAdvisorEndpoints: Endpoint[] = [ tag: "AI Advisor", }, { - method: "POST", - path: "/advisor/chat", + method: 'POST', + path: '/advisor/chat', summary: "Stream Advisor V2", requiresAuth: true, responses: [ @@ -289,20 +247,13 @@ export const aiAdvisorEndpoints: Endpoint[] = [ tag: "AI Advisor", }, { - method: "GET", - path: "/advisor/conversations/{domain}", + method: 'GET', + path: '/advisor/conversations/{domain}', summary: "List conversations for a domain", - description: - "Returns all conversations the current user has in the given advisor domain, most recent first. Lightweight summaries only — no message bodies.", + description: "Returns all conversations the current user has in the given advisor domain, most recent first. Lightweight summaries only — no message bodies.", requiresAuth: true, parameters: [ - { - name: "domain", - in: "path", - type: "string", - required: true, - description: "The domain", - }, + { name: 'domain', in: 'path', type: 'string', required: true, description: "The domain" }, ], responses: [ { status: 200, description: "Success" }, @@ -312,20 +263,13 @@ export const aiAdvisorEndpoints: Endpoint[] = [ tag: "AI Advisor", }, { - method: "POST", - path: "/advisor/conversations/{domain}", + method: 'POST', + path: '/advisor/conversations/{domain}', summary: "Create a new empty conversation", - description: - "Creates a fresh empty conversation in the given domain. Title is derived automatically when the first user message is saved.", + description: "Creates a fresh empty conversation in the given domain. Title is derived automatically when the first user message is saved.", requiresAuth: true, parameters: [ - { - name: "domain", - in: "path", - type: "string", - required: true, - description: "The domain", - }, + { name: 'domain', in: 'path', type: 'string', required: true, description: "The domain" }, ], responses: [ { status: 201, description: "Conversation created" }, @@ -335,26 +279,14 @@ export const aiAdvisorEndpoints: Endpoint[] = [ tag: "AI Advisor", }, { - method: "GET", - path: "/advisor/conversations/{domain}/{id}", + method: 'GET', + path: '/advisor/conversations/{domain}/{id}', summary: "Get a single conversation", description: "Returns the full conversation including its messages array.", requiresAuth: true, parameters: [ - { - name: "domain", - in: "path", - type: "string", - required: true, - description: "The domain", - }, - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'domain', in: 'path', type: 'string', required: true, description: "The domain" }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -365,30 +297,17 @@ export const aiAdvisorEndpoints: Endpoint[] = [ tag: "AI Advisor", }, { - method: "PUT", - path: "/advisor/conversations/{domain}/{id}", + method: 'PUT', + path: '/advisor/conversations/{domain}/{id}', summary: "Update conversation messages", - description: - "Replaces the messages array of an existing conversation. Bumps last_message_at and auto-derives the title on first save.", - requiresAuth: true, - parameters: [ - { - name: "domain", - in: "path", - type: "string", - required: true, - description: "The domain", - }, - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + description: "Replaces the messages array of an existing conversation. Bumps last_message_at and auto-derives the title on first save.", + requiresAuth: true, + parameters: [ + { name: 'domain', in: 'path', type: 'string', required: true, description: "The domain" }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], requestBody: { - messages: "array (optional)", + "messages": "array (optional)", }, responses: [ { status: 200, description: "Updated successfully" }, @@ -399,25 +318,13 @@ export const aiAdvisorEndpoints: Endpoint[] = [ tag: "AI Advisor", }, { - method: "DELETE", - path: "/advisor/conversations/{domain}/{id}", + method: 'DELETE', + path: '/advisor/conversations/{domain}/{id}', summary: "Delete a conversation", requiresAuth: true, parameters: [ - { - name: "domain", - in: "path", - type: "string", - required: true, - description: "The domain", - }, - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'domain', in: 'path', type: 'string', required: true, description: "The domain" }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 204, description: "Deleted" }, @@ -428,8 +335,8 @@ export const aiAdvisorEndpoints: Endpoint[] = [ tag: "AI Advisor", }, { - method: "GET", - path: "/advisor/memory", + method: 'GET', + path: '/advisor/memory', summary: "Get Memory Summary", requiresAuth: true, responses: [ @@ -439,8 +346,8 @@ export const aiAdvisorEndpoints: Endpoint[] = [ tag: "AI Advisor", }, { - method: "DELETE", - path: "/advisor/memory", + method: 'DELETE', + path: '/advisor/memory', summary: "Delete My Memory", requiresAuth: true, responses: [ @@ -450,8 +357,8 @@ export const aiAdvisorEndpoints: Endpoint[] = [ tag: "AI Advisor", }, { - method: "GET", - path: "/advisor/memory/admin/agent/{agentName}", + method: 'GET', + path: '/advisor/memory/admin/agent/{agentName}', summary: "Admin List Agent Messages", requiresAuth: true, responses: [ @@ -461,8 +368,8 @@ export const aiAdvisorEndpoints: Endpoint[] = [ tag: "AI Advisor", }, { - method: "DELETE", - path: "/advisor/memory/admin/agent/{agentName}", + method: 'DELETE', + path: '/advisor/memory/admin/agent/{agentName}', summary: "Admin Clear Agent Memory", requiresAuth: true, responses: [ @@ -476,8 +383,8 @@ export const aiAdvisorEndpoints: Endpoint[] = [ // AI Approval Rules endpoints export const aiApprovalRulesEndpoints: Endpoint[] = [ { - method: "POST", - path: "/ai-approval-rules/test", + method: 'POST', + path: '/ai-approval-rules/test', summary: "Test Rule Ctrl", requiresAuth: true, responses: [ @@ -487,8 +394,8 @@ export const aiApprovalRulesEndpoints: Endpoint[] = [ tag: "AI Approval Rules", }, { - method: "GET", - path: "/ai-approval-rules", + method: 'GET', + path: '/ai-approval-rules', summary: "List Rules Ctrl", requiresAuth: true, responses: [ @@ -498,8 +405,8 @@ export const aiApprovalRulesEndpoints: Endpoint[] = [ tag: "AI Approval Rules", }, { - method: "POST", - path: "/ai-approval-rules", + method: 'POST', + path: '/ai-approval-rules', summary: "Create Rule Ctrl", requiresAuth: true, responses: [ @@ -509,8 +416,8 @@ export const aiApprovalRulesEndpoints: Endpoint[] = [ tag: "AI Approval Rules", }, { - method: "PUT", - path: "/ai-approval-rules/{id}", + method: 'PUT', + path: '/ai-approval-rules/{id}', summary: "Update Rule Ctrl", requiresAuth: true, responses: [ @@ -520,8 +427,8 @@ export const aiApprovalRulesEndpoints: Endpoint[] = [ tag: "AI Approval Rules", }, { - method: "DELETE", - path: "/ai-approval-rules/{id}", + method: 'DELETE', + path: '/ai-approval-rules/{id}', summary: "Delete Rule Ctrl", requiresAuth: true, responses: [ @@ -535,8 +442,8 @@ export const aiApprovalRulesEndpoints: Endpoint[] = [ // AI Approvals endpoints export const aiApprovalsEndpoints: Endpoint[] = [ { - method: "GET", - path: "/ai-approvals/stats", + method: 'GET', + path: '/ai-approvals/stats', summary: "Get Approval Stats Ctrl", requiresAuth: true, responses: [ @@ -546,8 +453,8 @@ export const aiApprovalsEndpoints: Endpoint[] = [ tag: "AI Approvals", }, { - method: "GET", - path: "/ai-approvals", + method: 'GET', + path: '/ai-approvals', summary: "List Approvals Ctrl", requiresAuth: true, responses: [ @@ -557,8 +464,8 @@ export const aiApprovalsEndpoints: Endpoint[] = [ tag: "AI Approvals", }, { - method: "GET", - path: "/ai-approvals/{id}", + method: 'GET', + path: '/ai-approvals/{id}', summary: "Get Approval Detail Ctrl", requiresAuth: true, responses: [ @@ -568,8 +475,8 @@ export const aiApprovalsEndpoints: Endpoint[] = [ tag: "AI Approvals", }, { - method: "POST", - path: "/ai-approvals/{id}/approve", + method: 'POST', + path: '/ai-approvals/{id}/approve', summary: "Approve Approval Ctrl", requiresAuth: true, responses: [ @@ -579,8 +486,8 @@ export const aiApprovalsEndpoints: Endpoint[] = [ tag: "AI Approvals", }, { - method: "POST", - path: "/ai-approvals/{id}/reject", + method: 'POST', + path: '/ai-approvals/{id}/reject', summary: "Reject Approval Ctrl", requiresAuth: true, responses: [ @@ -594,8 +501,8 @@ export const aiApprovalsEndpoints: Endpoint[] = [ // AI Apps endpoints export const aiAppsEndpoints: Endpoint[] = [ { - method: "GET", - path: "/ai-apps", + method: 'GET', + path: '/ai-apps', summary: "Get All Ai Apps", requiresAuth: true, responses: [ @@ -605,8 +512,8 @@ export const aiAppsEndpoints: Endpoint[] = [ tag: "AI Apps", }, { - method: "POST", - path: "/ai-apps", + method: 'POST', + path: '/ai-apps', summary: "Create Ai App", requiresAuth: true, responses: [ @@ -616,8 +523,8 @@ export const aiAppsEndpoints: Endpoint[] = [ tag: "AI Apps", }, { - method: "GET", - path: "/ai-apps/policy-suggestions", + method: 'GET', + path: '/ai-apps/policy-suggestions', summary: "Get Policy Suggestions", requiresAuth: true, responses: [ @@ -627,8 +534,8 @@ export const aiAppsEndpoints: Endpoint[] = [ tag: "AI Apps", }, { - method: "GET", - path: "/ai-apps/{id}", + method: 'GET', + path: '/ai-apps/{id}', summary: "Get Ai App By Id", requiresAuth: true, responses: [ @@ -638,8 +545,8 @@ export const aiAppsEndpoints: Endpoint[] = [ tag: "AI Apps", }, { - method: "PATCH", - path: "/ai-apps/{id}", + method: 'PATCH', + path: '/ai-apps/{id}', summary: "Update Ai App By Id", requiresAuth: true, responses: [ @@ -649,8 +556,8 @@ export const aiAppsEndpoints: Endpoint[] = [ tag: "AI Apps", }, { - method: "DELETE", - path: "/ai-apps/{id}", + method: 'DELETE', + path: '/ai-apps/{id}', summary: "Delete Ai App By Id", requiresAuth: true, responses: [ @@ -660,8 +567,8 @@ export const aiAppsEndpoints: Endpoint[] = [ tag: "AI Apps", }, { - method: "POST", - path: "/ai-apps/{id}/models", + method: 'POST', + path: '/ai-apps/{id}/models', summary: "Link Models To Ai App", requiresAuth: true, responses: [ @@ -671,8 +578,8 @@ export const aiAppsEndpoints: Endpoint[] = [ tag: "AI Apps", }, { - method: "POST", - path: "/ai-apps/{id}/policies", + method: 'POST', + path: '/ai-apps/{id}/policies', summary: "Set Policies For Ai App", requiresAuth: true, responses: [ @@ -682,8 +589,8 @@ export const aiAppsEndpoints: Endpoint[] = [ tag: "AI Apps", }, { - method: "POST", - path: "/ai-apps/{id}/data-exposure", + method: 'POST', + path: '/ai-apps/{id}/data-exposure', summary: "Set Data Exposure For Ai App", requiresAuth: true, responses: [ @@ -693,8 +600,8 @@ export const aiAppsEndpoints: Endpoint[] = [ tag: "AI Apps", }, { - method: "POST", - path: "/ai-apps/from-shadow-ai/{shadowAiToolId}", + method: 'POST', + path: '/ai-apps/from-shadow-ai/{shadowAiToolId}', summary: "Promote From Shadow Ai", requiresAuth: true, responses: [ @@ -704,8 +611,8 @@ export const aiAppsEndpoints: Endpoint[] = [ tag: "AI Apps", }, { - method: "PATCH", - path: "/ai-apps/{id}/status", + method: 'PATCH', + path: '/ai-apps/{id}/status', summary: "Update Ai App Status", requiresAuth: true, responses: [ @@ -719,8 +626,8 @@ export const aiAppsEndpoints: Endpoint[] = [ // AI Audit endpoints export const aiAuditEndpoints: Endpoint[] = [ { - method: "GET", - path: "/ai-audit/analytics", + method: 'GET', + path: '/ai-audit/analytics', summary: "Get Analytics", requiresAuth: true, responses: [ @@ -730,8 +637,8 @@ export const aiAuditEndpoints: Endpoint[] = [ tag: "AI Audit", }, { - method: "GET", - path: "/ai-audit/export", + method: 'GET', + path: '/ai-audit/export', summary: "Export Audit Log", requiresAuth: true, responses: [ @@ -741,8 +648,8 @@ export const aiAuditEndpoints: Endpoint[] = [ tag: "AI Audit", }, { - method: "GET", - path: "/ai-audit/log/{actionId}", + method: 'GET', + path: '/ai-audit/log/{actionId}', summary: "Get Action Audit Trail", requiresAuth: true, responses: [ @@ -752,8 +659,8 @@ export const aiAuditEndpoints: Endpoint[] = [ tag: "AI Audit", }, { - method: "GET", - path: "/ai-audit/log", + method: 'GET', + path: '/ai-audit/log', summary: "Get Audit Log", requiresAuth: true, responses: [ @@ -767,8 +674,8 @@ export const aiAuditEndpoints: Endpoint[] = [ // AI Confirmation endpoints export const aiConfirmationEndpoints: Endpoint[] = [ { - method: "POST", - path: "/ai-confirmation/approve/{id}", + method: 'POST', + path: '/ai-confirmation/approve/{id}', summary: "Approve Confirmation", requiresAuth: true, responses: [ @@ -778,8 +685,8 @@ export const aiConfirmationEndpoints: Endpoint[] = [ tag: "AI Confirmation", }, { - method: "POST", - path: "/ai-confirmation/reject/{id}", + method: 'POST', + path: '/ai-confirmation/reject/{id}', summary: "Reject Confirmation", requiresAuth: true, responses: [ @@ -789,8 +696,8 @@ export const aiConfirmationEndpoints: Endpoint[] = [ tag: "AI Confirmation", }, { - method: "GET", - path: "/ai-confirmation/pending", + method: 'GET', + path: '/ai-confirmation/pending', summary: "Get Pending Confirmations", requiresAuth: true, responses: [ @@ -804,8 +711,8 @@ export const aiConfirmationEndpoints: Endpoint[] = [ // AI Content endpoints export const aiContentEndpoints: Endpoint[] = [ { - method: "GET", - path: "/ai-content/stats", + method: 'GET', + path: '/ai-content/stats', summary: "Get Stats", requiresAuth: true, responses: [ @@ -815,8 +722,8 @@ export const aiContentEndpoints: Endpoint[] = [ tag: "AI Content", }, { - method: "GET", - path: "/ai-content/unreviewed", + method: 'GET', + path: '/ai-content/unreviewed', summary: "Get Unreviewed", requiresAuth: true, responses: [ @@ -826,8 +733,8 @@ export const aiContentEndpoints: Endpoint[] = [ tag: "AI Content", }, { - method: "GET", - path: "/ai-content/{entityType}/{entityId}", + method: 'GET', + path: '/ai-content/{entityType}/{entityId}', summary: "Get Badges", requiresAuth: true, responses: [ @@ -837,8 +744,8 @@ export const aiContentEndpoints: Endpoint[] = [ tag: "AI Content", }, { - method: "PATCH", - path: "/ai-content/{id}/review", + method: 'PATCH', + path: '/ai-content/{id}/review', summary: "Review Content", requiresAuth: true, responses: [ @@ -852,8 +759,8 @@ export const aiContentEndpoints: Endpoint[] = [ // AI Detection endpoints export const aiDetectionEndpoints: Endpoint[] = [ { - method: "GET", - path: "/ai-detection/scans", + method: 'GET', + path: '/ai-detection/scans', summary: "Get Scans Controller", requiresAuth: true, responses: [ @@ -864,8 +771,8 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "POST", - path: "/ai-detection/scans", + method: 'POST', + path: '/ai-detection/scans', summary: "Start Scan Controller", requiresAuth: true, responses: [ @@ -876,8 +783,8 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "GET", - path: "/ai-detection/scans/active", + method: 'GET', + path: '/ai-detection/scans/active', summary: "Get Active Scan Controller", requiresAuth: true, responses: [ @@ -888,18 +795,12 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "GET", - path: "/ai-detection/scans/{scanId}", + method: 'GET', + path: '/ai-detection/scans/{scanId}', summary: "Get Scan Controller", requiresAuth: true, parameters: [ - { - name: "scanId", - in: "path", - type: "integer", - required: true, - description: "The scanId", - }, + { name: 'scanId', in: 'path', type: 'integer', required: true, description: "The scanId" }, ], responses: [ { status: 200, description: "Success" }, @@ -909,18 +810,12 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "DELETE", - path: "/ai-detection/scans/{scanId}", + method: 'DELETE', + path: '/ai-detection/scans/{scanId}', summary: "Delete Scan Controller", requiresAuth: true, parameters: [ - { - name: "scanId", - in: "path", - type: "integer", - required: true, - description: "The scanId", - }, + { name: 'scanId', in: 'path', type: 'integer', required: true, description: "The scanId" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -930,18 +825,12 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "GET", - path: "/ai-detection/scans/{scanId}/status", + method: 'GET', + path: '/ai-detection/scans/{scanId}/status', summary: "Get Scan Status Controller", requiresAuth: true, parameters: [ - { - name: "scanId", - in: "path", - type: "integer", - required: true, - description: "The scanId", - }, + { name: 'scanId', in: 'path', type: 'integer', required: true, description: "The scanId" }, ], responses: [ { status: 200, description: "Success" }, @@ -951,18 +840,12 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "GET", - path: "/ai-detection/scans/{scanId}/findings", + method: 'GET', + path: '/ai-detection/scans/{scanId}/findings', summary: "Get Scan Findings Controller", requiresAuth: true, parameters: [ - { - name: "scanId", - in: "path", - type: "integer", - required: true, - description: "The scanId", - }, + { name: 'scanId', in: 'path', type: 'integer', required: true, description: "The scanId" }, ], responses: [ { status: 200, description: "Success" }, @@ -972,18 +855,12 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "GET", - path: "/ai-detection/scans/{scanId}/security-findings", + method: 'GET', + path: '/ai-detection/scans/{scanId}/security-findings', summary: "Get Security Findings Controller", requiresAuth: true, parameters: [ - { - name: "scanId", - in: "path", - type: "integer", - required: true, - description: "The scanId", - }, + { name: 'scanId', in: 'path', type: 'integer', required: true, description: "The scanId" }, ], responses: [ { status: 200, description: "Success" }, @@ -993,18 +870,12 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "GET", - path: "/ai-detection/scans/{scanId}/security-summary", + method: 'GET', + path: '/ai-detection/scans/{scanId}/security-summary', summary: "Get Security Summary Controller", requiresAuth: true, parameters: [ - { - name: "scanId", - in: "path", - type: "integer", - required: true, - description: "The scanId", - }, + { name: 'scanId', in: 'path', type: 'integer', required: true, description: "The scanId" }, ], responses: [ { status: 200, description: "Success" }, @@ -1014,18 +885,12 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "POST", - path: "/ai-detection/scans/{scanId}/cancel", + method: 'POST', + path: '/ai-detection/scans/{scanId}/cancel', summary: "Cancel Scan Controller", requiresAuth: true, parameters: [ - { - name: "scanId", - in: "path", - type: "integer", - required: true, - description: "The scanId", - }, + { name: 'scanId', in: 'path', type: 'integer', required: true, description: "The scanId" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -1035,25 +900,13 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "PATCH", - path: "/ai-detection/scans/{scanId}/findings/{findingId}/governance", + method: 'PATCH', + path: '/ai-detection/scans/{scanId}/findings/{findingId}/governance', summary: "Update Governance Status Controller", requiresAuth: true, parameters: [ - { - name: "scanId", - in: "path", - type: "integer", - required: true, - description: "The scanId", - }, - { - name: "findingId", - in: "path", - type: "integer", - required: true, - description: "The findingId", - }, + { name: 'scanId', in: 'path', type: 'integer', required: true, description: "The scanId" }, + { name: 'findingId', in: 'path', type: 'integer', required: true, description: "The findingId" }, ], responses: [ { status: 200, description: "Success" }, @@ -1063,18 +916,12 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "GET", - path: "/ai-detection/scans/{scanId}/governance-summary", + method: 'GET', + path: '/ai-detection/scans/{scanId}/governance-summary', summary: "Get Governance Summary Controller", requiresAuth: true, parameters: [ - { - name: "scanId", - in: "path", - type: "integer", - required: true, - description: "The scanId", - }, + { name: 'scanId', in: 'path', type: 'integer', required: true, description: "The scanId" }, ], responses: [ { status: 200, description: "Success" }, @@ -1084,8 +931,8 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "GET", - path: "/ai-detection/stats", + method: 'GET', + path: '/ai-detection/stats', summary: "Get A I Detection Stats Controller", requiresAuth: true, responses: [ @@ -1096,18 +943,12 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "GET", - path: "/ai-detection/scans/{scanId}/export/ai-bom", + method: 'GET', + path: '/ai-detection/scans/{scanId}/export/ai-bom', summary: "Export A I B O M Controller", requiresAuth: true, parameters: [ - { - name: "scanId", - in: "path", - type: "integer", - required: true, - description: "The scanId", - }, + { name: 'scanId', in: 'path', type: 'integer', required: true, description: "The scanId" }, ], responses: [ { status: 200, description: "Success" }, @@ -1117,18 +958,12 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "GET", - path: "/ai-detection/scans/{scanId}/dependency-graph", + method: 'GET', + path: '/ai-detection/scans/{scanId}/dependency-graph', summary: "Get Dependency Graph Controller", requiresAuth: true, parameters: [ - { - name: "scanId", - in: "path", - type: "integer", - required: true, - description: "The scanId", - }, + { name: 'scanId', in: 'path', type: 'integer', required: true, description: "The scanId" }, ], responses: [ { status: 200, description: "Success" }, @@ -1138,18 +973,12 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "GET", - path: "/ai-detection/scans/{scanId}/compliance", + method: 'GET', + path: '/ai-detection/scans/{scanId}/compliance', summary: "Get Compliance Mapping Controller", requiresAuth: true, parameters: [ - { - name: "scanId", - in: "path", - type: "integer", - required: true, - description: "The scanId", - }, + { name: 'scanId', in: 'path', type: 'integer', required: true, description: "The scanId" }, ], responses: [ { status: 200, description: "Success" }, @@ -1159,18 +988,12 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "GET", - path: "/ai-detection/scans/{scanId}/risk-score", + method: 'GET', + path: '/ai-detection/scans/{scanId}/risk-score', summary: "Get Risk Score Controller", requiresAuth: true, parameters: [ - { - name: "scanId", - in: "path", - type: "integer", - required: true, - description: "The scanId", - }, + { name: 'scanId', in: 'path', type: 'integer', required: true, description: "The scanId" }, ], responses: [ { status: 200, description: "Success" }, @@ -1180,18 +1003,12 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "POST", - path: "/ai-detection/scans/{scanId}/risk-score/recalculate", + method: 'POST', + path: '/ai-detection/scans/{scanId}/risk-score/recalculate', summary: "Recalculate Risk Score Controller", requiresAuth: true, parameters: [ - { - name: "scanId", - in: "path", - type: "integer", - required: true, - description: "The scanId", - }, + { name: 'scanId', in: 'path', type: 'integer', required: true, description: "The scanId" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -1201,8 +1018,8 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "GET", - path: "/ai-detection/risk-scoring/config", + method: 'GET', + path: '/ai-detection/risk-scoring/config', summary: "Get Risk Scoring Config Controller", requiresAuth: true, responses: [ @@ -1213,8 +1030,8 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "PATCH", - path: "/ai-detection/risk-scoring/config", + method: 'PATCH', + path: '/ai-detection/risk-scoring/config', summary: "Update Risk Scoring Config Controller", requiresAuth: true, responses: [ @@ -1225,8 +1042,8 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "GET", - path: "/ai-detection/suppressions", + method: 'GET', + path: '/ai-detection/suppressions', summary: "List Suppressions Controller", requiresAuth: true, responses: [ @@ -1236,8 +1053,8 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "POST", - path: "/ai-detection/suppressions", + method: 'POST', + path: '/ai-detection/suppressions', summary: "Create Suppression Controller", requiresAuth: true, responses: [ @@ -1247,8 +1064,8 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "DELETE", - path: "/ai-detection/suppressions/{id}", + method: 'DELETE', + path: '/ai-detection/suppressions/{id}', summary: "Delete Suppression Controller", requiresAuth: true, responses: [ @@ -1258,8 +1075,8 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "GET", - path: "/ai-detection/repositories", + method: 'GET', + path: '/ai-detection/repositories', summary: "List Repositories", requiresAuth: true, responses: [ @@ -1270,8 +1087,8 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "POST", - path: "/ai-detection/repositories", + method: 'POST', + path: '/ai-detection/repositories', summary: "Create Repository", requiresAuth: true, responses: [ @@ -1282,18 +1099,12 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "GET", - path: "/ai-detection/repositories/{id}", + method: 'GET', + path: '/ai-detection/repositories/{id}', summary: "Get Repository", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -1303,18 +1114,12 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "PATCH", - path: "/ai-detection/repositories/{id}", + method: 'PATCH', + path: '/ai-detection/repositories/{id}', summary: "Update Repository", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -1324,18 +1129,12 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "DELETE", - path: "/ai-detection/repositories/{id}", + method: 'DELETE', + path: '/ai-detection/repositories/{id}', summary: "Delete Repository", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -1345,18 +1144,12 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "POST", - path: "/ai-detection/repositories/{id}/scan", + method: 'POST', + path: '/ai-detection/repositories/{id}/scan', summary: "Trigger Repository Scan", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -1366,18 +1159,12 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "POST", - path: "/ai-detection/repositories/{id}/webhook-secret", + method: 'POST', + path: '/ai-detection/repositories/{id}/webhook-secret', summary: "Generate Webhook Secret Controller", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -1387,18 +1174,12 @@ export const aiDetectionEndpoints: Endpoint[] = [ tag: "AI Detection", }, { - method: "GET", - path: "/ai-detection/repositories/{id}/scans", + method: 'GET', + path: '/ai-detection/repositories/{id}/scans', summary: "Get Repository Scans", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -1412,8 +1193,8 @@ export const aiDetectionEndpoints: Endpoint[] = [ // Incidents endpoints export const aiIncidentEndpoints: Endpoint[] = [ { - method: "GET", - path: "/ai-incident-managements", + method: 'GET', + path: '/ai-incident-managements', summary: "Get All Incidents", requiresAuth: true, responses: [ @@ -1424,8 +1205,8 @@ export const aiIncidentEndpoints: Endpoint[] = [ tag: "Incidents", }, { - method: "POST", - path: "/ai-incident-managements", + method: 'POST', + path: '/ai-incident-managements', summary: "Create New Incident", requiresAuth: true, responses: [ @@ -1436,18 +1217,12 @@ export const aiIncidentEndpoints: Endpoint[] = [ tag: "Incidents", }, { - method: "GET", - path: "/ai-incident-managements/{id}", + method: 'GET', + path: '/ai-incident-managements/{id}', summary: "Get Incident By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -1457,18 +1232,12 @@ export const aiIncidentEndpoints: Endpoint[] = [ tag: "Incidents", }, { - method: "PATCH", - path: "/ai-incident-managements/{id}", + method: 'PATCH', + path: '/ai-incident-managements/{id}', summary: "Update Incident By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -1478,18 +1247,12 @@ export const aiIncidentEndpoints: Endpoint[] = [ tag: "Incidents", }, { - method: "DELETE", - path: "/ai-incident-managements/{id}", + method: 'DELETE', + path: '/ai-incident-managements/{id}', summary: "Delete Incident By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -1499,18 +1262,12 @@ export const aiIncidentEndpoints: Endpoint[] = [ tag: "Incidents", }, { - method: "PATCH", - path: "/ai-incident-managements/{id}/archive", + method: 'PATCH', + path: '/ai-incident-managements/{id}/archive', summary: "Archive Incident By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -1524,8 +1281,8 @@ export const aiIncidentEndpoints: Endpoint[] = [ // AI Trust Centre endpoints export const aiTrustCentreEndpoints: Endpoint[] = [ { - method: "GET", - path: "/aiTrustCentre/overview", + method: 'GET', + path: '/aiTrustCentre/overview', summary: "Get A I Trust Centre Overview", requiresAuth: true, responses: [ @@ -1536,8 +1293,8 @@ export const aiTrustCentreEndpoints: Endpoint[] = [ tag: "AI Trust Centre", }, { - method: "PUT", - path: "/aiTrustCentre/overview", + method: 'PUT', + path: '/aiTrustCentre/overview', summary: "Update A I Trust Overview", requiresAuth: true, responses: [ @@ -1548,8 +1305,8 @@ export const aiTrustCentreEndpoints: Endpoint[] = [ tag: "AI Trust Centre", }, { - method: "GET", - path: "/aiTrustCentre/resources", + method: 'GET', + path: '/aiTrustCentre/resources', summary: "Get A I Trust Centre Resources", requiresAuth: true, responses: [ @@ -1560,8 +1317,8 @@ export const aiTrustCentreEndpoints: Endpoint[] = [ tag: "AI Trust Centre", }, { - method: "POST", - path: "/aiTrustCentre/resources", + method: 'POST', + path: '/aiTrustCentre/resources', summary: "Create A I Trust Resource", requiresAuth: true, responses: [ @@ -1572,8 +1329,8 @@ export const aiTrustCentreEndpoints: Endpoint[] = [ tag: "AI Trust Centre", }, { - method: "GET", - path: "/aiTrustCentre/subprocessors", + method: 'GET', + path: '/aiTrustCentre/subprocessors', summary: "Get A I Trust Centre Subprocessors", requiresAuth: true, responses: [ @@ -1584,8 +1341,8 @@ export const aiTrustCentreEndpoints: Endpoint[] = [ tag: "AI Trust Centre", }, { - method: "POST", - path: "/aiTrustCentre/subprocessors", + method: 'POST', + path: '/aiTrustCentre/subprocessors', summary: "Create A I Trust Subprocessor", requiresAuth: true, responses: [ @@ -1596,18 +1353,12 @@ export const aiTrustCentreEndpoints: Endpoint[] = [ tag: "AI Trust Centre", }, { - method: "GET", - path: "/aiTrustCentre/{hash}", + method: 'GET', + path: '/aiTrustCentre/{hash}', summary: "Get A I Trust Centre Public Page", requiresAuth: false, parameters: [ - { - name: "hash", - in: "path", - type: "string", - required: true, - description: "The hash", - }, + { name: 'hash', in: 'path', type: 'string', required: true, description: "The hash" }, ], responses: [ { status: 200, description: "Success" }, @@ -1616,18 +1367,12 @@ export const aiTrustCentreEndpoints: Endpoint[] = [ tag: "AI Trust Centre", }, { - method: "GET", - path: "/aiTrustCentre/{hash}/logo", + method: 'GET', + path: '/aiTrustCentre/{hash}/logo', summary: "Get Company Logo", requiresAuth: false, parameters: [ - { - name: "hash", - in: "path", - type: "string", - required: true, - description: "The hash", - }, + { name: 'hash', in: 'path', type: 'string', required: true, description: "The hash" }, ], responses: [ { status: 200, description: "Success" }, @@ -1636,25 +1381,13 @@ export const aiTrustCentreEndpoints: Endpoint[] = [ tag: "AI Trust Centre", }, { - method: "GET", - path: "/aiTrustCentre/{hash}/resources/{id}", + method: 'GET', + path: '/aiTrustCentre/{hash}/resources/{id}', summary: "Get A I Trust Centre Public Resource", requiresAuth: false, parameters: [ - { - name: "hash", - in: "path", - type: "string", - required: true, - description: "The hash", - }, - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'hash', in: 'path', type: 'string', required: true, description: "The hash" }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -1663,8 +1396,8 @@ export const aiTrustCentreEndpoints: Endpoint[] = [ tag: "AI Trust Centre", }, { - method: "POST", - path: "/aiTrustCentre/logo", + method: 'POST', + path: '/aiTrustCentre/logo', summary: "Upload company logo", requiresAuth: true, responses: [ @@ -1675,8 +1408,8 @@ export const aiTrustCentreEndpoints: Endpoint[] = [ tag: "AI Trust Centre", }, { - method: "DELETE", - path: "/aiTrustCentre/logo", + method: 'DELETE', + path: '/aiTrustCentre/logo', summary: "Delete Company Logo", requiresAuth: true, responses: [ @@ -1687,18 +1420,12 @@ export const aiTrustCentreEndpoints: Endpoint[] = [ tag: "AI Trust Centre", }, { - method: "PUT", - path: "/aiTrustCentre/resources/{id}", + method: 'PUT', + path: '/aiTrustCentre/resources/{id}', summary: "Update A I Trust Resource", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -1708,18 +1435,12 @@ export const aiTrustCentreEndpoints: Endpoint[] = [ tag: "AI Trust Centre", }, { - method: "DELETE", - path: "/aiTrustCentre/resources/{id}", + method: 'DELETE', + path: '/aiTrustCentre/resources/{id}', summary: "Delete A I Trust Resource", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -1729,18 +1450,12 @@ export const aiTrustCentreEndpoints: Endpoint[] = [ tag: "AI Trust Centre", }, { - method: "PUT", - path: "/aiTrustCentre/subprocessors/{id}", + method: 'PUT', + path: '/aiTrustCentre/subprocessors/{id}', summary: "Update A I Trust Subprocessor", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -1750,18 +1465,12 @@ export const aiTrustCentreEndpoints: Endpoint[] = [ tag: "AI Trust Centre", }, { - method: "DELETE", - path: "/aiTrustCentre/subprocessors/{id}", + method: 'DELETE', + path: '/aiTrustCentre/subprocessors/{id}', summary: "Delete A I Trust Subprocessor", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -1775,8 +1484,8 @@ export const aiTrustCentreEndpoints: Endpoint[] = [ // Ai Trust Index endpoints export const aiTrustIndexEndpoints: Endpoint[] = [ { - method: "GET", - path: "/ai-trust-index/apps", + method: 'GET', + path: '/ai-trust-index/apps', summary: "Get Apps", requiresAuth: true, responses: [ @@ -1786,8 +1495,8 @@ export const aiTrustIndexEndpoints: Endpoint[] = [ tag: "Ai Trust Index", }, { - method: "GET", - path: "/ai-trust-index/apps/{slug}", + method: 'GET', + path: '/ai-trust-index/apps/{slug}', summary: "Get App", requiresAuth: true, responses: [ @@ -1797,8 +1506,8 @@ export const aiTrustIndexEndpoints: Endpoint[] = [ tag: "Ai Trust Index", }, { - method: "GET", - path: "/ai-trust-index/tracked", + method: 'GET', + path: '/ai-trust-index/tracked', summary: "Get Tracked", requiresAuth: true, responses: [ @@ -1808,8 +1517,8 @@ export const aiTrustIndexEndpoints: Endpoint[] = [ tag: "Ai Trust Index", }, { - method: "POST", - path: "/ai-trust-index/tracked", + method: 'POST', + path: '/ai-trust-index/tracked', summary: "Track App", requiresAuth: true, responses: [ @@ -1819,8 +1528,8 @@ export const aiTrustIndexEndpoints: Endpoint[] = [ tag: "Ai Trust Index", }, { - method: "POST", - path: "/ai-trust-index/tracked/bulk", + method: 'POST', + path: '/ai-trust-index/tracked/bulk', summary: "Track Apps Bulk", requiresAuth: true, responses: [ @@ -1830,8 +1539,8 @@ export const aiTrustIndexEndpoints: Endpoint[] = [ tag: "Ai Trust Index", }, { - method: "DELETE", - path: "/ai-trust-index/tracked/{slug}", + method: 'DELETE', + path: '/ai-trust-index/tracked/{slug}', summary: "Untrack App", requiresAuth: true, responses: [ @@ -1841,8 +1550,8 @@ export const aiTrustIndexEndpoints: Endpoint[] = [ tag: "Ai Trust Index", }, { - method: "GET", - path: "/ai-trust-index/settings", + method: 'GET', + path: '/ai-trust-index/settings', summary: "Get Settings", requiresAuth: true, responses: [ @@ -1852,8 +1561,8 @@ export const aiTrustIndexEndpoints: Endpoint[] = [ tag: "Ai Trust Index", }, { - method: "PUT", - path: "/ai-trust-index/settings", + method: 'PUT', + path: '/ai-trust-index/settings', summary: "Update Settings", requiresAuth: true, responses: [ @@ -1867,8 +1576,8 @@ export const aiTrustIndexEndpoints: Endpoint[] = [ // Approval Workflows endpoints export const approvalWorkflowEndpoints: Endpoint[] = [ { - method: "GET", - path: "/approval-workflows", + method: 'GET', + path: '/approval-workflows', summary: "Get All Approval Workflows", requiresAuth: true, responses: [ @@ -1879,8 +1588,8 @@ export const approvalWorkflowEndpoints: Endpoint[] = [ tag: "Approval Workflows", }, { - method: "POST", - path: "/approval-workflows", + method: 'POST', + path: '/approval-workflows', summary: "Create Approval Workflow", description: "Requires role: Admin", requiresAuth: true, @@ -1893,18 +1602,12 @@ export const approvalWorkflowEndpoints: Endpoint[] = [ tag: "Approval Workflows", }, { - method: "GET", - path: "/approval-workflows/{id}", + method: 'GET', + path: '/approval-workflows/{id}', summary: "Get Approval Workflow By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -1914,19 +1617,13 @@ export const approvalWorkflowEndpoints: Endpoint[] = [ tag: "Approval Workflows", }, { - method: "PUT", - path: "/approval-workflows/{id}", + method: 'PUT', + path: '/approval-workflows/{id}', summary: "Update Approval Workflow", description: "Requires role: Admin", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -1937,19 +1634,13 @@ export const approvalWorkflowEndpoints: Endpoint[] = [ tag: "Approval Workflows", }, { - method: "DELETE", - path: "/approval-workflows/{id}", + method: 'DELETE', + path: '/approval-workflows/{id}', summary: "Delete Approval Workflow", description: "Requires role: Admin", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -1960,8 +1651,8 @@ export const approvalWorkflowEndpoints: Endpoint[] = [ tag: "Approval Workflows", }, { - method: "POST", - path: "/approval-requests", + method: 'POST', + path: '/approval-requests', summary: "Create Approval Request", requiresAuth: true, responses: [ @@ -1972,8 +1663,8 @@ export const approvalWorkflowEndpoints: Endpoint[] = [ tag: "Approval Workflows", }, { - method: "GET", - path: "/approval-requests/my-requests", + method: 'GET', + path: '/approval-requests/my-requests', summary: "Get My Approval Requests", requiresAuth: true, responses: [ @@ -1984,8 +1675,8 @@ export const approvalWorkflowEndpoints: Endpoint[] = [ tag: "Approval Workflows", }, { - method: "GET", - path: "/approval-requests/pending-approvals", + method: 'GET', + path: '/approval-requests/pending-approvals', summary: "Get Pending Approvals", requiresAuth: true, responses: [ @@ -1996,8 +1687,8 @@ export const approvalWorkflowEndpoints: Endpoint[] = [ tag: "Approval Workflows", }, { - method: "GET", - path: "/approval-requests/all", + method: 'GET', + path: '/approval-requests/all', summary: "Get All Approval Requests", description: "Requires role: Admin", requiresAuth: true, @@ -2010,18 +1701,12 @@ export const approvalWorkflowEndpoints: Endpoint[] = [ tag: "Approval Workflows", }, { - method: "GET", - path: "/approval-requests/{id}", + method: 'GET', + path: '/approval-requests/{id}', summary: "Get Approval Request By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -2031,18 +1716,12 @@ export const approvalWorkflowEndpoints: Endpoint[] = [ tag: "Approval Workflows", }, { - method: "POST", - path: "/approval-requests/{id}/approve", + method: 'POST', + path: '/approval-requests/{id}/approve', summary: "Approve Request", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -2052,18 +1731,12 @@ export const approvalWorkflowEndpoints: Endpoint[] = [ tag: "Approval Workflows", }, { - method: "POST", - path: "/approval-requests/{id}/reject", + method: 'POST', + path: '/approval-requests/{id}/reject', summary: "Reject Request", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -2073,18 +1746,12 @@ export const approvalWorkflowEndpoints: Endpoint[] = [ tag: "Approval Workflows", }, { - method: "POST", - path: "/approval-requests/{id}/withdraw", + method: 'POST', + path: '/approval-requests/{id}/withdraw', summary: "Withdraw approval request", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -2098,8 +1765,8 @@ export const approvalWorkflowEndpoints: Endpoint[] = [ // Assessments endpoints export const assessmentEndpoints: Endpoint[] = [ { - method: "GET", - path: "/questions", + method: 'GET', + path: '/questions', summary: "Get All Questions", requiresAuth: true, responses: [ @@ -2110,18 +1777,12 @@ export const assessmentEndpoints: Endpoint[] = [ tag: "Assessments", }, { - method: "GET", - path: "/questions/{id}", + method: 'GET', + path: '/questions/{id}', summary: "Get Question By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -2131,18 +1792,12 @@ export const assessmentEndpoints: Endpoint[] = [ tag: "Assessments", }, { - method: "GET", - path: "/questions/bysubtopic/{id}", + method: 'GET', + path: '/questions/bysubtopic/{id}', summary: "Get Questions By Subtopic Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -2152,18 +1807,12 @@ export const assessmentEndpoints: Endpoint[] = [ tag: "Assessments", }, { - method: "GET", - path: "/questions/bytopic/{id}", + method: 'GET', + path: '/questions/bytopic/{id}', summary: "Get Questions By Topic Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -2173,8 +1822,8 @@ export const assessmentEndpoints: Endpoint[] = [ tag: "Assessments", }, { - method: "GET", - path: "/assessments", + method: 'GET', + path: '/assessments', summary: "Get All Assessments", requiresAuth: true, responses: [ @@ -2185,8 +1834,8 @@ export const assessmentEndpoints: Endpoint[] = [ tag: "Assessments", }, { - method: "POST", - path: "/assessments", + method: 'POST', + path: '/assessments', summary: "Create Assessment", requiresAuth: true, responses: [ @@ -2196,18 +1845,12 @@ export const assessmentEndpoints: Endpoint[] = [ tag: "Assessments", }, { - method: "GET", - path: "/assessments/getAnswers/{id}", + method: 'GET', + path: '/assessments/getAnswers/{id}', summary: "Get Answers", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -2217,18 +1860,12 @@ export const assessmentEndpoints: Endpoint[] = [ tag: "Assessments", }, { - method: "GET", - path: "/assessments/{id}", + method: 'GET', + path: '/assessments/{id}', summary: "Get Assessment By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -2238,8 +1875,8 @@ export const assessmentEndpoints: Endpoint[] = [ tag: "Assessments", }, { - method: "PUT", - path: "/assessments/{id}", + method: 'PUT', + path: '/assessments/{id}', summary: "Update Assessment By Id", requiresAuth: true, responses: [ @@ -2249,8 +1886,8 @@ export const assessmentEndpoints: Endpoint[] = [ tag: "Assessments", }, { - method: "DELETE", - path: "/assessments/{id}", + method: 'DELETE', + path: '/assessments/{id}', summary: "Delete Assessment By Id", requiresAuth: true, responses: [ @@ -2260,18 +1897,12 @@ export const assessmentEndpoints: Endpoint[] = [ tag: "Assessments", }, { - method: "GET", - path: "/assessments/project/byid/{id}", + method: 'GET', + path: '/assessments/project/byid/{id}', summary: "Get Assessment By Project Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -2285,8 +1916,8 @@ export const assessmentEndpoints: Endpoint[] = [ // Audit endpoints export const auditEndpoints: Endpoint[] = [ { - method: "GET", - path: "/audit-ledger", + method: 'GET', + path: '/audit-ledger', summary: "Get Audit Ledger", description: "Requires role: Admin or SuperAdmin", requiresAuth: true, @@ -2299,8 +1930,8 @@ export const auditEndpoints: Endpoint[] = [ tag: "Audit", }, { - method: "GET", - path: "/audit-ledger/verify", + method: 'GET', + path: '/audit-ledger/verify', summary: "Verify Audit Ledger", description: "Requires role: Admin", requiresAuth: true, @@ -2317,8 +1948,8 @@ export const auditEndpoints: Endpoint[] = [ // Authentication endpoints export const authenticationEndpoints: Endpoint[] = [ { - method: "GET", - path: "/tokens", + method: 'GET', + path: '/tokens', summary: "Get Api Tokens", requiresAuth: true, responses: [ @@ -2329,8 +1960,8 @@ export const authenticationEndpoints: Endpoint[] = [ tag: "Authentication", }, { - method: "POST", - path: "/tokens", + method: 'POST', + path: '/tokens', summary: "Create Api Token", requiresAuth: true, responses: [ @@ -2341,8 +1972,8 @@ export const authenticationEndpoints: Endpoint[] = [ tag: "Authentication", }, { - method: "POST", - path: "/tokens/{id}/revoke", + method: 'POST', + path: '/tokens/{id}/revoke', summary: "Revoke Api Token", requiresAuth: true, responses: [ @@ -2352,18 +1983,12 @@ export const authenticationEndpoints: Endpoint[] = [ tag: "Authentication", }, { - method: "DELETE", - path: "/tokens/{id}", + method: 'DELETE', + path: '/tokens/{id}', summary: "Delete Api Token", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -2377,8 +2002,8 @@ export const authenticationEndpoints: Endpoint[] = [ // Automations endpoints export const automationEndpoints: Endpoint[] = [ { - method: "GET", - path: "/automations", + method: 'GET', + path: '/automations', summary: "Get All Automations", requiresAuth: true, responses: [ @@ -2389,8 +2014,8 @@ export const automationEndpoints: Endpoint[] = [ tag: "Automations", }, { - method: "POST", - path: "/automations", + method: 'POST', + path: '/automations', summary: "Create Automation", requiresAuth: true, responses: [ @@ -2401,8 +2026,8 @@ export const automationEndpoints: Endpoint[] = [ tag: "Automations", }, { - method: "GET", - path: "/automations/triggers", + method: 'GET', + path: '/automations/triggers', summary: "Get All Automation Triggers", requiresAuth: true, responses: [ @@ -2413,18 +2038,12 @@ export const automationEndpoints: Endpoint[] = [ tag: "Automations", }, { - method: "GET", - path: "/automations/actions/by-triggerId/{triggerId}", + method: 'GET', + path: '/automations/actions/by-triggerId/{triggerId}', summary: "Get All Automation Actions By Trigger Id", requiresAuth: true, parameters: [ - { - name: "triggerId", - in: "path", - type: "integer", - required: true, - description: "The triggerId", - }, + { name: 'triggerId', in: 'path', type: 'integer', required: true, description: "The triggerId" }, ], responses: [ { status: 200, description: "Success" }, @@ -2434,18 +2053,12 @@ export const automationEndpoints: Endpoint[] = [ tag: "Automations", }, { - method: "GET", - path: "/automations/{id}/history", + method: 'GET', + path: '/automations/{id}/history', summary: "Get Automation History", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -2455,18 +2068,12 @@ export const automationEndpoints: Endpoint[] = [ tag: "Automations", }, { - method: "GET", - path: "/automations/{id}/stats", + method: 'GET', + path: '/automations/{id}/stats', summary: "Get Automation Stats", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -2476,18 +2083,12 @@ export const automationEndpoints: Endpoint[] = [ tag: "Automations", }, { - method: "GET", - path: "/automations/{id}", + method: 'GET', + path: '/automations/{id}', summary: "Get Automation By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -2497,18 +2098,12 @@ export const automationEndpoints: Endpoint[] = [ tag: "Automations", }, { - method: "PUT", - path: "/automations/{id}", + method: 'PUT', + path: '/automations/{id}', summary: "Update Automation", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -2518,18 +2113,12 @@ export const automationEndpoints: Endpoint[] = [ tag: "Automations", }, { - method: "DELETE", - path: "/automations/{id}", + method: 'DELETE', + path: '/automations/{id}', summary: "Delete Automation By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -2543,18 +2132,12 @@ export const automationEndpoints: Endpoint[] = [ // CE Marking endpoints export const ceMarkingEndpoints: Endpoint[] = [ { - method: "GET", - path: "/ce-marking/{projectId}", + method: 'GET', + path: '/ce-marking/{projectId}', summary: "Get C E Marking", requiresAuth: true, parameters: [ - { - name: "projectId", - in: "path", - type: "integer", - required: true, - description: "The projectId", - }, + { name: 'projectId', in: 'path', type: 'integer', required: true, description: "The projectId" }, ], responses: [ { status: 200, description: "Success" }, @@ -2564,18 +2147,12 @@ export const ceMarkingEndpoints: Endpoint[] = [ tag: "CE Marking", }, { - method: "PUT", - path: "/ce-marking/{projectId}", + method: 'PUT', + path: '/ce-marking/{projectId}', summary: "Update C E Marking", requiresAuth: true, parameters: [ - { - name: "projectId", - in: "path", - type: "integer", - required: true, - description: "The projectId", - }, + { name: 'projectId', in: 'path', type: 'integer', required: true, description: "The projectId" }, ], responses: [ { status: 200, description: "Success" }, @@ -2589,18 +2166,12 @@ export const ceMarkingEndpoints: Endpoint[] = [ // Change History endpoints export const changeHistoryEndpoints: Endpoint[] = [ { - method: "GET", - path: "/vendor-change-history/{id}", + method: 'GET', + path: '/vendor-change-history/{id}', summary: "Get Vendor Change History By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -2610,18 +2181,12 @@ export const changeHistoryEndpoints: Endpoint[] = [ tag: "Change History", }, { - method: "GET", - path: "/model-inventory-change-history/{id}", + method: 'GET', + path: '/model-inventory-change-history/{id}', summary: "Get Model Inventory Change History By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -2631,18 +2196,12 @@ export const changeHistoryEndpoints: Endpoint[] = [ tag: "Change History", }, { - method: "GET", - path: "/vendor-risk-change-history/{id}", + method: 'GET', + path: '/vendor-risk-change-history/{id}', summary: "Get Vendor Risk Change History By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -2652,18 +2211,12 @@ export const changeHistoryEndpoints: Endpoint[] = [ tag: "Change History", }, { - method: "GET", - path: "/policy-change-history/{id}", + method: 'GET', + path: '/policy-change-history/{id}', summary: "Get Policy Change History By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -2673,18 +2226,12 @@ export const changeHistoryEndpoints: Endpoint[] = [ tag: "Change History", }, { - method: "GET", - path: "/incident-change-history/{incidentId}", + method: 'GET', + path: '/incident-change-history/{incidentId}', summary: "Get Incident History", requiresAuth: true, parameters: [ - { - name: "incidentId", - in: "path", - type: "integer", - required: true, - description: "The incidentId", - }, + { name: 'incidentId', in: 'path', type: 'integer', required: true, description: "The incidentId" }, ], responses: [ { status: 200, description: "Success" }, @@ -2694,18 +2241,12 @@ export const changeHistoryEndpoints: Endpoint[] = [ tag: "Change History", }, { - method: "GET", - path: "/use-case-change-history/{useCaseId}", + method: 'GET', + path: '/use-case-change-history/{useCaseId}', summary: "Get Use Case History", requiresAuth: true, parameters: [ - { - name: "useCaseId", - in: "path", - type: "integer", - required: true, - description: "The useCaseId", - }, + { name: 'useCaseId', in: 'path', type: 'integer', required: true, description: "The useCaseId" }, ], responses: [ { status: 200, description: "Success" }, @@ -2715,18 +2256,12 @@ export const changeHistoryEndpoints: Endpoint[] = [ tag: "Change History", }, { - method: "GET", - path: "/risk-change-history/{projectRiskId}", + method: 'GET', + path: '/risk-change-history/{projectRiskId}', summary: "Get Project Risk Change History By Risk Id", requiresAuth: true, parameters: [ - { - name: "projectRiskId", - in: "path", - type: "integer", - required: true, - description: "The projectRiskId", - }, + { name: 'projectRiskId', in: 'path', type: 'integer', required: true, description: "The projectRiskId" }, ], responses: [ { status: 200, description: "Success" }, @@ -2736,18 +2271,12 @@ export const changeHistoryEndpoints: Endpoint[] = [ tag: "Change History", }, { - method: "GET", - path: "/file-change-history/{id}", + method: 'GET', + path: '/file-change-history/{id}', summary: "Get File Change History By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -2757,18 +2286,12 @@ export const changeHistoryEndpoints: Endpoint[] = [ tag: "Change History", }, { - method: "GET", - path: "/task-change-history/{id}", + method: 'GET', + path: '/task-change-history/{id}', summary: "Get Task Change History By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -2778,18 +2301,12 @@ export const changeHistoryEndpoints: Endpoint[] = [ tag: "Change History", }, { - method: "GET", - path: "/training-change-history/{id}", + method: 'GET', + path: '/training-change-history/{id}', summary: "Get Training Change History By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -2799,18 +2316,12 @@ export const changeHistoryEndpoints: Endpoint[] = [ tag: "Change History", }, { - method: "GET", - path: "/model-risk-change-history/{id}", + method: 'GET', + path: '/model-risk-change-history/{id}', summary: "Get Model Risk Change History By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -2820,18 +2331,12 @@ export const changeHistoryEndpoints: Endpoint[] = [ tag: "Change History", }, { - method: "GET", - path: "/dataset-change-history/{id}", + method: 'GET', + path: '/dataset-change-history/{id}', summary: "Get Dataset Change History By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -2845,8 +2350,8 @@ export const changeHistoryEndpoints: Endpoint[] = [ // Compliance endpoints export const complianceEndpoints: Endpoint[] = [ { - method: "GET", - path: "/compliance/score", + method: 'GET', + path: '/compliance/score', summary: "Get Compliance Score", requiresAuth: true, responses: [ @@ -2857,18 +2362,12 @@ export const complianceEndpoints: Endpoint[] = [ tag: "Compliance", }, { - method: "GET", - path: "/compliance/score/{organizationId}", + method: 'GET', + path: '/compliance/score/{organizationId}', summary: "Get Compliance Score By Organization", requiresAuth: true, parameters: [ - { - name: "organizationId", - in: "path", - type: "integer", - required: true, - description: "The organizationId", - }, + { name: 'organizationId', in: 'path', type: 'integer', required: true, description: "The organizationId" }, ], responses: [ { status: 200, description: "Success" }, @@ -2878,18 +2377,12 @@ export const complianceEndpoints: Endpoint[] = [ tag: "Compliance", }, { - method: "GET", - path: "/compliance/details/{organizationId}", + method: 'GET', + path: '/compliance/details/{organizationId}', summary: "Get Compliance Details", requiresAuth: true, parameters: [ - { - name: "organizationId", - in: "path", - type: "integer", - required: true, - description: "The organizationId", - }, + { name: 'organizationId', in: 'path', type: 'integer', required: true, description: "The organizationId" }, ], responses: [ { status: 200, description: "Success" }, @@ -2903,8 +2396,8 @@ export const complianceEndpoints: Endpoint[] = [ // Custom Fields endpoints export const customFieldsEndpoints: Endpoint[] = [ { - method: "GET", - path: "/custom-fields/definitions/by-id/{id}", + method: 'GET', + path: '/custom-fields/definitions/by-id/{id}', summary: "Get Custom Field Definition By Id", requiresAuth: true, responses: [ @@ -2914,8 +2407,8 @@ export const customFieldsEndpoints: Endpoint[] = [ tag: "Custom Fields", }, { - method: "GET", - path: "/custom-fields/definitions/{entityType}", + method: 'GET', + path: '/custom-fields/definitions/{entityType}', summary: "List Custom Field Definitions", requiresAuth: true, responses: [ @@ -2925,8 +2418,8 @@ export const customFieldsEndpoints: Endpoint[] = [ tag: "Custom Fields", }, { - method: "POST", - path: "/custom-fields/definitions", + method: 'POST', + path: '/custom-fields/definitions', summary: "Create Custom Field Definition", description: "Requires role: Admin", requiresAuth: true, @@ -2937,8 +2430,8 @@ export const customFieldsEndpoints: Endpoint[] = [ tag: "Custom Fields", }, { - method: "PATCH", - path: "/custom-fields/definitions/{id}", + method: 'PATCH', + path: '/custom-fields/definitions/{id}', summary: "Update Custom Field Definition", description: "Requires role: Admin", requiresAuth: true, @@ -2949,8 +2442,8 @@ export const customFieldsEndpoints: Endpoint[] = [ tag: "Custom Fields", }, { - method: "DELETE", - path: "/custom-fields/definitions/{id}", + method: 'DELETE', + path: '/custom-fields/definitions/{id}', summary: "Delete Custom Field Definition", description: "Requires role: Admin", requiresAuth: true, @@ -2961,8 +2454,8 @@ export const customFieldsEndpoints: Endpoint[] = [ tag: "Custom Fields", }, { - method: "GET", - path: "/custom-fields/values/{entityType}/{entityId}/missing-required", + method: 'GET', + path: '/custom-fields/values/{entityType}/{entityId}/missing-required', summary: "Get Missing Required Custom Fields", requiresAuth: true, responses: [ @@ -2972,8 +2465,8 @@ export const customFieldsEndpoints: Endpoint[] = [ tag: "Custom Fields", }, { - method: "GET", - path: "/custom-fields/values/{entityType}/{entityId}", + method: 'GET', + path: '/custom-fields/values/{entityType}/{entityId}', summary: "Get Custom Field Values For Entity", requiresAuth: true, responses: [ @@ -2983,8 +2476,8 @@ export const customFieldsEndpoints: Endpoint[] = [ tag: "Custom Fields", }, { - method: "PUT", - path: "/custom-fields/values", + method: 'PUT', + path: '/custom-fields/values', summary: "Set Custom Field Value", requiresAuth: true, responses: [ @@ -2994,8 +2487,8 @@ export const customFieldsEndpoints: Endpoint[] = [ tag: "Custom Fields", }, { - method: "DELETE", - path: "/custom-fields/values/{definitionId}/{entityId}", + method: 'DELETE', + path: '/custom-fields/values/{definitionId}/{entityId}', summary: "Delete Custom Field Value", requiresAuth: true, responses: [ @@ -3009,8 +2502,8 @@ export const customFieldsEndpoints: Endpoint[] = [ // Dashboard endpoints export const dashboardEndpoints: Endpoint[] = [ { - method: "GET", - path: "/dashboard", + method: 'GET', + path: '/dashboard', summary: "Get Dashboard Data", requiresAuth: true, responses: [ @@ -3025,8 +2518,8 @@ export const dashboardEndpoints: Endpoint[] = [ // Datasets endpoints export const datasetEndpoints: Endpoint[] = [ { - method: "POST", - path: "/dataset-bulk-upload/upload", + method: 'POST', + path: '/dataset-bulk-upload/upload', summary: "Handle Multer Error", description: "Requires role: Admin or Editor", requiresAuth: true, @@ -3039,8 +2532,8 @@ export const datasetEndpoints: Endpoint[] = [ tag: "Datasets", }, { - method: "GET", - path: "/datasets", + method: 'GET', + path: '/datasets', summary: "Get All Datasets", requiresAuth: true, responses: [ @@ -3051,8 +2544,8 @@ export const datasetEndpoints: Endpoint[] = [ tag: "Datasets", }, { - method: "POST", - path: "/datasets", + method: 'POST', + path: '/datasets', summary: "Create New Dataset", requiresAuth: true, responses: [ @@ -3063,18 +2556,12 @@ export const datasetEndpoints: Endpoint[] = [ tag: "Datasets", }, { - method: "GET", - path: "/datasets/{id}", + method: 'GET', + path: '/datasets/{id}', summary: "Get Dataset By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -3084,18 +2571,12 @@ export const datasetEndpoints: Endpoint[] = [ tag: "Datasets", }, { - method: "PATCH", - path: "/datasets/{id}", + method: 'PATCH', + path: '/datasets/{id}', summary: "Update Dataset By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -3105,18 +2586,12 @@ export const datasetEndpoints: Endpoint[] = [ tag: "Datasets", }, { - method: "DELETE", - path: "/datasets/{id}", + method: 'DELETE', + path: '/datasets/{id}', summary: "Delete Dataset By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -3126,18 +2601,12 @@ export const datasetEndpoints: Endpoint[] = [ tag: "Datasets", }, { - method: "GET", - path: "/datasets/by-model/{modelId}", + method: 'GET', + path: '/datasets/by-model/{modelId}', summary: "Get Datasets By Model Id", requiresAuth: true, parameters: [ - { - name: "modelId", - in: "path", - type: "integer", - required: true, - description: "The modelId", - }, + { name: 'modelId', in: 'path', type: 'integer', required: true, description: "The modelId" }, ], responses: [ { status: 200, description: "Success" }, @@ -3147,18 +2616,12 @@ export const datasetEndpoints: Endpoint[] = [ tag: "Datasets", }, { - method: "GET", - path: "/datasets/by-project/{projectId}", + method: 'GET', + path: '/datasets/by-project/{projectId}', summary: "Get Datasets By Project Id", requiresAuth: true, parameters: [ - { - name: "projectId", - in: "path", - type: "integer", - required: true, - description: "The projectId", - }, + { name: 'projectId', in: 'path', type: 'integer', required: true, description: "The projectId" }, ], responses: [ { status: 200, description: "Success" }, @@ -3168,18 +2631,12 @@ export const datasetEndpoints: Endpoint[] = [ tag: "Datasets", }, { - method: "GET", - path: "/datasets/{id}/history", + method: 'GET', + path: '/datasets/{id}/history', summary: "Get Dataset History", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -3193,8 +2650,8 @@ export const datasetEndpoints: Endpoint[] = [ // Deadlines endpoints export const deadlinesEndpoints: Endpoint[] = [ { - method: "GET", - path: "/deadlines/summary", + method: 'GET', + path: '/deadlines/summary', summary: "Get Deadlines Summary", requiresAuth: true, responses: [ @@ -3208,8 +2665,8 @@ export const deadlinesEndpoints: Endpoint[] = [ // Demo Data endpoints export const demoDataEndpoints: Endpoint[] = [ { - method: "POST", - path: "/autoDrivers", + method: 'POST', + path: '/autoDrivers', summary: "Post Auto Driver", description: "Requires role: Admin", requiresAuth: true, @@ -3222,8 +2679,8 @@ export const demoDataEndpoints: Endpoint[] = [ tag: "Demo Data", }, { - method: "DELETE", - path: "/autoDrivers", + method: 'DELETE', + path: '/autoDrivers', summary: "Delete Auto Driver", description: "Requires role: Admin", requiresAuth: true, @@ -3240,8 +2697,8 @@ export const demoDataEndpoints: Endpoint[] = [ // Mail endpoints export const emailEndpoints: Endpoint[] = [ { - method: "POST", - path: "/mail/invite", + method: 'POST', + path: '/mail/invite', summary: "Invite Limiter", requiresAuth: true, responses: [ @@ -3252,8 +2709,8 @@ export const emailEndpoints: Endpoint[] = [ tag: "Mail", }, { - method: "POST", - path: "/mail/reset-password", + method: 'POST', + path: '/mail/reset-password', summary: "Email", requiresAuth: false, responses: [ @@ -3267,8 +2724,8 @@ export const emailEndpoints: Endpoint[] = [ // Entity Graph endpoints export const entityGraphEndpoints: Endpoint[] = [ { - method: "GET", - path: "/entity-graph/annotations", + method: 'GET', + path: '/entity-graph/annotations', summary: "Get Annotations", requiresAuth: true, responses: [ @@ -3279,8 +2736,8 @@ export const entityGraphEndpoints: Endpoint[] = [ tag: "Entity Graph", }, { - method: "POST", - path: "/entity-graph/annotations", + method: 'POST', + path: '/entity-graph/annotations', summary: "Save Annotation", requiresAuth: true, responses: [ @@ -3291,25 +2748,13 @@ export const entityGraphEndpoints: Endpoint[] = [ tag: "Entity Graph", }, { - method: "GET", - path: "/entity-graph/annotations/{entityType}/{entityId}", + method: 'GET', + path: '/entity-graph/annotations/{entityType}/{entityId}', summary: "Get Annotation By Entity", requiresAuth: true, parameters: [ - { - name: "entityType", - in: "path", - type: "string", - required: true, - description: "The entityType", - }, - { - name: "entityId", - in: "path", - type: "integer", - required: true, - description: "The entityId", - }, + { name: 'entityType', in: 'path', type: 'string', required: true, description: "The entityType" }, + { name: 'entityId', in: 'path', type: 'integer', required: true, description: "The entityId" }, ], responses: [ { status: 200, description: "Success" }, @@ -3319,18 +2764,12 @@ export const entityGraphEndpoints: Endpoint[] = [ tag: "Entity Graph", }, { - method: "DELETE", - path: "/entity-graph/annotations/{id}", + method: 'DELETE', + path: '/entity-graph/annotations/{id}', summary: "Delete Annotation", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -3340,25 +2779,13 @@ export const entityGraphEndpoints: Endpoint[] = [ tag: "Entity Graph", }, { - method: "DELETE", - path: "/entity-graph/annotations/entity/{entityType}/{entityId}", + method: 'DELETE', + path: '/entity-graph/annotations/entity/{entityType}/{entityId}', summary: "Delete Annotation By Entity", requiresAuth: true, parameters: [ - { - name: "entityType", - in: "path", - type: "string", - required: true, - description: "The entityType", - }, - { - name: "entityId", - in: "path", - type: "integer", - required: true, - description: "The entityId", - }, + { name: 'entityType', in: 'path', type: 'string', required: true, description: "The entityType" }, + { name: 'entityId', in: 'path', type: 'integer', required: true, description: "The entityId" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -3368,8 +2795,8 @@ export const entityGraphEndpoints: Endpoint[] = [ tag: "Entity Graph", }, { - method: "GET", - path: "/entity-graph/views", + method: 'GET', + path: '/entity-graph/views', summary: "Get Views", requiresAuth: true, responses: [ @@ -3380,8 +2807,8 @@ export const entityGraphEndpoints: Endpoint[] = [ tag: "Entity Graph", }, { - method: "POST", - path: "/entity-graph/views", + method: 'POST', + path: '/entity-graph/views', summary: "Create View", requiresAuth: true, responses: [ @@ -3392,18 +2819,12 @@ export const entityGraphEndpoints: Endpoint[] = [ tag: "Entity Graph", }, { - method: "GET", - path: "/entity-graph/views/{id}", + method: 'GET', + path: '/entity-graph/views/{id}', summary: "Get View By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -3413,18 +2834,12 @@ export const entityGraphEndpoints: Endpoint[] = [ tag: "Entity Graph", }, { - method: "PUT", - path: "/entity-graph/views/{id}", + method: 'PUT', + path: '/entity-graph/views/{id}', summary: "Update View", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -3434,18 +2849,12 @@ export const entityGraphEndpoints: Endpoint[] = [ tag: "Entity Graph", }, { - method: "DELETE", - path: "/entity-graph/views/{id}", + method: 'DELETE', + path: '/entity-graph/views/{id}', summary: "Delete View", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -3455,8 +2864,8 @@ export const entityGraphEndpoints: Endpoint[] = [ tag: "Entity Graph", }, { - method: "GET", - path: "/entity-graph/gap-rules/defaults", + method: 'GET', + path: '/entity-graph/gap-rules/defaults', summary: "Get Default Gap Rules", requiresAuth: false, responses: [ @@ -3466,8 +2875,8 @@ export const entityGraphEndpoints: Endpoint[] = [ tag: "Entity Graph", }, { - method: "GET", - path: "/entity-graph/gap-rules", + method: 'GET', + path: '/entity-graph/gap-rules', summary: "Get Gap Rules", requiresAuth: true, responses: [ @@ -3478,8 +2887,8 @@ export const entityGraphEndpoints: Endpoint[] = [ tag: "Entity Graph", }, { - method: "POST", - path: "/entity-graph/gap-rules", + method: 'POST', + path: '/entity-graph/gap-rules', summary: "Save Gap Rules", requiresAuth: true, responses: [ @@ -3490,8 +2899,8 @@ export const entityGraphEndpoints: Endpoint[] = [ tag: "Entity Graph", }, { - method: "DELETE", - path: "/entity-graph/gap-rules", + method: 'DELETE', + path: '/entity-graph/gap-rules', summary: "Reset Gap Rules", requiresAuth: true, responses: [ @@ -3506,8 +2915,8 @@ export const entityGraphEndpoints: Endpoint[] = [ // EU AI Act endpoints export const euAiActEndpoints: Endpoint[] = [ { - method: "GET", - path: "/eu-ai-act/controlCategories", + method: 'GET', + path: '/eu-ai-act/controlCategories', summary: "Get All Control Categories", requiresAuth: true, responses: [ @@ -3518,18 +2927,12 @@ export const euAiActEndpoints: Endpoint[] = [ tag: "EU AI Act", }, { - method: "GET", - path: "/eu-ai-act/controls/byControlCategoryId/{id}", + method: 'GET', + path: '/eu-ai-act/controls/byControlCategoryId/{id}', summary: "Get Controls By Control Category Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -3539,8 +2942,8 @@ export const euAiActEndpoints: Endpoint[] = [ tag: "EU AI Act", }, { - method: "GET", - path: "/eu-ai-act/topics", + method: 'GET', + path: '/eu-ai-act/topics', summary: "Get All Topics", requiresAuth: true, responses: [ @@ -3551,18 +2954,12 @@ export const euAiActEndpoints: Endpoint[] = [ tag: "EU AI Act", }, { - method: "GET", - path: "/eu-ai-act/assessments/byProjectId/{id}", + method: 'GET', + path: '/eu-ai-act/assessments/byProjectId/{id}', summary: "Get Assessments By Project Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -3572,18 +2969,12 @@ export const euAiActEndpoints: Endpoint[] = [ tag: "EU AI Act", }, { - method: "DELETE", - path: "/eu-ai-act/assessments/byProjectId/{id}", + method: 'DELETE', + path: '/eu-ai-act/assessments/byProjectId/{id}', summary: "Delete Assessments By Project Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -3593,18 +2984,12 @@ export const euAiActEndpoints: Endpoint[] = [ tag: "EU AI Act", }, { - method: "GET", - path: "/eu-ai-act/compliances/byProjectId/{id}", + method: 'GET', + path: '/eu-ai-act/compliances/byProjectId/{id}', summary: "Get Compliances By Project Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -3614,18 +2999,12 @@ export const euAiActEndpoints: Endpoint[] = [ tag: "EU AI Act", }, { - method: "DELETE", - path: "/eu-ai-act/compliances/byProjectId/{id}", + method: 'DELETE', + path: '/eu-ai-act/compliances/byProjectId/{id}', summary: "Delete Compliances By Project Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -3635,18 +3014,12 @@ export const euAiActEndpoints: Endpoint[] = [ tag: "EU AI Act", }, { - method: "GET", - path: "/eu-ai-act/compliances/progress/{id}", + method: 'GET', + path: '/eu-ai-act/compliances/progress/{id}', summary: "Get Project Compliance Progress", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -3656,18 +3029,12 @@ export const euAiActEndpoints: Endpoint[] = [ tag: "EU AI Act", }, { - method: "GET", - path: "/eu-ai-act/assessments/progress/{id}", + method: 'GET', + path: '/eu-ai-act/assessments/progress/{id}', summary: "Get Project Assessment Progress", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -3677,8 +3044,8 @@ export const euAiActEndpoints: Endpoint[] = [ tag: "EU AI Act", }, { - method: "GET", - path: "/eu-ai-act/all/compliances/progress", + method: 'GET', + path: '/eu-ai-act/all/compliances/progress', summary: "Get All Projects Compliance Progress", requiresAuth: true, responses: [ @@ -3689,8 +3056,8 @@ export const euAiActEndpoints: Endpoint[] = [ tag: "EU AI Act", }, { - method: "GET", - path: "/eu-ai-act/all/assessments/progress", + method: 'GET', + path: '/eu-ai-act/all/assessments/progress', summary: "Get All Projects Assessment Progress", requiresAuth: true, responses: [ @@ -3701,8 +3068,8 @@ export const euAiActEndpoints: Endpoint[] = [ tag: "EU AI Act", }, { - method: "GET", - path: "/eu-ai-act/topicById", + method: 'GET', + path: '/eu-ai-act/topicById', summary: "Get Topic By Id", requiresAuth: true, responses: [ @@ -3713,8 +3080,8 @@ export const euAiActEndpoints: Endpoint[] = [ tag: "EU AI Act", }, { - method: "GET", - path: "/eu-ai-act/controlById", + method: 'GET', + path: '/eu-ai-act/controlById', summary: "Get Control By Id", requiresAuth: true, responses: [ @@ -3725,18 +3092,12 @@ export const euAiActEndpoints: Endpoint[] = [ tag: "EU AI Act", }, { - method: "PATCH", - path: "/eu-ai-act/saveControls/{id}", + method: 'PATCH', + path: '/eu-ai-act/saveControls/{id}', summary: "Save Controls", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -3746,18 +3107,12 @@ export const euAiActEndpoints: Endpoint[] = [ tag: "EU AI Act", }, { - method: "PATCH", - path: "/eu-ai-act/saveAnswer/{id}", + method: 'PATCH', + path: '/eu-ai-act/saveAnswer/{id}', summary: "Update Question By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -3771,8 +3126,8 @@ export const euAiActEndpoints: Endpoint[] = [ // Evidence AI endpoints export const evidenceAiEndpoints: Endpoint[] = [ { - method: "POST", - path: "/evidence-ai/analyze/{fileId}", + method: 'POST', + path: '/evidence-ai/analyze/{fileId}', summary: "Analyze File", requiresAuth: true, responses: [ @@ -3782,8 +3137,8 @@ export const evidenceAiEndpoints: Endpoint[] = [ tag: "Evidence AI", }, { - method: "GET", - path: "/evidence-ai/analysis/{fileId}", + method: 'GET', + path: '/evidence-ai/analysis/{fileId}', summary: "Get Analysis", requiresAuth: true, responses: [ @@ -3793,8 +3148,8 @@ export const evidenceAiEndpoints: Endpoint[] = [ tag: "Evidence AI", }, { - method: "GET", - path: "/evidence-ai/quality-scores", + method: 'GET', + path: '/evidence-ai/quality-scores', summary: "Get Quality Scores", requiresAuth: true, responses: [ @@ -3804,8 +3159,8 @@ export const evidenceAiEndpoints: Endpoint[] = [ tag: "Evidence AI", }, { - method: "GET", - path: "/evidence-ai/gaps", + method: 'GET', + path: '/evidence-ai/gaps', summary: "Get Gaps", requiresAuth: true, responses: [ @@ -3815,8 +3170,8 @@ export const evidenceAiEndpoints: Endpoint[] = [ tag: "Evidence AI", }, { - method: "GET", - path: "/evidence-ai/suggestions/{fileId}", + method: 'GET', + path: '/evidence-ai/suggestions/{fileId}', summary: "Get Suggestions", requiresAuth: true, responses: [ @@ -3826,8 +3181,8 @@ export const evidenceAiEndpoints: Endpoint[] = [ tag: "Evidence AI", }, { - method: "POST", - path: "/evidence-ai/suggestions/{fileId}/apply", + method: 'POST', + path: '/evidence-ai/suggestions/{fileId}/apply', summary: "Apply Suggestions", requiresAuth: true, responses: [ @@ -3841,8 +3196,8 @@ export const evidenceAiEndpoints: Endpoint[] = [ // Evidence endpoints export const evidenceHubEndpoints: Endpoint[] = [ { - method: "GET", - path: "/evidenceHub", + method: 'GET', + path: '/evidenceHub', summary: "Get All Evidences", requiresAuth: true, responses: [ @@ -3853,8 +3208,8 @@ export const evidenceHubEndpoints: Endpoint[] = [ tag: "Evidence", }, { - method: "POST", - path: "/evidenceHub", + method: 'POST', + path: '/evidenceHub', summary: "Create New Evidence", requiresAuth: true, responses: [ @@ -3865,18 +3220,12 @@ export const evidenceHubEndpoints: Endpoint[] = [ tag: "Evidence", }, { - method: "GET", - path: "/evidenceHub/{id}", + method: 'GET', + path: '/evidenceHub/{id}', summary: "Get Evidence By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -3886,18 +3235,12 @@ export const evidenceHubEndpoints: Endpoint[] = [ tag: "Evidence", }, { - method: "PATCH", - path: "/evidenceHub/{id}", + method: 'PATCH', + path: '/evidenceHub/{id}', summary: "Update Evidence By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -3907,18 +3250,12 @@ export const evidenceHubEndpoints: Endpoint[] = [ tag: "Evidence", }, { - method: "DELETE", - path: "/evidenceHub/{id}", + method: 'DELETE', + path: '/evidenceHub/{id}', summary: "Delete Evidence By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -3932,8 +3269,8 @@ export const evidenceHubEndpoints: Endpoint[] = [ // Files endpoints export const fileEndpoints: Endpoint[] = [ { - method: "GET", - path: "/files", + method: 'GET', + path: '/files', summary: "Get User Files Meta Data", requiresAuth: true, responses: [ @@ -3944,8 +3281,8 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "POST", - path: "/files", + method: 'POST', + path: '/files', summary: "Post File Content", requiresAuth: true, responses: [ @@ -3956,18 +3293,12 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "GET", - path: "/files/by-projid/{id}", + method: 'GET', + path: '/files/by-projid/{id}', summary: "Get File Meta By Project Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -3977,32 +3308,14 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "GET", - path: "/files/entity/{framework_type}/{entity_type}/{entity_id}", + method: 'GET', + path: '/files/entity/{framework_type}/{entity_type}/{entity_id}', summary: "Get Entity Files", requiresAuth: true, parameters: [ - { - name: "framework_type", - in: "path", - type: "string", - required: true, - description: "The framework_type", - }, - { - name: "entity_type", - in: "path", - type: "string", - required: true, - description: "The entity_type", - }, - { - name: "entity_id", - in: "path", - type: "integer", - required: true, - description: "The entity_id", - }, + { name: 'framework_type', in: 'path', type: 'string', required: true, description: "The framework_type" }, + { name: 'entity_type', in: 'path', type: 'string', required: true, description: "The entity_type" }, + { name: 'entity_id', in: 'path', type: 'integer', required: true, description: "The entity_id" }, ], responses: [ { status: 200, description: "Success" }, @@ -4012,8 +3325,8 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "POST", - path: "/files/attach", + method: 'POST', + path: '/files/attach', summary: "Attach File To Entity", requiresAuth: true, responses: [ @@ -4024,8 +3337,8 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "POST", - path: "/files/attach-bulk", + method: 'POST', + path: '/files/attach-bulk', summary: "Attach Files To Entity", requiresAuth: true, responses: [ @@ -4036,8 +3349,8 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "DELETE", - path: "/files/detach", + method: 'DELETE', + path: '/files/detach', summary: "Detach File From Entity", requiresAuth: true, responses: [ @@ -4048,8 +3361,8 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "PATCH", - path: "/files/bulk-tags", + method: 'PATCH', + path: '/files/bulk-tags', summary: "Bulk Update File Tags", description: "Requires role: Admin or Editor", requiresAuth: true, @@ -4060,19 +3373,13 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "GET", - path: "/files/{id}", + method: 'GET', + path: '/files/{id}', summary: "Get File Content By Id", description: "Requires role: Admin", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -4083,18 +3390,12 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "PATCH", - path: "/files/{id}", + method: 'PATCH', + path: '/files/{id}', summary: "Update Folder", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -4104,18 +3405,12 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "DELETE", - path: "/files/{id}", + method: 'DELETE', + path: '/files/{id}', summary: "Delete Folder", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -4125,8 +3420,8 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "GET", - path: "/file-manager", + method: 'GET', + path: '/file-manager', summary: "List Files", requiresAuth: true, responses: [ @@ -4137,8 +3432,8 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "POST", - path: "/file-manager", + method: 'POST', + path: '/file-manager', summary: "Upload File", description: "Requires role: Admin or Reviewer or Editor", requiresAuth: true, @@ -4151,8 +3446,8 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "GET", - path: "/file-manager/search", + method: 'GET', + path: '/file-manager/search', summary: "Search Files", requiresAuth: true, responses: [ @@ -4163,8 +3458,8 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "GET", - path: "/file-manager/with-metadata", + method: 'GET', + path: '/file-manager/with-metadata', summary: "List Files With Metadata", requiresAuth: true, responses: [ @@ -4175,19 +3470,13 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "GET", - path: "/file-manager/{id}", + method: 'GET', + path: '/file-manager/{id}', summary: "Download File", description: "Requires role: Admin", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -4198,19 +3487,13 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "DELETE", - path: "/file-manager/{id}", + method: 'DELETE', + path: '/file-manager/{id}', summary: "Remove File", description: "Requires role: Admin or Reviewer or Editor", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -4221,18 +3504,12 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "GET", - path: "/file-manager/{id}/metadata", + method: 'GET', + path: '/file-manager/{id}/metadata', summary: "Get File Metadata", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -4242,19 +3519,13 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "PATCH", - path: "/file-manager/{id}/metadata", + method: 'PATCH', + path: '/file-manager/{id}/metadata', summary: "Update Metadata", description: "Requires role: Admin or Reviewer or Editor", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -4265,18 +3536,12 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "GET", - path: "/file-manager/{id}/versions", + method: 'GET', + path: '/file-manager/{id}/versions', summary: "Get File Version History", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -4286,18 +3551,12 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "GET", - path: "/file-manager/{id}/preview", + method: 'GET', + path: '/file-manager/{id}/preview', summary: "Preview File", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -4307,8 +3566,8 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "GET", - path: "/virtual-folders", + method: 'GET', + path: '/virtual-folders', summary: "Get All Folders", requiresAuth: true, responses: [ @@ -4319,8 +3578,8 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "POST", - path: "/virtual-folders", + method: 'POST', + path: '/virtual-folders', summary: "Create Folder", requiresAuth: true, responses: [ @@ -4331,8 +3590,8 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "GET", - path: "/virtual-folders/tree", + method: 'GET', + path: '/virtual-folders/tree', summary: "Get Folder Tree", requiresAuth: true, responses: [ @@ -4343,8 +3602,8 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "GET", - path: "/virtual-folders/uncategorized", + method: 'GET', + path: '/virtual-folders/uncategorized', summary: "Get Uncategorized Files", requiresAuth: true, responses: [ @@ -4355,18 +3614,12 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "GET", - path: "/virtual-folders/{id}", + method: 'GET', + path: '/virtual-folders/{id}', summary: "Get Folder By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -4376,18 +3629,12 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "PATCH", - path: "/virtual-folders/{id}", + method: 'PATCH', + path: '/virtual-folders/{id}', summary: "Update Folder", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -4397,18 +3644,12 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "DELETE", - path: "/virtual-folders/{id}", + method: 'DELETE', + path: '/virtual-folders/{id}', summary: "Delete Folder", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -4418,18 +3659,12 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "GET", - path: "/virtual-folders/{id}/path", + method: 'GET', + path: '/virtual-folders/{id}/path', summary: "Get Folder Path", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -4439,18 +3674,12 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "GET", - path: "/virtual-folders/{id}/files", + method: 'GET', + path: '/virtual-folders/{id}/files', summary: "Get Files In Folder", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -4460,18 +3689,12 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "POST", - path: "/virtual-folders/{id}/files", + method: 'POST', + path: '/virtual-folders/{id}/files', summary: "Assign Files To Folder", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -4481,25 +3704,13 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "DELETE", - path: "/virtual-folders/{id}/files/{fileId}", + method: 'DELETE', + path: '/virtual-folders/{id}/files/{fileId}', summary: "Remove File From Folder", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, - { - name: "fileId", - in: "path", - type: "integer", - required: true, - description: "The fileId", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, + { name: 'fileId', in: 'path', type: 'integer', required: true, description: "The fileId" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -4509,8 +3720,8 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "GET", - path: "/files/tree", + method: 'GET', + path: '/files/tree', summary: "Get Folder Tree", requiresAuth: true, responses: [ @@ -4521,8 +3732,8 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "GET", - path: "/files/uncategorized", + method: 'GET', + path: '/files/uncategorized', summary: "Get Uncategorized Files", requiresAuth: true, responses: [ @@ -4533,18 +3744,12 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "GET", - path: "/files/{id}/path", + method: 'GET', + path: '/files/{id}/path', summary: "Get Folder Path", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -4554,18 +3759,12 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "GET", - path: "/files/{id}/files", + method: 'GET', + path: '/files/{id}/files', summary: "Get Files In Folder", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -4575,18 +3774,12 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "POST", - path: "/files/{id}/files", + method: 'POST', + path: '/files/{id}/files', summary: "Assign Files To Folder", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -4596,25 +3789,13 @@ export const fileEndpoints: Endpoint[] = [ tag: "Files", }, { - method: "DELETE", - path: "/files/{id}/files/{fileId}", + method: 'DELETE', + path: '/files/{id}/files/{fileId}', summary: "Remove File From Folder", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, - { - name: "fileId", - in: "path", - type: "integer", - required: true, - description: "The fileId", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, + { name: 'fileId', in: 'path', type: 'integer', required: true, description: "The fileId" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -4628,8 +3809,8 @@ export const fileEndpoints: Endpoint[] = [ // Frameworks endpoints export const frameworkEndpoints: Endpoint[] = [ { - method: "GET", - path: "/frameworks", + method: 'GET', + path: '/frameworks', summary: "Get All Frameworks", requiresAuth: true, responses: [ @@ -4640,18 +3821,12 @@ export const frameworkEndpoints: Endpoint[] = [ tag: "Frameworks", }, { - method: "GET", - path: "/frameworks/{id}", + method: 'GET', + path: '/frameworks/{id}', summary: "Get Framework By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -4661,8 +3836,8 @@ export const frameworkEndpoints: Endpoint[] = [ tag: "Frameworks", }, { - method: "POST", - path: "/frameworks/toProject", + method: 'POST', + path: '/frameworks/toProject', summary: "Add Framework To Project", requiresAuth: true, responses: [ @@ -4673,8 +3848,8 @@ export const frameworkEndpoints: Endpoint[] = [ tag: "Frameworks", }, { - method: "DELETE", - path: "/frameworks/fromProject", + method: 'DELETE', + path: '/frameworks/fromProject', summary: "Delete Framework From Project", requiresAuth: true, responses: [ @@ -4689,19 +3864,13 @@ export const frameworkEndpoints: Endpoint[] = [ // FRIA endpoints export const friaEndpoints: Endpoint[] = [ { - method: "PUT", - path: "/fria/{friaId}/rights", + method: 'PUT', + path: '/fria/{friaId}/rights', summary: "Update Fria Rights", description: "Requires role: Admin or Editor", requiresAuth: true, parameters: [ - { - name: "friaId", - in: "path", - type: "integer", - required: true, - description: "The friaId", - }, + { name: 'friaId', in: 'path', type: 'integer', required: true, description: "The friaId" }, ], responses: [ { status: 200, description: "Success" }, @@ -4712,18 +3881,12 @@ export const friaEndpoints: Endpoint[] = [ tag: "FRIA", }, { - method: "GET", - path: "/fria/{friaId}/risk-items", + method: 'GET', + path: '/fria/{friaId}/risk-items', summary: "Get Risk Items", requiresAuth: true, parameters: [ - { - name: "friaId", - in: "path", - type: "integer", - required: true, - description: "The friaId", - }, + { name: 'friaId', in: 'path', type: 'integer', required: true, description: "The friaId" }, ], responses: [ { status: 200, description: "Success" }, @@ -4733,19 +3896,13 @@ export const friaEndpoints: Endpoint[] = [ tag: "FRIA", }, { - method: "POST", - path: "/fria/{friaId}/risk-items", + method: 'POST', + path: '/fria/{friaId}/risk-items', summary: "Add Risk Item", description: "Requires role: Admin or Editor", requiresAuth: true, parameters: [ - { - name: "friaId", - in: "path", - type: "integer", - required: true, - description: "The friaId", - }, + { name: 'friaId', in: 'path', type: 'integer', required: true, description: "The friaId" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -4756,26 +3913,14 @@ export const friaEndpoints: Endpoint[] = [ tag: "FRIA", }, { - method: "PATCH", - path: "/fria/{friaId}/risk-items/{itemId}", + method: 'PATCH', + path: '/fria/{friaId}/risk-items/{itemId}', summary: "Update Risk Item", description: "Requires role: Admin or Editor", requiresAuth: true, parameters: [ - { - name: "friaId", - in: "path", - type: "integer", - required: true, - description: "The friaId", - }, - { - name: "itemId", - in: "path", - type: "integer", - required: true, - description: "The itemId", - }, + { name: 'friaId', in: 'path', type: 'integer', required: true, description: "The friaId" }, + { name: 'itemId', in: 'path', type: 'integer', required: true, description: "The itemId" }, ], responses: [ { status: 200, description: "Success" }, @@ -4786,26 +3931,14 @@ export const friaEndpoints: Endpoint[] = [ tag: "FRIA", }, { - method: "DELETE", - path: "/fria/{friaId}/risk-items/{itemId}", + method: 'DELETE', + path: '/fria/{friaId}/risk-items/{itemId}', summary: "Delete Risk Item", description: "Requires role: Admin or Editor", requiresAuth: true, parameters: [ - { - name: "friaId", - in: "path", - type: "integer", - required: true, - description: "The friaId", - }, - { - name: "itemId", - in: "path", - type: "integer", - required: true, - description: "The itemId", - }, + { name: 'friaId', in: 'path', type: 'integer', required: true, description: "The friaId" }, + { name: 'itemId', in: 'path', type: 'integer', required: true, description: "The itemId" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -4816,18 +3949,12 @@ export const friaEndpoints: Endpoint[] = [ tag: "FRIA", }, { - method: "GET", - path: "/fria/{friaId}/models", + method: 'GET', + path: '/fria/{friaId}/models', summary: "Get Model Links", requiresAuth: true, parameters: [ - { - name: "friaId", - in: "path", - type: "integer", - required: true, - description: "The friaId", - }, + { name: 'friaId', in: 'path', type: 'integer', required: true, description: "The friaId" }, ], responses: [ { status: 200, description: "Success" }, @@ -4837,26 +3964,14 @@ export const friaEndpoints: Endpoint[] = [ tag: "FRIA", }, { - method: "POST", - path: "/fria/{friaId}/models/{modelId}", + method: 'POST', + path: '/fria/{friaId}/models/{modelId}', summary: "Link Model", description: "Requires role: Admin or Editor", requiresAuth: true, parameters: [ - { - name: "friaId", - in: "path", - type: "integer", - required: true, - description: "The friaId", - }, - { - name: "modelId", - in: "path", - type: "integer", - required: true, - description: "The modelId", - }, + { name: 'friaId', in: 'path', type: 'integer', required: true, description: "The friaId" }, + { name: 'modelId', in: 'path', type: 'integer', required: true, description: "The modelId" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -4867,26 +3982,14 @@ export const friaEndpoints: Endpoint[] = [ tag: "FRIA", }, { - method: "DELETE", - path: "/fria/{friaId}/models/{modelId}", + method: 'DELETE', + path: '/fria/{friaId}/models/{modelId}', summary: "Unlink Model", description: "Requires role: Admin or Editor", requiresAuth: true, parameters: [ - { - name: "friaId", - in: "path", - type: "integer", - required: true, - description: "The friaId", - }, - { - name: "modelId", - in: "path", - type: "integer", - required: true, - description: "The modelId", - }, + { name: 'friaId', in: 'path', type: 'integer', required: true, description: "The friaId" }, + { name: 'modelId', in: 'path', type: 'integer', required: true, description: "The modelId" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -4897,18 +4000,12 @@ export const friaEndpoints: Endpoint[] = [ tag: "FRIA", }, { - method: "GET", - path: "/fria/{friaId}/evidence", + method: 'GET', + path: '/fria/{friaId}/evidence', summary: "Get Fria Evidence", requiresAuth: true, parameters: [ - { - name: "friaId", - in: "path", - type: "integer", - required: true, - description: "The friaId", - }, + { name: 'friaId', in: 'path', type: 'integer', required: true, description: "The friaId" }, ], responses: [ { status: 200, description: "Success" }, @@ -4918,19 +4015,13 @@ export const friaEndpoints: Endpoint[] = [ tag: "FRIA", }, { - method: "POST", - path: "/fria/{friaId}/evidence", + method: 'POST', + path: '/fria/{friaId}/evidence', summary: "Link Fria Evidence", description: "Requires role: Admin or Editor", requiresAuth: true, parameters: [ - { - name: "friaId", - in: "path", - type: "integer", - required: true, - description: "The friaId", - }, + { name: 'friaId', in: 'path', type: 'integer', required: true, description: "The friaId" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -4941,26 +4032,14 @@ export const friaEndpoints: Endpoint[] = [ tag: "FRIA", }, { - method: "DELETE", - path: "/fria/{friaId}/evidence/{linkId}", + method: 'DELETE', + path: '/fria/{friaId}/evidence/{linkId}', summary: "Unlink Fria Evidence", description: "Requires role: Admin or Editor", requiresAuth: true, parameters: [ - { - name: "friaId", - in: "path", - type: "integer", - required: true, - description: "The friaId", - }, - { - name: "linkId", - in: "path", - type: "integer", - required: true, - description: "The linkId", - }, + { name: 'friaId', in: 'path', type: 'integer', required: true, description: "The friaId" }, + { name: 'linkId', in: 'path', type: 'integer', required: true, description: "The linkId" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -4971,19 +4050,13 @@ export const friaEndpoints: Endpoint[] = [ tag: "FRIA", }, { - method: "POST", - path: "/fria/{friaId}/submit", + method: 'POST', + path: '/fria/{friaId}/submit', summary: "Submit Fria", description: "Requires role: Admin or Editor", requiresAuth: true, parameters: [ - { - name: "friaId", - in: "path", - type: "integer", - required: true, - description: "The friaId", - }, + { name: 'friaId', in: 'path', type: 'integer', required: true, description: "The friaId" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -4994,18 +4067,12 @@ export const friaEndpoints: Endpoint[] = [ tag: "FRIA", }, { - method: "GET", - path: "/fria/{friaId}/versions", + method: 'GET', + path: '/fria/{friaId}/versions', summary: "Get Versions", requiresAuth: true, parameters: [ - { - name: "friaId", - in: "path", - type: "integer", - required: true, - description: "The friaId", - }, + { name: 'friaId', in: 'path', type: 'integer', required: true, description: "The friaId" }, ], responses: [ { status: 200, description: "Success" }, @@ -5015,25 +4082,13 @@ export const friaEndpoints: Endpoint[] = [ tag: "FRIA", }, { - method: "GET", - path: "/fria/{friaId}/versions/{version}", + method: 'GET', + path: '/fria/{friaId}/versions/{version}', summary: "Get Version", requiresAuth: true, parameters: [ - { - name: "friaId", - in: "path", - type: "integer", - required: true, - description: "The friaId", - }, - { - name: "version", - in: "path", - type: "integer", - required: true, - description: "The version", - }, + { name: 'friaId', in: 'path', type: 'integer', required: true, description: "The friaId" }, + { name: 'version', in: 'path', type: 'integer', required: true, description: "The version" }, ], responses: [ { status: 200, description: "Success" }, @@ -5043,18 +4098,12 @@ export const friaEndpoints: Endpoint[] = [ tag: "FRIA", }, { - method: "GET", - path: "/fria/{projectId}", + method: 'GET', + path: '/fria/{projectId}', summary: "Get Fria", requiresAuth: true, parameters: [ - { - name: "projectId", - in: "path", - type: "integer", - required: true, - description: "The projectId", - }, + { name: 'projectId', in: 'path', type: 'integer', required: true, description: "The projectId" }, ], responses: [ { status: 200, description: "Success" }, @@ -5064,19 +4113,13 @@ export const friaEndpoints: Endpoint[] = [ tag: "FRIA", }, { - method: "PUT", - path: "/fria/{projectId}", + method: 'PUT', + path: '/fria/{projectId}', summary: "Update Fria", description: "Requires role: Admin or Editor", requiresAuth: true, parameters: [ - { - name: "projectId", - in: "path", - type: "integer", - required: true, - description: "The projectId", - }, + { name: 'projectId', in: 'path', type: 'integer', required: true, description: "The projectId" }, ], responses: [ { status: 200, description: "Success" }, @@ -5091,8 +4134,8 @@ export const friaEndpoints: Endpoint[] = [ // Governance OS endpoints export const governanceOsEndpoints: Endpoint[] = [ { - method: "GET", - path: "/governance-os/mappings", + method: 'GET', + path: '/governance-os/mappings', summary: "Get All Mappings", requiresAuth: true, responses: [ @@ -5102,8 +4145,8 @@ export const governanceOsEndpoints: Endpoint[] = [ tag: "Governance OS", }, { - method: "POST", - path: "/governance-os/mappings", + method: 'POST', + path: '/governance-os/mappings', summary: "Create Mapping", description: "Requires role: Admin or Editor", requiresAuth: true, @@ -5114,8 +4157,8 @@ export const governanceOsEndpoints: Endpoint[] = [ tag: "Governance OS", }, { - method: "GET", - path: "/governance-os/mappings/between/{sourceId}/{targetId}", + method: 'GET', + path: '/governance-os/mappings/between/{sourceId}/{targetId}', summary: "Get Mappings Between", requiresAuth: true, responses: [ @@ -5125,8 +4168,8 @@ export const governanceOsEndpoints: Endpoint[] = [ tag: "Governance OS", }, { - method: "GET", - path: "/governance-os/mappings/control/{controlType}/{controlId}", + method: 'GET', + path: '/governance-os/mappings/control/{controlType}/{controlId}', summary: "Get Mappings For Control", requiresAuth: true, responses: [ @@ -5136,8 +4179,8 @@ export const governanceOsEndpoints: Endpoint[] = [ tag: "Governance OS", }, { - method: "PUT", - path: "/governance-os/mappings/{id}", + method: 'PUT', + path: '/governance-os/mappings/{id}', summary: "Update Mapping", description: "Requires role: Admin or Editor", requiresAuth: true, @@ -5148,8 +4191,8 @@ export const governanceOsEndpoints: Endpoint[] = [ tag: "Governance OS", }, { - method: "DELETE", - path: "/governance-os/mappings/{id}", + method: 'DELETE', + path: '/governance-os/mappings/{id}', summary: "Delete Mapping", description: "Requires role: Admin", requiresAuth: true, @@ -5160,8 +4203,8 @@ export const governanceOsEndpoints: Endpoint[] = [ tag: "Governance OS", }, { - method: "POST", - path: "/governance-os/mappings/bulk", + method: 'POST', + path: '/governance-os/mappings/bulk', summary: "Create Bulk Mappings", description: "Requires role: Admin or Editor", requiresAuth: true, @@ -5172,8 +4215,8 @@ export const governanceOsEndpoints: Endpoint[] = [ tag: "Governance OS", }, { - method: "GET", - path: "/governance-os/scenarios", + method: 'GET', + path: '/governance-os/scenarios', summary: "Get All Scenarios", requiresAuth: true, responses: [ @@ -5183,8 +4226,8 @@ export const governanceOsEndpoints: Endpoint[] = [ tag: "Governance OS", }, { - method: "POST", - path: "/governance-os/scenarios", + method: 'POST', + path: '/governance-os/scenarios', summary: "Create Scenario", description: "Requires role: Admin or Editor", requiresAuth: true, @@ -5195,8 +4238,8 @@ export const governanceOsEndpoints: Endpoint[] = [ tag: "Governance OS", }, { - method: "GET", - path: "/governance-os/scenarios/{id}", + method: 'GET', + path: '/governance-os/scenarios/{id}', summary: "Get Scenario By Id", requiresAuth: true, responses: [ @@ -5206,8 +4249,8 @@ export const governanceOsEndpoints: Endpoint[] = [ tag: "Governance OS", }, { - method: "PUT", - path: "/governance-os/scenarios/{id}", + method: 'PUT', + path: '/governance-os/scenarios/{id}', summary: "Update Scenario", description: "Requires role: Admin or Editor", requiresAuth: true, @@ -5218,8 +4261,8 @@ export const governanceOsEndpoints: Endpoint[] = [ tag: "Governance OS", }, { - method: "DELETE", - path: "/governance-os/scenarios/{id}", + method: 'DELETE', + path: '/governance-os/scenarios/{id}', summary: "Delete Scenario", description: "Requires role: Admin", requiresAuth: true, @@ -5230,8 +4273,8 @@ export const governanceOsEndpoints: Endpoint[] = [ tag: "Governance OS", }, { - method: "POST", - path: "/governance-os/scenarios/{id}/activate", + method: 'POST', + path: '/governance-os/scenarios/{id}/activate', summary: "Activate Scenario", description: "Requires role: Admin or Editor", requiresAuth: true, @@ -5242,8 +4285,8 @@ export const governanceOsEndpoints: Endpoint[] = [ tag: "Governance OS", }, { - method: "POST", - path: "/governance-os/scenarios/simulate", + method: 'POST', + path: '/governance-os/scenarios/simulate', summary: "Simulate Scenario", requiresAuth: true, responses: [ @@ -5253,8 +4296,8 @@ export const governanceOsEndpoints: Endpoint[] = [ tag: "Governance OS", }, { - method: "GET", - path: "/governance-os/activations", + method: 'GET', + path: '/governance-os/activations', summary: "Get Activation History", requiresAuth: true, responses: [ @@ -5264,8 +4307,8 @@ export const governanceOsEndpoints: Endpoint[] = [ tag: "Governance OS", }, { - method: "POST", - path: "/governance-os/activations/{id}/deactivate", + method: 'POST', + path: '/governance-os/activations/{id}/deactivate', summary: "Deactivate Scenario", description: "Requires role: Admin or Editor", requiresAuth: true, @@ -5276,8 +4319,8 @@ export const governanceOsEndpoints: Endpoint[] = [ tag: "Governance OS", }, { - method: "GET", - path: "/governance-os/activations/{id}/progress", + method: 'GET', + path: '/governance-os/activations/{id}/progress', summary: "Get Scenario Progress", requiresAuth: true, responses: [ @@ -5287,8 +4330,8 @@ export const governanceOsEndpoints: Endpoint[] = [ tag: "Governance OS", }, { - method: "POST", - path: "/governance-os/recommend", + method: 'POST', + path: '/governance-os/recommend', summary: "Get Recommendations", requiresAuth: true, responses: [ @@ -5298,8 +4341,8 @@ export const governanceOsEndpoints: Endpoint[] = [ tag: "Governance OS", }, { - method: "GET", - path: "/governance-os/coverage/{projectId}", + method: 'GET', + path: '/governance-os/coverage/{projectId}', summary: "Get Coverage", requiresAuth: true, responses: [ @@ -5309,8 +4352,8 @@ export const governanceOsEndpoints: Endpoint[] = [ tag: "Governance OS", }, { - method: "POST", - path: "/governance-os/coverage/{projectId}/refresh", + method: 'POST', + path: '/governance-os/coverage/{projectId}/refresh', summary: "Refresh Coverage", description: "Requires role: Admin or Editor", requiresAuth: true, @@ -5321,8 +4364,8 @@ export const governanceOsEndpoints: Endpoint[] = [ tag: "Governance OS", }, { - method: "GET", - path: "/governance-os/unified-view/{projectId}", + method: 'GET', + path: '/governance-os/unified-view/{projectId}', summary: "Get Unified View", requiresAuth: true, responses: [ @@ -5332,8 +4375,8 @@ export const governanceOsEndpoints: Endpoint[] = [ tag: "Governance OS", }, { - method: "GET", - path: "/governance-os/eligibility", + method: 'GET', + path: '/governance-os/eligibility', summary: "Get Eligibility", requiresAuth: true, responses: [ @@ -5343,8 +4386,8 @@ export const governanceOsEndpoints: Endpoint[] = [ tag: "Governance OS", }, { - method: "GET", - path: "/governance-os/preferences", + method: 'GET', + path: '/governance-os/preferences', summary: "Get Preferences", requiresAuth: true, responses: [ @@ -5354,8 +4397,8 @@ export const governanceOsEndpoints: Endpoint[] = [ tag: "Governance OS", }, { - method: "PUT", - path: "/governance-os/preferences", + method: 'PUT', + path: '/governance-os/preferences', summary: "Update Preferences", description: "Requires role: Admin", requiresAuth: true, @@ -5370,8 +4413,8 @@ export const governanceOsEndpoints: Endpoint[] = [ // Intake Forms endpoints export const intakeFormEndpoints: Endpoint[] = [ { - method: "GET", - path: "/intake/forms", + method: 'GET', + path: '/intake/forms', summary: "Get All Intake Forms", requiresAuth: true, responses: [ @@ -5382,8 +4425,8 @@ export const intakeFormEndpoints: Endpoint[] = [ tag: "Intake Forms", }, { - method: "POST", - path: "/intake/forms", + method: 'POST', + path: '/intake/forms', summary: "Create Intake Form", requiresAuth: true, responses: [ @@ -5394,18 +4437,12 @@ export const intakeFormEndpoints: Endpoint[] = [ tag: "Intake Forms", }, { - method: "GET", - path: "/intake/forms/{id}", + method: 'GET', + path: '/intake/forms/{id}', summary: "Get Intake Form By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -5415,18 +4452,12 @@ export const intakeFormEndpoints: Endpoint[] = [ tag: "Intake Forms", }, { - method: "PATCH", - path: "/intake/forms/{id}", + method: 'PATCH', + path: '/intake/forms/{id}', summary: "Update Intake Form", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -5436,18 +4467,12 @@ export const intakeFormEndpoints: Endpoint[] = [ tag: "Intake Forms", }, { - method: "DELETE", - path: "/intake/forms/{id}", + method: 'DELETE', + path: '/intake/forms/{id}', summary: "Delete Intake Form", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -5457,18 +4482,12 @@ export const intakeFormEndpoints: Endpoint[] = [ tag: "Intake Forms", }, { - method: "POST", - path: "/intake/forms/{id}/archive", + method: 'POST', + path: '/intake/forms/{id}/archive', summary: "Archive Intake Form", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -5478,18 +4497,12 @@ export const intakeFormEndpoints: Endpoint[] = [ tag: "Intake Forms", }, { - method: "GET", - path: "/intake/forms/{id}/preview", + method: 'GET', + path: '/intake/forms/{id}/preview', summary: "Preview Form", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -5499,8 +4512,8 @@ export const intakeFormEndpoints: Endpoint[] = [ tag: "Intake Forms", }, { - method: "POST", - path: "/intake/forms/suggested-questions", + method: 'POST', + path: '/intake/forms/suggested-questions', summary: "Get L L M Suggested Questions", requiresAuth: true, responses: [ @@ -5511,8 +4524,8 @@ export const intakeFormEndpoints: Endpoint[] = [ tag: "Intake Forms", }, { - method: "POST", - path: "/intake/forms/field-guidance", + method: 'POST', + path: '/intake/forms/field-guidance', summary: "Get Field Guidance", requiresAuth: true, responses: [ @@ -5523,8 +4536,8 @@ export const intakeFormEndpoints: Endpoint[] = [ tag: "Intake Forms", }, { - method: "GET", - path: "/intake/submissions", + method: 'GET', + path: '/intake/submissions', summary: "Get Pending Submissions", requiresAuth: true, responses: [ @@ -5535,8 +4548,8 @@ export const intakeFormEndpoints: Endpoint[] = [ tag: "Intake Forms", }, { - method: "GET", - path: "/intake/submissions/stats", + method: 'GET', + path: '/intake/submissions/stats', summary: "Get Submission Stats", requiresAuth: true, responses: [ @@ -5547,25 +4560,13 @@ export const intakeFormEndpoints: Endpoint[] = [ tag: "Intake Forms", }, { - method: "GET", - path: "/intake/submissions/by-entity/{entityType}/{entityId}", + method: 'GET', + path: '/intake/submissions/by-entity/{entityType}/{entityId}', summary: "Get Submission By Entity", requiresAuth: true, parameters: [ - { - name: "entityType", - in: "path", - type: "string", - required: true, - description: "The entityType", - }, - { - name: "entityId", - in: "path", - type: "integer", - required: true, - description: "The entityId", - }, + { name: 'entityType', in: 'path', type: 'string', required: true, description: "The entityType" }, + { name: 'entityId', in: 'path', type: 'integer', required: true, description: "The entityId" }, ], responses: [ { status: 200, description: "Success" }, @@ -5575,18 +4576,12 @@ export const intakeFormEndpoints: Endpoint[] = [ tag: "Intake Forms", }, { - method: "GET", - path: "/intake/submissions/{id}", + method: 'GET', + path: '/intake/submissions/{id}', summary: "Get Submission By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -5596,18 +4591,12 @@ export const intakeFormEndpoints: Endpoint[] = [ tag: "Intake Forms", }, { - method: "GET", - path: "/intake/submissions/{id}/preview", + method: 'GET', + path: '/intake/submissions/{id}/preview', summary: "Get Submission Preview", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -5617,18 +4606,12 @@ export const intakeFormEndpoints: Endpoint[] = [ tag: "Intake Forms", }, { - method: "PATCH", - path: "/intake/submissions/{id}/risk-override", + method: 'PATCH', + path: '/intake/submissions/{id}/risk-override', summary: "Override Submission Risk", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -5638,18 +4621,12 @@ export const intakeFormEndpoints: Endpoint[] = [ tag: "Intake Forms", }, { - method: "GET", - path: "/intake/forms/{id}/submissions", + method: 'GET', + path: '/intake/forms/{id}/submissions', summary: "Get Form Submissions", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -5659,18 +4636,12 @@ export const intakeFormEndpoints: Endpoint[] = [ tag: "Intake Forms", }, { - method: "POST", - path: "/intake/submissions/{id}/approve", + method: 'POST', + path: '/intake/submissions/{id}/approve', summary: "Approve Submission", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -5680,18 +4651,12 @@ export const intakeFormEndpoints: Endpoint[] = [ tag: "Intake Forms", }, { - method: "POST", - path: "/intake/submissions/{id}/reject", + method: 'POST', + path: '/intake/submissions/{id}/reject', summary: "Reject Submission", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -5701,8 +4666,8 @@ export const intakeFormEndpoints: Endpoint[] = [ tag: "Intake Forms", }, { - method: "GET", - path: "/intake/public/captcha", + method: 'GET', + path: '/intake/public/captcha', summary: "Get Captcha", requiresAuth: false, responses: [ @@ -5712,18 +4677,12 @@ export const intakeFormEndpoints: Endpoint[] = [ tag: "Intake Forms", }, { - method: "GET", - path: "/intake/public/by-id/{publicId}", + method: 'GET', + path: '/intake/public/by-id/{publicId}', summary: "Get Public Form By Public Id", requiresAuth: false, parameters: [ - { - name: "publicId", - in: "path", - type: "integer", - required: true, - description: "The publicId", - }, + { name: 'publicId', in: 'path', type: 'integer', required: true, description: "The publicId" }, ], responses: [ { status: 200, description: "Success" }, @@ -5732,18 +4691,12 @@ export const intakeFormEndpoints: Endpoint[] = [ tag: "Intake Forms", }, { - method: "POST", - path: "/intake/public/by-id/{publicId}", + method: 'POST', + path: '/intake/public/by-id/{publicId}', summary: "Submit Public Form By Public Id", requiresAuth: false, parameters: [ - { - name: "publicId", - in: "path", - type: "integer", - required: true, - description: "The publicId", - }, + { name: 'publicId', in: 'path', type: 'integer', required: true, description: "The publicId" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -5752,25 +4705,13 @@ export const intakeFormEndpoints: Endpoint[] = [ tag: "Intake Forms", }, { - method: "GET", - path: "/intake/public/{tenantSlug}/{formSlug}", + method: 'GET', + path: '/intake/public/{tenantSlug}/{formSlug}', summary: "Get Public Form", requiresAuth: false, parameters: [ - { - name: "tenantSlug", - in: "path", - type: "string", - required: true, - description: "The tenantSlug", - }, - { - name: "formSlug", - in: "path", - type: "string", - required: true, - description: "The formSlug", - }, + { name: 'tenantSlug', in: 'path', type: 'string', required: true, description: "The tenantSlug" }, + { name: 'formSlug', in: 'path', type: 'string', required: true, description: "The formSlug" }, ], responses: [ { status: 200, description: "Success" }, @@ -5779,25 +4720,13 @@ export const intakeFormEndpoints: Endpoint[] = [ tag: "Intake Forms", }, { - method: "POST", - path: "/intake/public/{tenantSlug}/{formSlug}", + method: 'POST', + path: '/intake/public/{tenantSlug}/{formSlug}', summary: "Submit Public Form", requiresAuth: false, parameters: [ - { - name: "tenantSlug", - in: "path", - type: "string", - required: true, - description: "The tenantSlug", - }, - { - name: "formSlug", - in: "path", - type: "string", - required: true, - description: "The formSlug", - }, + { name: 'tenantSlug', in: 'path', type: 'string', required: true, description: "The tenantSlug" }, + { name: 'formSlug', in: 'path', type: 'string', required: true, description: "The formSlug" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -5810,8 +4739,8 @@ export const intakeFormEndpoints: Endpoint[] = [ // Integrations endpoints export const integrationEndpoints: Endpoint[] = [ { - method: "GET", - path: "/slackWebhooks", + method: 'GET', + path: '/slackWebhooks', summary: "Get All Slack Webhooks", requiresAuth: true, responses: [ @@ -5822,8 +4751,8 @@ export const integrationEndpoints: Endpoint[] = [ tag: "Integrations", }, { - method: "POST", - path: "/slackWebhooks", + method: 'POST', + path: '/slackWebhooks', summary: "Create New Slack Webhook", requiresAuth: true, responses: [ @@ -5834,18 +4763,12 @@ export const integrationEndpoints: Endpoint[] = [ tag: "Integrations", }, { - method: "GET", - path: "/slackWebhooks/{id}", + method: 'GET', + path: '/slackWebhooks/{id}', summary: "Get Slack Webhook By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -5855,18 +4778,12 @@ export const integrationEndpoints: Endpoint[] = [ tag: "Integrations", }, { - method: "PATCH", - path: "/slackWebhooks/{id}", + method: 'PATCH', + path: '/slackWebhooks/{id}', summary: "Update Slack Webhook By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -5876,18 +4793,12 @@ export const integrationEndpoints: Endpoint[] = [ tag: "Integrations", }, { - method: "DELETE", - path: "/slackWebhooks/{id}", + method: 'DELETE', + path: '/slackWebhooks/{id}', summary: "Delete Slack Webhook By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -5897,18 +4808,12 @@ export const integrationEndpoints: Endpoint[] = [ tag: "Integrations", }, { - method: "POST", - path: "/slackWebhooks/{id}/send", + method: 'POST', + path: '/slackWebhooks/{id}/send', summary: "Send Slack Message", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -5918,8 +4823,8 @@ export const integrationEndpoints: Endpoint[] = [ tag: "Integrations", }, { - method: "GET", - path: "/integrations/github/token", + method: 'GET', + path: '/integrations/github/token', summary: "Get Git Hub Token Status Controller", requiresAuth: true, responses: [ @@ -5930,8 +4835,8 @@ export const integrationEndpoints: Endpoint[] = [ tag: "Integrations", }, { - method: "POST", - path: "/integrations/github/token", + method: 'POST', + path: '/integrations/github/token', summary: "Save Git Hub Token Controller", requiresAuth: true, responses: [ @@ -5942,8 +4847,8 @@ export const integrationEndpoints: Endpoint[] = [ tag: "Integrations", }, { - method: "DELETE", - path: "/integrations/github/token", + method: 'DELETE', + path: '/integrations/github/token', summary: "Delete Git Hub Token Controller", requiresAuth: true, responses: [ @@ -5954,8 +4859,8 @@ export const integrationEndpoints: Endpoint[] = [ tag: "Integrations", }, { - method: "POST", - path: "/integrations/github/token/test", + method: 'POST', + path: '/integrations/github/token/test', summary: "Test Git Hub Token Controller", requiresAuth: true, responses: [ @@ -5970,8 +4875,8 @@ export const integrationEndpoints: Endpoint[] = [ // Internal endpoints export const internalEndpoints: Endpoint[] = [ { - method: "POST", - path: "/internal/ai-gateway/notify", + method: 'POST', + path: '/internal/ai-gateway/notify', summary: "AI Gateway notification callback", requiresAuth: false, responses: [ @@ -5985,8 +4890,8 @@ export const internalEndpoints: Endpoint[] = [ // Invitations endpoints export const invitationEndpoints: Endpoint[] = [ { - method: "GET", - path: "/invitations", + method: 'GET', + path: '/invitations', summary: "Get Invitations", description: "Requires role: Admin or SuperAdmin", requiresAuth: true, @@ -5999,19 +4904,13 @@ export const invitationEndpoints: Endpoint[] = [ tag: "Invitations", }, { - method: "DELETE", - path: "/invitations/{id}", + method: 'DELETE', + path: '/invitations/{id}', summary: "Revoke Invitation", description: "Requires role: Admin or SuperAdmin", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -6022,19 +4921,13 @@ export const invitationEndpoints: Endpoint[] = [ tag: "Invitations", }, { - method: "POST", - path: "/invitations/{id}/resend", + method: 'POST', + path: '/invitations/{id}/resend', summary: "Resend Invitation", description: "Requires role: Admin or SuperAdmin", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -6049,8 +4942,8 @@ export const invitationEndpoints: Endpoint[] = [ // ISO 27001 endpoints export const iso27001Endpoints: Endpoint[] = [ { - method: "GET", - path: "/iso-27001/clauses", + method: 'GET', + path: '/iso-27001/clauses', summary: "Get All Clauses", requiresAuth: true, responses: [ @@ -6061,18 +4954,12 @@ export const iso27001Endpoints: Endpoint[] = [ tag: "ISO 27001", }, { - method: "GET", - path: "/iso-27001/clauses/struct/byProjectId/{id}", + method: 'GET', + path: '/iso-27001/clauses/struct/byProjectId/{id}', summary: "Get All Clauses Struct For Project", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6082,8 +4969,8 @@ export const iso27001Endpoints: Endpoint[] = [ tag: "ISO 27001", }, { - method: "GET", - path: "/iso-27001/annexes", + method: 'GET', + path: '/iso-27001/annexes', summary: "Get All Annexes", requiresAuth: true, responses: [ @@ -6094,18 +4981,12 @@ export const iso27001Endpoints: Endpoint[] = [ tag: "ISO 27001", }, { - method: "GET", - path: "/iso-27001/annexes/struct/byProjectId/{id}", + method: 'GET', + path: '/iso-27001/annexes/struct/byProjectId/{id}', summary: "Get All Annexes Struct For Project", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6115,18 +4996,12 @@ export const iso27001Endpoints: Endpoint[] = [ tag: "ISO 27001", }, { - method: "GET", - path: "/iso-27001/clauses/byProjectId/{id}", + method: 'GET', + path: '/iso-27001/clauses/byProjectId/{id}', summary: "Get Clauses By Project Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6136,18 +5011,12 @@ export const iso27001Endpoints: Endpoint[] = [ tag: "ISO 27001", }, { - method: "DELETE", - path: "/iso-27001/clauses/byProjectId/{id}", + method: 'DELETE', + path: '/iso-27001/clauses/byProjectId/{id}', summary: "Delete Management System Clauses", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -6157,18 +5026,12 @@ export const iso27001Endpoints: Endpoint[] = [ tag: "ISO 27001", }, { - method: "GET", - path: "/iso-27001/annexes/byProjectId/{id}", + method: 'GET', + path: '/iso-27001/annexes/byProjectId/{id}', summary: "Get Annexes By Project Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6178,18 +5041,12 @@ export const iso27001Endpoints: Endpoint[] = [ tag: "ISO 27001", }, { - method: "DELETE", - path: "/iso-27001/annexes/byProjectId/{id}", + method: 'DELETE', + path: '/iso-27001/annexes/byProjectId/{id}', summary: "Delete Reference Controls", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -6199,18 +5056,12 @@ export const iso27001Endpoints: Endpoint[] = [ tag: "ISO 27001", }, { - method: "GET", - path: "/iso-27001/subClauses/byClauseId/{id}", + method: 'GET', + path: '/iso-27001/subClauses/byClauseId/{id}', summary: "Get Sub Clauses By Clause Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6220,18 +5071,12 @@ export const iso27001Endpoints: Endpoint[] = [ tag: "ISO 27001", }, { - method: "GET", - path: "/iso-27001/annexControls/byAnnexId/{id}", + method: 'GET', + path: '/iso-27001/annexControls/byAnnexId/{id}', summary: "Get Annex Controls By Annex Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6241,18 +5086,12 @@ export const iso27001Endpoints: Endpoint[] = [ tag: "ISO 27001", }, { - method: "GET", - path: "/iso-27001/subClause/byId/{id}", + method: 'GET', + path: '/iso-27001/subClause/byId/{id}', summary: "Get Sub Clause By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6262,18 +5101,12 @@ export const iso27001Endpoints: Endpoint[] = [ tag: "ISO 27001", }, { - method: "GET", - path: "/iso-27001/annexControl/byId/{id}", + method: 'GET', + path: '/iso-27001/annexControl/byId/{id}', summary: "Get Annex Control By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6283,18 +5116,12 @@ export const iso27001Endpoints: Endpoint[] = [ tag: "ISO 27001", }, { - method: "GET", - path: "/iso-27001/clauses/progress/{id}", + method: 'GET', + path: '/iso-27001/clauses/progress/{id}', summary: "Get Project Clauses Progress", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6304,18 +5131,12 @@ export const iso27001Endpoints: Endpoint[] = [ tag: "ISO 27001", }, { - method: "GET", - path: "/iso-27001/annexes/progress/{id}", + method: 'GET', + path: '/iso-27001/annexes/progress/{id}', summary: "Get Project Annxes Progress", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6325,8 +5146,8 @@ export const iso27001Endpoints: Endpoint[] = [ tag: "ISO 27001", }, { - method: "GET", - path: "/iso-27001/all/clauses/progress", + method: 'GET', + path: '/iso-27001/all/clauses/progress', summary: "Get All Projects Clauses Progress", requiresAuth: true, responses: [ @@ -6337,8 +5158,8 @@ export const iso27001Endpoints: Endpoint[] = [ tag: "ISO 27001", }, { - method: "GET", - path: "/iso-27001/all/annexes/progress", + method: 'GET', + path: '/iso-27001/all/annexes/progress', summary: "Get All Projects Annxes Progress", requiresAuth: true, responses: [ @@ -6349,18 +5170,12 @@ export const iso27001Endpoints: Endpoint[] = [ tag: "ISO 27001", }, { - method: "GET", - path: "/iso-27001/clauses/assignments/{id}", + method: 'GET', + path: '/iso-27001/clauses/assignments/{id}', summary: "Get Project Clauses Assignments", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6370,18 +5185,12 @@ export const iso27001Endpoints: Endpoint[] = [ tag: "ISO 27001", }, { - method: "GET", - path: "/iso-27001/annexes/assignments/{id}", + method: 'GET', + path: '/iso-27001/annexes/assignments/{id}', summary: "Get Project Annexes Assignments", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6391,18 +5200,12 @@ export const iso27001Endpoints: Endpoint[] = [ tag: "ISO 27001", }, { - method: "PATCH", - path: "/iso-27001/saveClauses/{id}", + method: 'PATCH', + path: '/iso-27001/saveClauses/{id}', summary: "Save Clauses", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6412,18 +5215,12 @@ export const iso27001Endpoints: Endpoint[] = [ tag: "ISO 27001", }, { - method: "PATCH", - path: "/iso-27001/saveAnnexes/{id}", + method: 'PATCH', + path: '/iso-27001/saveAnnexes/{id}', summary: "Save Annexes", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6437,8 +5234,8 @@ export const iso27001Endpoints: Endpoint[] = [ // ISO 42001 endpoints export const iso42001Endpoints: Endpoint[] = [ { - method: "GET", - path: "/iso-42001/clauses", + method: 'GET', + path: '/iso-42001/clauses', summary: "Get All Clauses", requiresAuth: true, responses: [ @@ -6449,18 +5246,12 @@ export const iso42001Endpoints: Endpoint[] = [ tag: "ISO 42001", }, { - method: "GET", - path: "/iso-42001/clauses/struct/byProjectId/{id}", + method: 'GET', + path: '/iso-42001/clauses/struct/byProjectId/{id}', summary: "Get All Clauses Struct For Project", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6470,8 +5261,8 @@ export const iso42001Endpoints: Endpoint[] = [ tag: "ISO 42001", }, { - method: "GET", - path: "/iso-42001/annexes", + method: 'GET', + path: '/iso-42001/annexes', summary: "Get All Annexes", requiresAuth: true, responses: [ @@ -6482,18 +5273,12 @@ export const iso42001Endpoints: Endpoint[] = [ tag: "ISO 42001", }, { - method: "GET", - path: "/iso-42001/annexes/struct/byProjectId/{id}", + method: 'GET', + path: '/iso-42001/annexes/struct/byProjectId/{id}', summary: "Get All Annexes Struct For Project", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6503,18 +5288,12 @@ export const iso42001Endpoints: Endpoint[] = [ tag: "ISO 42001", }, { - method: "GET", - path: "/iso-42001/clauses/byProjectId/{id}", + method: 'GET', + path: '/iso-42001/clauses/byProjectId/{id}', summary: "Get Clauses By Project Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6524,18 +5303,12 @@ export const iso42001Endpoints: Endpoint[] = [ tag: "ISO 42001", }, { - method: "DELETE", - path: "/iso-42001/clauses/byProjectId/{id}", + method: 'DELETE', + path: '/iso-42001/clauses/byProjectId/{id}', summary: "Delete Management System Clauses", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -6545,18 +5318,12 @@ export const iso42001Endpoints: Endpoint[] = [ tag: "ISO 42001", }, { - method: "GET", - path: "/iso-42001/annexes/byProjectId/{id}", + method: 'GET', + path: '/iso-42001/annexes/byProjectId/{id}', summary: "Get Annexes By Project Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6566,18 +5333,12 @@ export const iso42001Endpoints: Endpoint[] = [ tag: "ISO 42001", }, { - method: "DELETE", - path: "/iso-42001/annexes/byProjectId/{id}", + method: 'DELETE', + path: '/iso-42001/annexes/byProjectId/{id}', summary: "Delete Reference Controls", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -6587,18 +5348,12 @@ export const iso42001Endpoints: Endpoint[] = [ tag: "ISO 42001", }, { - method: "GET", - path: "/iso-42001/subClauses/byClauseId/{id}", + method: 'GET', + path: '/iso-42001/subClauses/byClauseId/{id}', summary: "Get Sub Clauses By Clause Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6608,18 +5363,12 @@ export const iso42001Endpoints: Endpoint[] = [ tag: "ISO 42001", }, { - method: "GET", - path: "/iso-42001/annexCategories/byAnnexId/{id}", + method: 'GET', + path: '/iso-42001/annexCategories/byAnnexId/{id}', summary: "Get Annex Categories By Annex Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6629,18 +5378,12 @@ export const iso42001Endpoints: Endpoint[] = [ tag: "ISO 42001", }, { - method: "GET", - path: "/iso-42001/subClause/byId/{id}", + method: 'GET', + path: '/iso-42001/subClause/byId/{id}', summary: "Get Sub Clause By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6650,18 +5393,12 @@ export const iso42001Endpoints: Endpoint[] = [ tag: "ISO 42001", }, { - method: "GET", - path: "/iso-42001/subclauses/{id}/risks", + method: 'GET', + path: '/iso-42001/subclauses/{id}/risks', summary: "Get Sub Clause Risks", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6671,18 +5408,12 @@ export const iso42001Endpoints: Endpoint[] = [ tag: "ISO 42001", }, { - method: "GET", - path: "/iso-42001/annexCategories/{id}/risks", + method: 'GET', + path: '/iso-42001/annexCategories/{id}/risks', summary: "Get Annex Category Risks", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6692,18 +5423,12 @@ export const iso42001Endpoints: Endpoint[] = [ tag: "ISO 42001", }, { - method: "GET", - path: "/iso-42001/annexCategory/byId/{id}", + method: 'GET', + path: '/iso-42001/annexCategory/byId/{id}', summary: "Get Annex Category By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6713,18 +5438,12 @@ export const iso42001Endpoints: Endpoint[] = [ tag: "ISO 42001", }, { - method: "GET", - path: "/iso-42001/clauses/progress/{id}", + method: 'GET', + path: '/iso-42001/clauses/progress/{id}', summary: "Get Project Clauses Progress", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6734,18 +5453,12 @@ export const iso42001Endpoints: Endpoint[] = [ tag: "ISO 42001", }, { - method: "GET", - path: "/iso-42001/annexes/progress/{id}", + method: 'GET', + path: '/iso-42001/annexes/progress/{id}', summary: "Get Project Annxes Progress", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6755,8 +5468,8 @@ export const iso42001Endpoints: Endpoint[] = [ tag: "ISO 42001", }, { - method: "GET", - path: "/iso-42001/all/clauses/progress", + method: 'GET', + path: '/iso-42001/all/clauses/progress', summary: "Get All Projects Clauses Progress", requiresAuth: true, responses: [ @@ -6767,8 +5480,8 @@ export const iso42001Endpoints: Endpoint[] = [ tag: "ISO 42001", }, { - method: "GET", - path: "/iso-42001/all/annexes/progress", + method: 'GET', + path: '/iso-42001/all/annexes/progress', summary: "Get All Projects Annxes Progress", requiresAuth: true, responses: [ @@ -6779,18 +5492,12 @@ export const iso42001Endpoints: Endpoint[] = [ tag: "ISO 42001", }, { - method: "GET", - path: "/iso-42001/clauses/assignments/{id}", + method: 'GET', + path: '/iso-42001/clauses/assignments/{id}', summary: "Get Project Clauses Assignments", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6800,18 +5507,12 @@ export const iso42001Endpoints: Endpoint[] = [ tag: "ISO 42001", }, { - method: "GET", - path: "/iso-42001/annexes/assignments/{id}", + method: 'GET', + path: '/iso-42001/annexes/assignments/{id}', summary: "Get Project Annexes Assignments", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6821,18 +5522,12 @@ export const iso42001Endpoints: Endpoint[] = [ tag: "ISO 42001", }, { - method: "PATCH", - path: "/iso-42001/saveClauses/{id}", + method: 'PATCH', + path: '/iso-42001/saveClauses/{id}', summary: "Save Clauses", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6842,18 +5537,12 @@ export const iso42001Endpoints: Endpoint[] = [ tag: "ISO 42001", }, { - method: "PATCH", - path: "/iso-42001/saveAnnexes/{id}", + method: 'PATCH', + path: '/iso-42001/saveAnnexes/{id}', summary: "Save Annexes", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -6867,8 +5556,8 @@ export const iso42001Endpoints: Endpoint[] = [ // LLM Evals endpoints export const llmEvalsEndpoints: Endpoint[] = [ { - method: "POST", - path: "/deepeval/playground/chat", + method: 'POST', + path: '/deepeval/playground/chat', summary: "Provider", requiresAuth: true, responses: [ @@ -6878,8 +5567,8 @@ export const llmEvalsEndpoints: Endpoint[] = [ tag: "LLM Evals", }, { - method: "GET", - path: "/evaluation-llm-keys", + method: 'GET', + path: '/evaluation-llm-keys', summary: "Get All Evaluation LLM Keys", requiresAuth: true, responses: [ @@ -6890,15 +5579,14 @@ export const llmEvalsEndpoints: Endpoint[] = [ tag: "LLM Evals", }, { - method: "POST", - path: "/evaluation-llm-keys", + method: 'POST', + path: '/evaluation-llm-keys', summary: "Add Evaluation LLM Key", description: "Requires role: Admin", requiresAuth: true, requestBody: { - provider: - "openai | anthropic | google | xai | mistral | huggingface (required)", - apiKey: "string (required)", + "provider": "openai | anthropic | google | xai | mistral | huggingface (required)", + "apiKey": "string (required)", }, responses: [ { status: 201, description: "Created successfully" }, @@ -6909,15 +5597,14 @@ export const llmEvalsEndpoints: Endpoint[] = [ tag: "LLM Evals", }, { - method: "POST", - path: "/evaluation-llm-keys/verify", + method: 'POST', + path: '/evaluation-llm-keys/verify', summary: "Verify Evaluation LLM Key", description: "Requires role: Admin", requiresAuth: true, requestBody: { - provider: - "openai | anthropic | google | xai | mistral | huggingface | openrouter (required)", - apiKey: "string (required)", + "provider": "openai | anthropic | google | xai | mistral | huggingface | openrouter (required)", + "apiKey": "string (required)", }, responses: [ { status: 200, description: "Success" }, @@ -6928,19 +5615,13 @@ export const llmEvalsEndpoints: Endpoint[] = [ tag: "LLM Evals", }, { - method: "DELETE", - path: "/evaluation-llm-keys/{provider}", + method: 'DELETE', + path: '/evaluation-llm-keys/{provider}', summary: "Delete Evaluation LLM Key", description: "Requires role: Admin", requiresAuth: true, parameters: [ - { - name: "provider", - in: "path", - type: "string", - required: true, - description: "The provider", - }, + { name: 'provider', in: 'path', type: 'string', required: true, description: "The provider" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -6951,8 +5632,8 @@ export const llmEvalsEndpoints: Endpoint[] = [ tag: "LLM Evals", }, { - method: "GET", - path: "/evaluation-llm-keys/internal/decrypted", + method: 'GET', + path: '/evaluation-llm-keys/internal/decrypted', summary: "Eval Keys Retired", requiresAuth: false, responses: [ @@ -6966,8 +5647,8 @@ export const llmEvalsEndpoints: Endpoint[] = [ // LLM Keys endpoints export const llmKeyEndpoints: Endpoint[] = [ { - method: "GET", - path: "/llm-keys", + method: 'GET', + path: '/llm-keys', summary: "Get L L M Keys", requiresAuth: true, responses: [ @@ -6978,8 +5659,8 @@ export const llmKeyEndpoints: Endpoint[] = [ tag: "LLM Keys", }, { - method: "POST", - path: "/llm-keys", + method: 'POST', + path: '/llm-keys', summary: "Create L L M Key", requiresAuth: true, responses: [ @@ -6990,8 +5671,8 @@ export const llmKeyEndpoints: Endpoint[] = [ tag: "LLM Keys", }, { - method: "GET", - path: "/llm-keys/status", + method: 'GET', + path: '/llm-keys/status', summary: "Get L L M Key Status", requiresAuth: true, responses: [ @@ -7002,18 +5683,12 @@ export const llmKeyEndpoints: Endpoint[] = [ tag: "LLM Keys", }, { - method: "GET", - path: "/llm-keys/{name}", + method: 'GET', + path: '/llm-keys/{name}', summary: "Get L L M Key", requiresAuth: true, parameters: [ - { - name: "name", - in: "path", - type: "string", - required: true, - description: "The name", - }, + { name: 'name', in: 'path', type: 'string', required: true, description: "The name" }, ], responses: [ { status: 200, description: "Success" }, @@ -7023,18 +5698,12 @@ export const llmKeyEndpoints: Endpoint[] = [ tag: "LLM Keys", }, { - method: "PATCH", - path: "/llm-keys/{id}", + method: 'PATCH', + path: '/llm-keys/{id}', summary: "Update L L M Key", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -7044,18 +5713,12 @@ export const llmKeyEndpoints: Endpoint[] = [ tag: "LLM Keys", }, { - method: "DELETE", - path: "/llm-keys/{id}", + method: 'DELETE', + path: '/llm-keys/{id}', summary: "Delete L L M Key", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -7069,11 +5732,10 @@ export const llmKeyEndpoints: Endpoint[] = [ // Model Inventory endpoints export const modelInventoryEndpoints: Endpoint[] = [ { - method: "GET", - path: "/modelInventory", + method: 'GET', + path: '/modelInventory', summary: "Get all model inventories", - description: - "Returns every model inventory record belonging to the caller's organization, ordered by created_at DESC, id ASC. Each record includes its associated project and framework IDs.", + description: "Returns every model inventory record belonging to the caller's organization, ordered by created_at DESC, id ASC. Each record includes its associated project and framework IDs.", requiresAuth: true, responses: [ { status: 200, description: "List of model inventories (may be empty)" }, @@ -7083,11 +5745,10 @@ export const modelInventoryEndpoints: Endpoint[] = [ tag: "Model Inventory", }, { - method: "POST", - path: "/modelInventory", + method: 'POST', + path: '/modelInventory', summary: "Create a new model inventory", - description: - 'Creates a model inventory record, links it to the supplied project and framework IDs, records a change-history entry, fires any "model_added" automations, and notifies the approver (if set).', + description: "Creates a model inventory record, links it to the supplied project and framework IDs, records a change-history entry, fires any \"model_added\" automations, and notifies the approver (if set).", requiresAuth: true, requestBody: { "(schema)": "ModelInventoryCreateRequest", @@ -7100,8 +5761,8 @@ export const modelInventoryEndpoints: Endpoint[] = [ tag: "Model Inventory", }, { - method: "GET", - path: "/modelInventory/evaluations", + method: 'GET', + path: '/modelInventory/evaluations', summary: "Get All Model Evaluations", requiresAuth: true, responses: [ @@ -7112,18 +5773,12 @@ export const modelInventoryEndpoints: Endpoint[] = [ tag: "Model Inventory", }, { - method: "GET", - path: "/modelInventory/{id}/evaluations", + method: 'GET', + path: '/modelInventory/{id}/evaluations', summary: "Get Model Evaluations", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -7133,20 +5788,13 @@ export const modelInventoryEndpoints: Endpoint[] = [ tag: "Model Inventory", }, { - method: "GET", - path: "/modelInventory/{id}", + method: 'GET', + path: '/modelInventory/{id}', summary: "Get a model inventory by ID", - description: - "Returns a single model inventory record with its associated project and framework IDs. Returns 204 if the record does not exist.", + description: "Returns a single model inventory record with its associated project and framework IDs. Returns 204 if the record does not exist.", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "Model inventory ID", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "Model inventory ID" }, ], responses: [ { status: 200, description: "Model inventory found" }, @@ -7157,20 +5805,13 @@ export const modelInventoryEndpoints: Endpoint[] = [ tag: "Model Inventory", }, { - method: "PATCH", - path: "/modelInventory/{id}", + method: 'PATCH', + path: '/modelInventory/{id}', summary: "Update a model inventory by ID", - description: - 'Partially updates a model inventory record. All body fields are optional; only provided fields are changed. Project and framework associations can be replaced or cleared. Fires "model_updated" automations and notifies a new approver if changed.', + description: "Partially updates a model inventory record. All body fields are optional; only provided fields are changed. Project and framework associations can be replaced or cleared. Fires \"model_updated\" automations and notifies a new approver if changed.", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "Model inventory ID", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "Model inventory ID" }, ], requestBody: { "(schema)": "ModelInventoryUpdateRequest", @@ -7184,28 +5825,14 @@ export const modelInventoryEndpoints: Endpoint[] = [ tag: "Model Inventory", }, { - method: "DELETE", - path: "/modelInventory/{id}", + method: 'DELETE', + path: '/modelInventory/{id}', summary: "Delete a model inventory by ID", - description: - 'Deletes a model inventory record and its project/framework associations. Optionally deletes linked model risks when deleteRisks=true. Records deletion in change history and fires "model_deleted" automations.', - requiresAuth: true, - parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "Model inventory ID", - }, - { - name: "deleteRisks", - in: "query", - type: "string", - required: false, - description: - 'When "true", also deletes associated rows from the model_risks table.\n', - }, + description: "Deletes a model inventory record and its project/framework associations. Optionally deletes linked model risks when deleteRisks=true. Records deletion in change history and fires \"model_deleted\" automations.", + requiresAuth: true, + parameters: [ + { name: 'id', in: 'path', type: 'integer', required: true, description: "Model inventory ID" }, + { name: 'deleteRisks', in: 'query', type: 'string', required: false, description: "When \"true\", also deletes associated rows from the model_risks table.\n" }, ], responses: [ { status: 200, description: "Model inventory deleted" }, @@ -7216,62 +5843,40 @@ export const modelInventoryEndpoints: Endpoint[] = [ tag: "Model Inventory", }, { - method: "GET", - path: "/modelInventory/by-projectId/{projectId}", + method: 'GET', + path: '/modelInventory/by-projectId/{projectId}', summary: "Get model inventories by project ID", - description: - "Returns all model inventories associated with a project (via the model_inventories_projects_frameworks join table where framework_id IS NULL). Non-numeric project IDs (e.g. plugin-sourced) return an empty array.", + description: "Returns all model inventories associated with a project (via the model_inventories_projects_frameworks join table where framework_id IS NULL). Non-numeric project IDs (e.g. plugin-sourced) return an empty array.", requiresAuth: true, parameters: [ - { - name: "projectId", - in: "path", - type: "integer", - required: true, - description: - "Project ID (integer). Non-numeric values return an empty array.", - }, + { name: 'projectId', in: 'path', type: 'integer', required: true, description: "Project ID (integer). Non-numeric values return an empty array." }, ], responses: [ - { - status: 200, - description: "List of model inventories for the project (may be empty)", - }, + { status: 200, description: "List of model inventories for the project (may be empty)" }, { status: 401, description: "Missing or invalid JWT" }, { status: 500, description: "Internal server error" }, ], tag: "Model Inventory", }, { - method: "GET", - path: "/modelInventory/by-frameworkId/{frameworkId}", + method: 'GET', + path: '/modelInventory/by-frameworkId/{frameworkId}', summary: "Get model inventories by framework ID", - description: - "Returns all model inventories associated with a framework (via the model_inventories_projects_frameworks join table where framework_id matches).", + description: "Returns all model inventories associated with a framework (via the model_inventories_projects_frameworks join table where framework_id matches).", requiresAuth: true, parameters: [ - { - name: "frameworkId", - in: "path", - type: "integer", - required: true, - description: "Framework ID", - }, + { name: 'frameworkId', in: 'path', type: 'integer', required: true, description: "Framework ID" }, ], responses: [ - { - status: 200, - description: - "List of model inventories for the framework (may be empty)", - }, + { status: 200, description: "List of model inventories for the framework (may be empty)" }, { status: 401, description: "Missing or invalid JWT" }, { status: 500, description: "Internal server error" }, ], tag: "Model Inventory", }, { - method: "GET", - path: "/modelInventoryHistory/timeseries", + method: 'GET', + path: '/modelInventoryHistory/timeseries', summary: "Get Timeseries", requiresAuth: true, responses: [ @@ -7282,8 +5887,8 @@ export const modelInventoryEndpoints: Endpoint[] = [ tag: "Model Inventory", }, { - method: "GET", - path: "/modelInventoryHistory/current-counts", + method: 'GET', + path: '/modelInventoryHistory/current-counts', summary: "Get Current Counts", requiresAuth: true, responses: [ @@ -7294,8 +5899,8 @@ export const modelInventoryEndpoints: Endpoint[] = [ tag: "Model Inventory", }, { - method: "POST", - path: "/modelInventoryHistory/snapshot", + method: 'POST', + path: '/modelInventoryHistory/snapshot', summary: "Create Snapshot", requiresAuth: true, responses: [ @@ -7310,18 +5915,12 @@ export const modelInventoryEndpoints: Endpoint[] = [ // Model Risks endpoints export const modelRiskEndpoints: Endpoint[] = [ { - method: "GET", - path: "/modelRisks", + method: 'GET', + path: '/modelRisks', summary: "Get All Model Risks", requiresAuth: true, parameters: [ - { - name: "filter", - in: "query", - type: "string", - required: false, - description: "The filter", - }, + { name: 'filter', in: 'query', type: 'string', required: false, description: "The filter" }, ], responses: [ { status: 200, description: "Success" }, @@ -7331,8 +5930,8 @@ export const modelRiskEndpoints: Endpoint[] = [ tag: "Model Risks", }, { - method: "POST", - path: "/modelRisks", + method: 'POST', + path: '/modelRisks', summary: "Create New Model Risk", requiresAuth: true, requestBody: { @@ -7346,18 +5945,12 @@ export const modelRiskEndpoints: Endpoint[] = [ tag: "Model Risks", }, { - method: "GET", - path: "/modelRisks/{id}", + method: 'GET', + path: '/modelRisks/{id}', summary: "Get Model Risk By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -7367,18 +5960,12 @@ export const modelRiskEndpoints: Endpoint[] = [ tag: "Model Risks", }, { - method: "PUT", - path: "/modelRisks/{id}", + method: 'PUT', + path: '/modelRisks/{id}', summary: "Update Model Risk By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], requestBody: { "(schema)": "ModelRiskInput", @@ -7391,18 +5978,12 @@ export const modelRiskEndpoints: Endpoint[] = [ tag: "Model Risks", }, { - method: "PATCH", - path: "/modelRisks/{id}", + method: 'PATCH', + path: '/modelRisks/{id}', summary: "Update Model Risk By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], requestBody: { "(schema)": "ModelRiskInput", @@ -7415,18 +5996,12 @@ export const modelRiskEndpoints: Endpoint[] = [ tag: "Model Risks", }, { - method: "DELETE", - path: "/modelRisks/{id}", + method: 'DELETE', + path: '/modelRisks/{id}', summary: "Delete Model Risk By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -7440,8 +6015,8 @@ export const modelRiskEndpoints: Endpoint[] = [ // NIST AI RMF endpoints export const nistAiRmfEndpoints: Endpoint[] = [ { - method: "GET", - path: "/nist-ai-rmf/functions", + method: 'GET', + path: '/nist-ai-rmf/functions', summary: "Get All N I S T A I R M Ffunctions", requiresAuth: true, responses: [ @@ -7452,18 +6027,12 @@ export const nistAiRmfEndpoints: Endpoint[] = [ tag: "NIST AI RMF", }, { - method: "GET", - path: "/nist-ai-rmf/functions/{id}", + method: 'GET', + path: '/nist-ai-rmf/functions/{id}', summary: "Get N I S T A I R M Ffunction By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -7473,18 +6042,12 @@ export const nistAiRmfEndpoints: Endpoint[] = [ tag: "NIST AI RMF", }, { - method: "GET", - path: "/nist-ai-rmf/categories/{title}", + method: 'GET', + path: '/nist-ai-rmf/categories/{title}', summary: "Get All N I S T A I R M F Categories Byfunction Id", requiresAuth: true, parameters: [ - { - name: "title", - in: "path", - type: "string", - required: true, - description: "The title", - }, + { name: 'title', in: 'path', type: 'string', required: true, description: "The title" }, ], responses: [ { status: 200, description: "Success" }, @@ -7494,18 +6057,12 @@ export const nistAiRmfEndpoints: Endpoint[] = [ tag: "NIST AI RMF", }, { - method: "GET", - path: "/nist-ai-rmf/subcategories/byId/{id}", + method: 'GET', + path: '/nist-ai-rmf/subcategories/byId/{id}', summary: "Get N I S T A I R M F Subcategory By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -7515,18 +6072,12 @@ export const nistAiRmfEndpoints: Endpoint[] = [ tag: "NIST AI RMF", }, { - method: "GET", - path: "/nist-ai-rmf/subcategories/{id}/risks", + method: 'GET', + path: '/nist-ai-rmf/subcategories/{id}/risks', summary: "Get N I S T A I R M F Subcategory Risks", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -7536,25 +6087,13 @@ export const nistAiRmfEndpoints: Endpoint[] = [ tag: "NIST AI RMF", }, { - method: "GET", - path: "/nist-ai-rmf/subcategories/{categoryId}/{title}", + method: 'GET', + path: '/nist-ai-rmf/subcategories/{categoryId}/{title}', summary: "Get All N I S T A I R M F Subcategories Bycategory Id Andtitle", requiresAuth: true, parameters: [ - { - name: "categoryId", - in: "path", - type: "integer", - required: true, - description: "The categoryId", - }, - { - name: "title", - in: "path", - type: "string", - required: true, - description: "The title", - }, + { name: 'categoryId', in: 'path', type: 'integer', required: true, description: "The categoryId" }, + { name: 'title', in: 'path', type: 'string', required: true, description: "The title" }, ], responses: [ { status: 200, description: "Success" }, @@ -7564,18 +6103,12 @@ export const nistAiRmfEndpoints: Endpoint[] = [ tag: "NIST AI RMF", }, { - method: "PATCH", - path: "/nist-ai-rmf/subcategories/{id}", + method: 'PATCH', + path: '/nist-ai-rmf/subcategories/{id}', summary: "Update N I S T A I R M F Subcategory By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -7585,18 +6118,12 @@ export const nistAiRmfEndpoints: Endpoint[] = [ tag: "NIST AI RMF", }, { - method: "PATCH", - path: "/nist-ai-rmf/subcategories/{id}/status", + method: 'PATCH', + path: '/nist-ai-rmf/subcategories/{id}/status', summary: "Update N I S T A I R M F Subcategory Status", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -7606,8 +6133,8 @@ export const nistAiRmfEndpoints: Endpoint[] = [ tag: "NIST AI RMF", }, { - method: "GET", - path: "/nist-ai-rmf/progress", + method: 'GET', + path: '/nist-ai-rmf/progress', summary: "Get N I S T A I R M F Progress", requiresAuth: true, responses: [ @@ -7618,8 +6145,8 @@ export const nistAiRmfEndpoints: Endpoint[] = [ tag: "NIST AI RMF", }, { - method: "GET", - path: "/nist-ai-rmf/progress-by-function", + method: 'GET', + path: '/nist-ai-rmf/progress-by-function', summary: "Get N I S T A I R M F Progress By Function", requiresAuth: true, responses: [ @@ -7630,8 +6157,8 @@ export const nistAiRmfEndpoints: Endpoint[] = [ tag: "NIST AI RMF", }, { - method: "GET", - path: "/nist-ai-rmf/assignments", + method: 'GET', + path: '/nist-ai-rmf/assignments', summary: "Get N I S T A I R M F Assignments", requiresAuth: true, responses: [ @@ -7642,8 +6169,8 @@ export const nistAiRmfEndpoints: Endpoint[] = [ tag: "NIST AI RMF", }, { - method: "GET", - path: "/nist-ai-rmf/assignments-by-function", + method: 'GET', + path: '/nist-ai-rmf/assignments-by-function', summary: "Get N I S T A I R M F Assignments By Function", requiresAuth: true, responses: [ @@ -7654,8 +6181,8 @@ export const nistAiRmfEndpoints: Endpoint[] = [ tag: "NIST AI RMF", }, { - method: "GET", - path: "/nist-ai-rmf/status-breakdown", + method: 'GET', + path: '/nist-ai-rmf/status-breakdown', summary: "Get N I S T A I R M F Status Breakdown", requiresAuth: true, responses: [ @@ -7666,8 +6193,8 @@ export const nistAiRmfEndpoints: Endpoint[] = [ tag: "NIST AI RMF", }, { - method: "GET", - path: "/nist-ai-rmf/overview", + method: 'GET', + path: '/nist-ai-rmf/overview', summary: "Get N I S T A I R M F Overview", requiresAuth: true, responses: [ @@ -7682,8 +6209,8 @@ export const nistAiRmfEndpoints: Endpoint[] = [ // Notes endpoints export const noteEndpoints: Endpoint[] = [ { - method: "GET", - path: "/notes", + method: 'GET', + path: '/notes', summary: "Get Notes", requiresAuth: true, responses: [ @@ -7694,8 +6221,8 @@ export const noteEndpoints: Endpoint[] = [ tag: "Notes", }, { - method: "POST", - path: "/notes", + method: 'POST', + path: '/notes', summary: "Create Note", requiresAuth: true, responses: [ @@ -7706,18 +6233,12 @@ export const noteEndpoints: Endpoint[] = [ tag: "Notes", }, { - method: "PUT", - path: "/notes/{id}", + method: 'PUT', + path: '/notes/{id}', summary: "Update Note", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -7727,18 +6248,12 @@ export const noteEndpoints: Endpoint[] = [ tag: "Notes", }, { - method: "DELETE", - path: "/notes/{id}", + method: 'DELETE', + path: '/notes/{id}', summary: "Delete Note", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -7752,8 +6267,8 @@ export const noteEndpoints: Endpoint[] = [ // Notifications endpoints export const notificationEndpoints: Endpoint[] = [ { - method: "GET", - path: "/notifications/stream", + method: 'GET', + path: '/notifications/stream', summary: "Stream Notifications", requiresAuth: true, responses: [ @@ -7764,8 +6279,8 @@ export const notificationEndpoints: Endpoint[] = [ tag: "Notifications", }, { - method: "GET", - path: "/notifications", + method: 'GET', + path: '/notifications', summary: "Get Notifications", requiresAuth: true, responses: [ @@ -7776,8 +6291,8 @@ export const notificationEndpoints: Endpoint[] = [ tag: "Notifications", }, { - method: "GET", - path: "/notifications/summary", + method: 'GET', + path: '/notifications/summary', summary: "Get Notification Summary", requiresAuth: true, responses: [ @@ -7788,8 +6303,8 @@ export const notificationEndpoints: Endpoint[] = [ tag: "Notifications", }, { - method: "GET", - path: "/notifications/unread-count", + method: 'GET', + path: '/notifications/unread-count', summary: "Get Unread Count", requiresAuth: true, responses: [ @@ -7800,8 +6315,8 @@ export const notificationEndpoints: Endpoint[] = [ tag: "Notifications", }, { - method: "PATCH", - path: "/notifications/read-all", + method: 'PATCH', + path: '/notifications/read-all', summary: "Mark All As Read", requiresAuth: true, responses: [ @@ -7812,18 +6327,12 @@ export const notificationEndpoints: Endpoint[] = [ tag: "Notifications", }, { - method: "PATCH", - path: "/notifications/{id}/read", + method: 'PATCH', + path: '/notifications/{id}/read', summary: "Mark As Read", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -7833,18 +6342,12 @@ export const notificationEndpoints: Endpoint[] = [ tag: "Notifications", }, { - method: "DELETE", - path: "/notifications/{id}", + method: 'DELETE', + path: '/notifications/{id}', summary: "Delete Notification", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -7858,8 +6361,8 @@ export const notificationEndpoints: Endpoint[] = [ // Organizations endpoints export const organizationEndpoints: Endpoint[] = [ { - method: "GET", - path: "/organizations/exists", + method: 'GET', + path: '/organizations/exists', summary: "Get Organizations Exists", requiresAuth: false, responses: [ @@ -7869,18 +6372,12 @@ export const organizationEndpoints: Endpoint[] = [ tag: "Organizations", }, { - method: "GET", - path: "/organizations/{id}", + method: 'GET', + path: '/organizations/{id}', summary: "Get Organization By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -7890,18 +6387,12 @@ export const organizationEndpoints: Endpoint[] = [ tag: "Organizations", }, { - method: "PATCH", - path: "/organizations/{id}", + method: 'PATCH', + path: '/organizations/{id}', summary: "Update Organization By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -7911,8 +6402,8 @@ export const organizationEndpoints: Endpoint[] = [ tag: "Organizations", }, { - method: "POST", - path: "/organizations", + method: 'POST', + path: '/organizations', summary: "Create Organization", description: "Requires role: Super Admin", requiresAuth: true, @@ -7924,18 +6415,12 @@ export const organizationEndpoints: Endpoint[] = [ tag: "Organizations", }, { - method: "PATCH", - path: "/organizations/{id}/onboarding-status", + method: 'PATCH', + path: '/organizations/{id}/onboarding-status', summary: "Update Onboarding Status", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -7949,8 +6434,8 @@ export const organizationEndpoints: Endpoint[] = [ // Plugins endpoints export const pluginEndpoints: Endpoint[] = [ { - method: "GET", - path: "/plugins/marketplace", + method: 'GET', + path: '/plugins/marketplace', summary: "Get All Plugins", requiresAuth: true, responses: [ @@ -7961,18 +6446,12 @@ export const pluginEndpoints: Endpoint[] = [ tag: "Plugins", }, { - method: "GET", - path: "/plugins/marketplace/{key}", + method: 'GET', + path: '/plugins/marketplace/{key}', summary: "Get Plugin By Key", requiresAuth: true, parameters: [ - { - name: "key", - in: "path", - type: "string", - required: true, - description: "The key", - }, + { name: 'key', in: 'path', type: 'string', required: true, description: "The key" }, ], responses: [ { status: 200, description: "Success" }, @@ -7982,8 +6461,8 @@ export const pluginEndpoints: Endpoint[] = [ tag: "Plugins", }, { - method: "GET", - path: "/plugins/marketplace/search", + method: 'GET', + path: '/plugins/marketplace/search', summary: "Search Plugins", requiresAuth: true, responses: [ @@ -7994,8 +6473,8 @@ export const pluginEndpoints: Endpoint[] = [ tag: "Plugins", }, { - method: "GET", - path: "/plugins/categories", + method: 'GET', + path: '/plugins/categories', summary: "Get Categories", requiresAuth: true, responses: [ @@ -8006,8 +6485,8 @@ export const pluginEndpoints: Endpoint[] = [ tag: "Plugins", }, { - method: "POST", - path: "/plugins/install", + method: 'POST', + path: '/plugins/install', summary: "Install Plugin", requiresAuth: true, responses: [ @@ -8018,18 +6497,12 @@ export const pluginEndpoints: Endpoint[] = [ tag: "Plugins", }, { - method: "DELETE", - path: "/plugins/installations/{id}", + method: 'DELETE', + path: '/plugins/installations/{id}', summary: "Uninstall Plugin", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -8039,8 +6512,8 @@ export const pluginEndpoints: Endpoint[] = [ tag: "Plugins", }, { - method: "GET", - path: "/plugins/installations", + method: 'GET', + path: '/plugins/installations', summary: "Get Installed Plugins", requiresAuth: true, responses: [ @@ -8051,18 +6524,12 @@ export const pluginEndpoints: Endpoint[] = [ tag: "Plugins", }, { - method: "PUT", - path: "/plugins/installations/{id}/configuration", + method: 'PUT', + path: '/plugins/installations/{id}/configuration', summary: "Update Plugin Configuration", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -8072,18 +6539,12 @@ export const pluginEndpoints: Endpoint[] = [ tag: "Plugins", }, { - method: "POST", - path: "/plugins/{key}/test-connection", + method: 'POST', + path: '/plugins/{key}/test-connection', summary: "Test Plugin Connection", requiresAuth: true, parameters: [ - { - name: "key", - in: "path", - type: "string", - required: true, - description: "The key", - }, + { name: 'key', in: 'path', type: 'string', required: true, description: "The key" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -8093,25 +6554,13 @@ export const pluginEndpoints: Endpoint[] = [ tag: "Plugins", }, { - method: "GET", - path: "/plugins/{key}/ui/dist/{filename}", + method: 'GET', + path: '/plugins/{key}/ui/dist/{filename}', summary: "Serve plugin UI assets", requiresAuth: true, parameters: [ - { - name: "key", - in: "path", - type: "string", - required: true, - description: "The key", - }, - { - name: "filename", - in: "path", - type: "string", - required: true, - description: "The filename", - }, + { name: 'key', in: 'path', type: 'string', required: true, description: "The key" }, + { name: 'filename', in: 'path', type: 'string', required: true, description: "The filename" }, ], responses: [ { status: 200, description: "Success" }, @@ -8125,28 +6574,24 @@ export const pluginEndpoints: Endpoint[] = [ // Policies endpoints export const policyEndpoints: Endpoint[] = [ { - method: "POST", - path: "/policies/import/docx", + method: 'POST', + path: '/policies/import/docx', summary: "Import DOCX and convert to HTML", - description: - "Uploads a .docx file (max 10 MB) and converts it to HTML suitable for the policy content editor. Returns the converted HTML and any conversion warnings.", + description: "Uploads a .docx file (max 10 MB) and converts it to HTML suitable for the policy content editor. Returns the converted HTML and any conversion warnings.", requiresAuth: true, requestBody: { - file: "string (required)", + "file": "string (required)", }, responses: [ { status: 200, description: "DOCX converted to HTML successfully" }, - { - status: 400, - description: "Bad request — no file uploaded or invalid file type", - }, + { status: 400, description: "Bad request — no file uploaded or invalid file type" }, { status: 500, description: "No description" }, ], tag: "Policies", }, { - method: "PATCH", - path: "/policies/bulk", + method: 'PATCH', + path: '/policies/bulk', summary: "Unknown", description: "Requires role: Admin or Editor", requiresAuth: true, @@ -8157,11 +6602,10 @@ export const policyEndpoints: Endpoint[] = [ tag: "Policies", }, { - method: "GET", - path: "/policies", + method: 'GET', + path: '/policies', summary: "Get all policies", - description: - "Returns all policies for the authenticated user's organization, including assigned reviewer IDs.", + description: "Returns all policies for the authenticated user's organization, including assigned reviewer IDs.", requiresAuth: true, responses: [ { status: 200, description: "List of policies retrieved successfully" }, @@ -8170,11 +6614,10 @@ export const policyEndpoints: Endpoint[] = [ tag: "Policies", }, { - method: "POST", - path: "/policies", + method: 'POST', + path: '/policies', summary: "Create a new policy", - description: - "Creates a new policy. The author_id and last_updated_by are set from the JWT token automatically.", + description: "Creates a new policy. The author_id and last_updated_by are set from the JWT token automatically.", requiresAuth: true, requestBody: { "(schema)": "PolicyCreateRequest", @@ -8182,16 +6625,13 @@ export const policyEndpoints: Endpoint[] = [ responses: [ { status: 201, description: "Policy created successfully" }, { status: 500, description: "No description" }, - { - status: 503, - description: "Service unavailable — policy creation failed", - }, + { status: 503, description: "Service unavailable — policy creation failed" }, ], tag: "Policies", }, { - method: "GET", - path: "/policies/tags", + method: 'GET', + path: '/policies/tags', summary: "Get available policy tags", description: "Returns the static list of allowed policy tags.", requiresAuth: true, @@ -8202,8 +6642,8 @@ export const policyEndpoints: Endpoint[] = [ tag: "Policies", }, { - method: "GET", - path: "/policies/{id}/export/pdf", + method: 'GET', + path: '/policies/{id}/export/pdf', summary: "Export policy as PDF", description: "Generates and downloads the policy as a PDF file.", requiresAuth: true, @@ -8216,8 +6656,8 @@ export const policyEndpoints: Endpoint[] = [ tag: "Policies", }, { - method: "GET", - path: "/policies/{id}/export/docx", + method: 'GET', + path: '/policies/{id}/export/docx', summary: "Export policy as DOCX", description: "Generates and downloads the policy as a DOCX file.", requiresAuth: true, @@ -8230,11 +6670,10 @@ export const policyEndpoints: Endpoint[] = [ tag: "Policies", }, { - method: "GET", - path: "/policies/{id}", + method: 'GET', + path: '/policies/{id}', summary: "Get policy by ID", - description: - "Returns a single policy by its ID, including assigned reviewer IDs.", + description: "Returns a single policy by its ID, including assigned reviewer IDs.", requiresAuth: true, responses: [ { status: 200, description: "Policy retrieved successfully" }, @@ -8244,11 +6683,10 @@ export const policyEndpoints: Endpoint[] = [ tag: "Policies", }, { - method: "PUT", - path: "/policies/{id}", + method: 'PUT', + path: '/policies/{id}', summary: "Update a policy", - description: - "Updates an existing policy. Only provided fields are updated. The last_updated_by and last_updated_at are set automatically from the JWT token. If assigned_reviewer_ids is provided, the reviewer list is fully replaced.", + description: "Updates an existing policy. Only provided fields are updated. The last_updated_by and last_updated_at are set automatically from the JWT token. If assigned_reviewer_ids is provided, the reviewer list is fully replaced.", requiresAuth: true, requestBody: { "(schema)": "PolicyUpdateRequest", @@ -8261,11 +6699,10 @@ export const policyEndpoints: Endpoint[] = [ tag: "Policies", }, { - method: "DELETE", - path: "/policies/{id}", + method: 'DELETE', + path: '/policies/{id}', summary: "Delete a policy by ID", - description: - "Permanently deletes a policy and its associated reviewer mappings (via CASCADE).", + description: "Permanently deletes a policy and its associated reviewer mappings (via CASCADE).", requiresAuth: true, responses: [ { status: 202, description: "Policy deleted successfully" }, @@ -8275,22 +6712,17 @@ export const policyEndpoints: Endpoint[] = [ tag: "Policies", }, { - method: "POST", - path: "/policies/{id}/review/request", + method: 'POST', + path: '/policies/{id}/review/request', summary: "Request review for a policy", - description: - "Sets the policy review status to pending_review and sends in-app notifications to each specified reviewer.", + description: "Sets the policy review status to pending_review and sends in-app notifications to each specified reviewer.", requiresAuth: true, requestBody: { - reviewer_ids: "array (required)", - message: "string (optional)", + "reviewer_ids": "array (required)", + "message": "string (optional)", }, responses: [ - { - status: 200, - description: - "Review requested successfully; returns the updated policy", - }, + { status: 200, description: "Review requested successfully; returns the updated policy" }, { status: 400, description: "Invalid policy ID or missing reviewer_ids" }, { status: 404, description: "No description" }, { status: 500, description: "No description" }, @@ -8298,20 +6730,16 @@ export const policyEndpoints: Endpoint[] = [ tag: "Policies", }, { - method: "PUT", - path: "/policies/{id}/review/approve", + method: 'PUT', + path: '/policies/{id}/review/approve', summary: "Approve a policy review", - description: - "Sets the policy review status to approved and sends an in-app notification to the policy author.", + description: "Sets the policy review status to approved and sends an in-app notification to the policy author.", requiresAuth: true, requestBody: { - comment: "string (optional)", + "comment": "string (optional)", }, responses: [ - { - status: 200, - description: "Policy review approved; returns the updated policy", - }, + { status: 200, description: "Policy review approved; returns the updated policy" }, { status: 400, description: "Invalid policy ID" }, { status: 404, description: "No description" }, { status: 500, description: "No description" }, @@ -8319,20 +6747,16 @@ export const policyEndpoints: Endpoint[] = [ tag: "Policies", }, { - method: "PUT", - path: "/policies/{id}/review/reject", + method: 'PUT', + path: '/policies/{id}/review/reject', summary: "Reject a policy review (request changes)", - description: - "Sets the policy review status to changes_requested and sends an in-app notification to the policy author. A comment is required.", + description: "Sets the policy review status to changes_requested and sends an in-app notification to the policy author. A comment is required.", requiresAuth: true, requestBody: { - comment: "string (required)", + "comment": "string (required)", }, responses: [ - { - status: 200, - description: "Policy review rejected; returns the updated policy", - }, + { status: 200, description: "Policy review rejected; returns the updated policy" }, { status: 400, description: "Invalid policy ID or missing comment" }, { status: 404, description: "No description" }, { status: 500, description: "No description" }, @@ -8340,18 +6764,12 @@ export const policyEndpoints: Endpoint[] = [ tag: "Policies", }, { - method: "GET", - path: "/policies/folders/{folderId}/policies", + method: 'GET', + path: '/policies/folders/{folderId}/policies', summary: "Get Policies In Folder", requiresAuth: true, parameters: [ - { - name: "folderId", - in: "path", - type: "integer", - required: true, - description: "The folderId", - }, + { name: 'folderId', in: 'path', type: 'integer', required: true, description: "The folderId" }, ], responses: [ { status: 200, description: "Success" }, @@ -8361,18 +6779,12 @@ export const policyEndpoints: Endpoint[] = [ tag: "Policies", }, { - method: "GET", - path: "/policies/{id}/folders", + method: 'GET', + path: '/policies/{id}/folders', summary: "Get Policy Folders", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -8382,18 +6794,12 @@ export const policyEndpoints: Endpoint[] = [ tag: "Policies", }, { - method: "PATCH", - path: "/policies/{id}/folders", + method: 'PATCH', + path: '/policies/{id}/folders', summary: "Update Policy Folders", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -8403,8 +6809,8 @@ export const policyEndpoints: Endpoint[] = [ tag: "Policies", }, { - method: "GET", - path: "/policy-linked", + method: 'GET', + path: '/policy-linked', summary: "Get All Linked Objects", requiresAuth: true, responses: [ @@ -8415,18 +6821,12 @@ export const policyEndpoints: Endpoint[] = [ tag: "Policies", }, { - method: "GET", - path: "/policy-linked/{policyId}/linked-objects", + method: 'GET', + path: '/policy-linked/{policyId}/linked-objects', summary: "Get Linked Objects For Policy", requiresAuth: true, parameters: [ - { - name: "policyId", - in: "path", - type: "integer", - required: true, - description: "The policyId", - }, + { name: 'policyId', in: 'path', type: 'integer', required: true, description: "The policyId" }, ], responses: [ { status: 200, description: "Success" }, @@ -8436,18 +6836,12 @@ export const policyEndpoints: Endpoint[] = [ tag: "Policies", }, { - method: "POST", - path: "/policy-linked/{policyId}/linked-objects", + method: 'POST', + path: '/policy-linked/{policyId}/linked-objects', summary: "Create Linked Object For Policy", requiresAuth: true, parameters: [ - { - name: "policyId", - in: "path", - type: "integer", - required: true, - description: "The policyId", - }, + { name: 'policyId', in: 'path', type: 'integer', required: true, description: "The policyId" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -8457,18 +6851,12 @@ export const policyEndpoints: Endpoint[] = [ tag: "Policies", }, { - method: "DELETE", - path: "/policy-linked/{policyId}/linked-objects", + method: 'DELETE', + path: '/policy-linked/{policyId}/linked-objects', summary: "Delete Linked Object For Policy", requiresAuth: true, parameters: [ - { - name: "policyId", - in: "path", - type: "integer", - required: true, - description: "The policyId", - }, + { name: 'policyId', in: 'path', type: 'integer', required: true, description: "The policyId" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -8478,18 +6866,12 @@ export const policyEndpoints: Endpoint[] = [ tag: "Policies", }, { - method: "DELETE", - path: "/policy-linked/risk/{riskId}/unlink-all", + method: 'DELETE', + path: '/policy-linked/risk/{riskId}/unlink-all', summary: "Unlink Risk From All Policies", requiresAuth: true, parameters: [ - { - name: "riskId", - in: "path", - type: "integer", - required: true, - description: "The riskId", - }, + { name: 'riskId', in: 'path', type: 'integer', required: true, description: "The riskId" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -8499,18 +6881,12 @@ export const policyEndpoints: Endpoint[] = [ tag: "Policies", }, { - method: "DELETE", - path: "/policy-linked/evidence/{evidenceId}/unlink-all", + method: 'DELETE', + path: '/policy-linked/evidence/{evidenceId}/unlink-all', summary: "Unlink Evidence From All Policies", requiresAuth: true, parameters: [ - { - name: "evidenceId", - in: "path", - type: "integer", - required: true, - description: "The evidenceId", - }, + { name: 'evidenceId', in: 'path', type: 'integer', required: true, description: "The evidenceId" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -8524,18 +6900,12 @@ export const policyEndpoints: Endpoint[] = [ // Post-Market Monitoring endpoints export const postMarketMonitoringEndpoints: Endpoint[] = [ { - method: "GET", - path: "/pmm/config/{projectId}", + method: 'GET', + path: '/pmm/config/{projectId}', summary: "Get Config By Project Id", requiresAuth: true, parameters: [ - { - name: "projectId", - in: "path", - type: "integer", - required: true, - description: "The projectId", - }, + { name: 'projectId', in: 'path', type: 'integer', required: true, description: "The projectId" }, ], responses: [ { status: 200, description: "Success" }, @@ -8545,8 +6915,8 @@ export const postMarketMonitoringEndpoints: Endpoint[] = [ tag: "Post-Market Monitoring", }, { - method: "POST", - path: "/pmm/config", + method: 'POST', + path: '/pmm/config', summary: "Create Config", requiresAuth: true, responses: [ @@ -8557,18 +6927,12 @@ export const postMarketMonitoringEndpoints: Endpoint[] = [ tag: "Post-Market Monitoring", }, { - method: "PUT", - path: "/pmm/config/{configId}", + method: 'PUT', + path: '/pmm/config/{configId}', summary: "Update Config", requiresAuth: true, parameters: [ - { - name: "configId", - in: "path", - type: "integer", - required: true, - description: "The configId", - }, + { name: 'configId', in: 'path', type: 'integer', required: true, description: "The configId" }, ], responses: [ { status: 200, description: "Success" }, @@ -8578,18 +6942,12 @@ export const postMarketMonitoringEndpoints: Endpoint[] = [ tag: "Post-Market Monitoring", }, { - method: "DELETE", - path: "/pmm/config/{configId}", + method: 'DELETE', + path: '/pmm/config/{configId}', summary: "Delete Config", requiresAuth: true, parameters: [ - { - name: "configId", - in: "path", - type: "integer", - required: true, - description: "The configId", - }, + { name: 'configId', in: 'path', type: 'integer', required: true, description: "The configId" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -8599,18 +6957,12 @@ export const postMarketMonitoringEndpoints: Endpoint[] = [ tag: "Post-Market Monitoring", }, { - method: "GET", - path: "/pmm/config/{configId}/questions", + method: 'GET', + path: '/pmm/config/{configId}/questions', summary: "Get Questions", requiresAuth: true, parameters: [ - { - name: "configId", - in: "path", - type: "integer", - required: true, - description: "The configId", - }, + { name: 'configId', in: 'path', type: 'integer', required: true, description: "The configId" }, ], responses: [ { status: 200, description: "Success" }, @@ -8620,18 +6972,12 @@ export const postMarketMonitoringEndpoints: Endpoint[] = [ tag: "Post-Market Monitoring", }, { - method: "POST", - path: "/pmm/config/{configId}/questions", + method: 'POST', + path: '/pmm/config/{configId}/questions', summary: "Add Question", requiresAuth: true, parameters: [ - { - name: "configId", - in: "path", - type: "integer", - required: true, - description: "The configId", - }, + { name: 'configId', in: 'path', type: 'integer', required: true, description: "The configId" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -8641,8 +6987,8 @@ export const postMarketMonitoringEndpoints: Endpoint[] = [ tag: "Post-Market Monitoring", }, { - method: "GET", - path: "/pmm/org/questions", + method: 'GET', + path: '/pmm/org/questions', summary: "Get Questions", requiresAuth: true, responses: [ @@ -8653,18 +6999,12 @@ export const postMarketMonitoringEndpoints: Endpoint[] = [ tag: "Post-Market Monitoring", }, { - method: "PUT", - path: "/pmm/questions/{questionId}", + method: 'PUT', + path: '/pmm/questions/{questionId}', summary: "Update Question", requiresAuth: true, parameters: [ - { - name: "questionId", - in: "path", - type: "integer", - required: true, - description: "The questionId", - }, + { name: 'questionId', in: 'path', type: 'integer', required: true, description: "The questionId" }, ], responses: [ { status: 200, description: "Success" }, @@ -8674,18 +7014,12 @@ export const postMarketMonitoringEndpoints: Endpoint[] = [ tag: "Post-Market Monitoring", }, { - method: "DELETE", - path: "/pmm/questions/{questionId}", + method: 'DELETE', + path: '/pmm/questions/{questionId}', summary: "Delete Question", requiresAuth: true, parameters: [ - { - name: "questionId", - in: "path", - type: "integer", - required: true, - description: "The questionId", - }, + { name: 'questionId', in: 'path', type: 'integer', required: true, description: "The questionId" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -8695,8 +7029,8 @@ export const postMarketMonitoringEndpoints: Endpoint[] = [ tag: "Post-Market Monitoring", }, { - method: "POST", - path: "/pmm/questions/reorder", + method: 'POST', + path: '/pmm/questions/reorder', summary: "Reorder Questions", requiresAuth: true, responses: [ @@ -8707,18 +7041,12 @@ export const postMarketMonitoringEndpoints: Endpoint[] = [ tag: "Post-Market Monitoring", }, { - method: "GET", - path: "/pmm/active-cycle/{projectId}", + method: 'GET', + path: '/pmm/active-cycle/{projectId}', summary: "Get Active Cycle", requiresAuth: true, parameters: [ - { - name: "projectId", - in: "path", - type: "integer", - required: true, - description: "The projectId", - }, + { name: 'projectId', in: 'path', type: 'integer', required: true, description: "The projectId" }, ], responses: [ { status: 200, description: "Success" }, @@ -8728,18 +7056,12 @@ export const postMarketMonitoringEndpoints: Endpoint[] = [ tag: "Post-Market Monitoring", }, { - method: "GET", - path: "/pmm/cycles/{cycleId}", + method: 'GET', + path: '/pmm/cycles/{cycleId}', summary: "Get Cycle By Id", requiresAuth: true, parameters: [ - { - name: "cycleId", - in: "path", - type: "integer", - required: true, - description: "The cycleId", - }, + { name: 'cycleId', in: 'path', type: 'integer', required: true, description: "The cycleId" }, ], responses: [ { status: 200, description: "Success" }, @@ -8749,18 +7071,12 @@ export const postMarketMonitoringEndpoints: Endpoint[] = [ tag: "Post-Market Monitoring", }, { - method: "GET", - path: "/pmm/cycles/{cycleId}/responses", + method: 'GET', + path: '/pmm/cycles/{cycleId}/responses', summary: "Get Responses", requiresAuth: true, parameters: [ - { - name: "cycleId", - in: "path", - type: "integer", - required: true, - description: "The cycleId", - }, + { name: 'cycleId', in: 'path', type: 'integer', required: true, description: "The cycleId" }, ], responses: [ { status: 200, description: "Success" }, @@ -8770,18 +7086,12 @@ export const postMarketMonitoringEndpoints: Endpoint[] = [ tag: "Post-Market Monitoring", }, { - method: "POST", - path: "/pmm/cycles/{cycleId}/responses", + method: 'POST', + path: '/pmm/cycles/{cycleId}/responses', summary: "Save Responses", requiresAuth: true, parameters: [ - { - name: "cycleId", - in: "path", - type: "integer", - required: true, - description: "The cycleId", - }, + { name: 'cycleId', in: 'path', type: 'integer', required: true, description: "The cycleId" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -8791,18 +7101,12 @@ export const postMarketMonitoringEndpoints: Endpoint[] = [ tag: "Post-Market Monitoring", }, { - method: "POST", - path: "/pmm/cycles/{cycleId}/submit", + method: 'POST', + path: '/pmm/cycles/{cycleId}/submit', summary: "Submit Cycle", requiresAuth: true, parameters: [ - { - name: "cycleId", - in: "path", - type: "integer", - required: true, - description: "The cycleId", - }, + { name: 'cycleId', in: 'path', type: 'integer', required: true, description: "The cycleId" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -8812,18 +7116,12 @@ export const postMarketMonitoringEndpoints: Endpoint[] = [ tag: "Post-Market Monitoring", }, { - method: "POST", - path: "/pmm/cycles/{cycleId}/flag", + method: 'POST', + path: '/pmm/cycles/{cycleId}/flag', summary: "Flag Concern", requiresAuth: true, parameters: [ - { - name: "cycleId", - in: "path", - type: "integer", - required: true, - description: "The cycleId", - }, + { name: 'cycleId', in: 'path', type: 'integer', required: true, description: "The cycleId" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -8833,8 +7131,8 @@ export const postMarketMonitoringEndpoints: Endpoint[] = [ tag: "Post-Market Monitoring", }, { - method: "GET", - path: "/pmm/reports", + method: 'GET', + path: '/pmm/reports', summary: "Get Reports", requiresAuth: true, responses: [ @@ -8845,18 +7143,12 @@ export const postMarketMonitoringEndpoints: Endpoint[] = [ tag: "Post-Market Monitoring", }, { - method: "GET", - path: "/pmm/reports/{reportId}/download", + method: 'GET', + path: '/pmm/reports/{reportId}/download', summary: "Download Report", requiresAuth: true, parameters: [ - { - name: "reportId", - in: "path", - type: "integer", - required: true, - description: "The reportId", - }, + { name: 'reportId', in: 'path', type: 'integer', required: true, description: "The reportId" }, ], responses: [ { status: 200, description: "Success" }, @@ -8866,18 +7158,12 @@ export const postMarketMonitoringEndpoints: Endpoint[] = [ tag: "Post-Market Monitoring", }, { - method: "POST", - path: "/pmm/cycles/{cycleId}/reassign", + method: 'POST', + path: '/pmm/cycles/{cycleId}/reassign', summary: "Reassign Stakeholder", requiresAuth: true, parameters: [ - { - name: "cycleId", - in: "path", - type: "integer", - required: true, - description: "The cycleId", - }, + { name: 'cycleId', in: 'path', type: 'integer', required: true, description: "The cycleId" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -8887,18 +7173,12 @@ export const postMarketMonitoringEndpoints: Endpoint[] = [ tag: "Post-Market Monitoring", }, { - method: "POST", - path: "/pmm/projects/{projectId}/start-cycle", + method: 'POST', + path: '/pmm/projects/{projectId}/start-cycle', summary: "Start New Cycle", requiresAuth: true, parameters: [ - { - name: "projectId", - in: "path", - type: "integer", - required: true, - description: "The projectId", - }, + { name: 'projectId', in: 'path', type: 'integer', required: true, description: "The projectId" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -8912,11 +7192,10 @@ export const postMarketMonitoringEndpoints: Endpoint[] = [ // Projects endpoints export const projectEndpoints: Endpoint[] = [ { - method: "GET", - path: "/projects", + method: 'GET', + path: '/projects', summary: "Get all projects", - description: - "Returns all projects visible to the authenticated user. Admins and SuperAdmins see all projects in the organization; other roles see only projects they own or are members of.", + description: "Returns all projects visible to the authenticated user. Admins and SuperAdmins see all projects in the organization; other roles see only projects they own or are members of.", requiresAuth: true, responses: [ { status: 200, description: "List of projects retrieved successfully" }, @@ -8926,11 +7205,10 @@ export const projectEndpoints: Endpoint[] = [ tag: "Projects", }, { - method: "POST", - path: "/projects", + method: 'POST', + path: '/projects', summary: "Create a new project (use case)", - description: - "Creates a new project with associated members and frameworks. If an approval_workflow_id is provided, framework creation is deferred until the approval request is approved.", + description: "Creates a new project with associated members and frameworks. If an approval_workflow_id is provided, framework creation is deferred until the approval request is approved.", requiresAuth: true, requestBody: { "(schema)": "CreateProjectRequest", @@ -8938,21 +7216,15 @@ export const projectEndpoints: Endpoint[] = [ responses: [ { status: 201, description: "Project created successfully" }, { status: 400, description: "Validation error" }, - { - status: 403, - description: "Business logic error (e.g. framework not allowed)", - }, + { status: 403, description: "Business logic error (e.g. framework not allowed)" }, { status: 500, description: "Internal server error" }, - { - status: 503, - description: "Service unavailable — project creation returned null", - }, + { status: 503, description: "Service unavailable — project creation returned null" }, ], tag: "Projects", }, { - method: "GET", - path: "/projects/calculateProjectRisks/{id}", + method: 'GET', + path: '/projects/calculateProjectRisks/{id}', summary: "Calculate project risk distribution", requiresAuth: true, responses: [ @@ -8963,8 +7235,8 @@ export const projectEndpoints: Endpoint[] = [ tag: "Projects", }, { - method: "GET", - path: "/projects/calculateVendorRisks/{id}", + method: 'GET', + path: '/projects/calculateVendorRisks/{id}', summary: "Calculate vendor risk distribution", requiresAuth: true, responses: [ @@ -8975,11 +7247,10 @@ export const projectEndpoints: Endpoint[] = [ tag: "Projects", }, { - method: "GET", - path: "/projects/{id}", + method: 'GET', + path: '/projects/{id}', summary: "Get a project by ID", - description: - "Returns a single project with its frameworks, owner name, members, and approval status.", + description: "Returns a single project with its frameworks, owner name, members, and approval status.", requiresAuth: true, responses: [ { status: 200, description: "Project found" }, @@ -8989,11 +7260,10 @@ export const projectEndpoints: Endpoint[] = [ tag: "Projects", }, { - method: "PATCH", - path: "/projects/{id}", + method: 'PATCH', + path: '/projects/{id}', summary: "Update a project by ID", - description: - "Partially updates a project and its member list. Only provided fields are updated.", + description: "Partially updates a project and its member list. Only provided fields are updated.", requiresAuth: true, requestBody: { "(schema)": "UpdateProjectRequest", @@ -9009,11 +7279,10 @@ export const projectEndpoints: Endpoint[] = [ tag: "Projects", }, { - method: "DELETE", - path: "/projects/{id}", + method: 'DELETE', + path: '/projects/{id}', summary: "Delete a project by ID", - description: - "Deletes a project and all dependent entities (files, risks, members, framework data).", + description: "Deletes a project and all dependent entities (files, risks, members, framework data).", requiresAuth: true, responses: [ { status: 202, description: "Project deleted successfully" }, @@ -9023,8 +7292,8 @@ export const projectEndpoints: Endpoint[] = [ tag: "Projects", }, { - method: "GET", - path: "/projects/stats/{id}", + method: 'GET', + path: '/projects/stats/{id}', summary: "Get project statistics by ID", requiresAuth: true, responses: [ @@ -9034,18 +7303,12 @@ export const projectEndpoints: Endpoint[] = [ tag: "Projects", }, { - method: "GET", - path: "/projects/complainces/{projid}", + method: 'GET', + path: '/projects/complainces/{projid}', summary: "Get compliance data for a project", requiresAuth: true, parameters: [ - { - name: "projid", - in: "path", - type: "integer", - required: true, - description: "The project ID", - }, + { name: 'projid', in: 'path', type: 'integer', required: true, description: "The project ID" }, ], responses: [ { status: 200, description: "Compliance data returned" }, @@ -9055,8 +7318,8 @@ export const projectEndpoints: Endpoint[] = [ tag: "Projects", }, { - method: "GET", - path: "/projects/compliance/progress/{id}", + method: 'GET', + path: '/projects/compliance/progress/{id}', summary: "Get compliance progress for a single project", requiresAuth: true, responses: [ @@ -9067,8 +7330,8 @@ export const projectEndpoints: Endpoint[] = [ tag: "Projects", }, { - method: "GET", - path: "/projects/assessment/progress/{id}", + method: 'GET', + path: '/projects/assessment/progress/{id}', summary: "Get assessment progress for a single project", requiresAuth: true, responses: [ @@ -9079,8 +7342,8 @@ export const projectEndpoints: Endpoint[] = [ tag: "Projects", }, { - method: "GET", - path: "/projects/all/compliance/progress", + method: 'GET', + path: '/projects/all/compliance/progress', summary: "Get compliance progress across all projects", requiresAuth: true, responses: [ @@ -9092,8 +7355,8 @@ export const projectEndpoints: Endpoint[] = [ tag: "Projects", }, { - method: "GET", - path: "/projects/all/assessment/progress", + method: 'GET', + path: '/projects/all/assessment/progress', summary: "Get assessment progress across all projects", requiresAuth: true, responses: [ @@ -9105,12 +7368,12 @@ export const projectEndpoints: Endpoint[] = [ tag: "Projects", }, { - method: "PATCH", - path: "/projects/{id}/status", + method: 'PATCH', + path: '/projects/{id}/status', summary: "Update project status", requiresAuth: true, requestBody: { - status: "ProjectStatus (required)", + "status": "ProjectStatus (required)", }, responses: [ { status: 200, description: "Project status updated successfully" }, @@ -9124,18 +7387,12 @@ export const projectEndpoints: Endpoint[] = [ // Project Risks endpoints export const projectRiskEndpoints: Endpoint[] = [ { - method: "GET", - path: "/projectRisks", + method: 'GET', + path: '/projectRisks', summary: "Get All Risks", requiresAuth: true, parameters: [ - { - name: "filter", - in: "query", - type: "string", - required: false, - description: "Filter by soft-delete state.", - }, + { name: 'filter', in: 'query', type: 'string', required: false, description: "Filter by soft-delete state." }, ], responses: [ { status: 200, description: "Success" }, @@ -9145,8 +7402,8 @@ export const projectRiskEndpoints: Endpoint[] = [ tag: "Project Risks", }, { - method: "POST", - path: "/projectRisks", + method: 'POST', + path: '/projectRisks', summary: "Create Risk", requiresAuth: true, requestBody: { @@ -9160,25 +7417,13 @@ export const projectRiskEndpoints: Endpoint[] = [ tag: "Project Risks", }, { - method: "GET", - path: "/projectRisks/by-projid/{id}", + method: 'GET', + path: '/projectRisks/by-projid/{id}', summary: "Get Risks By Project", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "string", - required: true, - description: "The id", - }, - { - name: "filter", - in: "query", - type: "string", - required: false, - description: "The filter", - }, + { name: 'id', in: 'path', type: 'string', required: true, description: "The id" }, + { name: 'filter', in: 'query', type: 'string', required: false, description: "The filter" }, ], responses: [ { status: 200, description: "Success" }, @@ -9188,25 +7433,13 @@ export const projectRiskEndpoints: Endpoint[] = [ tag: "Project Risks", }, { - method: "GET", - path: "/projectRisks/by-frameworkid/{id}", + method: 'GET', + path: '/projectRisks/by-frameworkid/{id}', summary: "Get Risks By Framework", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, - { - name: "filter", - in: "query", - type: "string", - required: false, - description: "The filter", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, + { name: 'filter', in: 'query', type: 'string', required: false, description: "The filter" }, ], responses: [ { status: 200, description: "Success" }, @@ -9216,18 +7449,12 @@ export const projectRiskEndpoints: Endpoint[] = [ tag: "Project Risks", }, { - method: "GET", - path: "/projectRisks/{id}", + method: 'GET', + path: '/projectRisks/{id}', summary: "Get Risk By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -9237,18 +7464,12 @@ export const projectRiskEndpoints: Endpoint[] = [ tag: "Project Risks", }, { - method: "PUT", - path: "/projectRisks/{id}", + method: 'PUT', + path: '/projectRisks/{id}', summary: "Update Risk By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], requestBody: { "(schema)": "ProjectRiskInput", @@ -9261,18 +7482,12 @@ export const projectRiskEndpoints: Endpoint[] = [ tag: "Project Risks", }, { - method: "DELETE", - path: "/projectRisks/{id}", + method: 'DELETE', + path: '/projectRisks/{id}', summary: "Delete Risk By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -9282,8 +7497,8 @@ export const projectRiskEndpoints: Endpoint[] = [ tag: "Project Risks", }, { - method: "PATCH", - path: "/projectRisks/bulk", + method: 'PATCH', + path: '/projectRisks/bulk', summary: "Bulk Update Project Risks", description: "Requires role: Admin or Editor", requiresAuth: true, @@ -9298,8 +7513,8 @@ export const projectRiskEndpoints: Endpoint[] = [ // Quantitative Risks endpoints export const quantitativeRiskEndpoints: Endpoint[] = [ { - method: "GET", - path: "/quantitative-risks/portfolio/org", + method: 'GET', + path: '/quantitative-risks/portfolio/org', summary: "Get Org Portfolio", requiresAuth: true, responses: [ @@ -9310,18 +7525,12 @@ export const quantitativeRiskEndpoints: Endpoint[] = [ tag: "Quantitative Risks", }, { - method: "GET", - path: "/quantitative-risks/portfolio/project/{projectId}", + method: 'GET', + path: '/quantitative-risks/portfolio/project/{projectId}', summary: "Get Project Portfolio", requiresAuth: true, parameters: [ - { - name: "projectId", - in: "path", - type: "integer", - required: true, - description: "The projectId", - }, + { name: 'projectId', in: 'path', type: 'integer', required: true, description: "The projectId" }, ], responses: [ { status: 200, description: "Success" }, @@ -9331,8 +7540,8 @@ export const quantitativeRiskEndpoints: Endpoint[] = [ tag: "Quantitative Risks", }, { - method: "GET", - path: "/quantitative-risks/portfolio/trend", + method: 'GET', + path: '/quantitative-risks/portfolio/trend', summary: "Get Portfolio Trend Handler", requiresAuth: true, responses: [ @@ -9343,25 +7552,13 @@ export const quantitativeRiskEndpoints: Endpoint[] = [ tag: "Quantitative Risks", }, { - method: "POST", - path: "/quantitative-risks/{riskId}/apply-benchmark/{benchmarkId}", + method: 'POST', + path: '/quantitative-risks/{riskId}/apply-benchmark/{benchmarkId}', summary: "Apply Benchmark", requiresAuth: true, parameters: [ - { - name: "riskId", - in: "path", - type: "integer", - required: true, - description: "The riskId", - }, - { - name: "benchmarkId", - in: "path", - type: "integer", - required: true, - description: "The benchmarkId", - }, + { name: 'riskId', in: 'path', type: 'integer', required: true, description: "The riskId" }, + { name: 'benchmarkId', in: 'path', type: 'integer', required: true, description: "The benchmarkId" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -9371,8 +7568,8 @@ export const quantitativeRiskEndpoints: Endpoint[] = [ tag: "Quantitative Risks", }, { - method: "GET", - path: "/quantitative-risks/assessment-mode", + method: 'GET', + path: '/quantitative-risks/assessment-mode', summary: "Get Risk Assessment Mode", requiresAuth: true, responses: [ @@ -9383,8 +7580,8 @@ export const quantitativeRiskEndpoints: Endpoint[] = [ tag: "Quantitative Risks", }, { - method: "PUT", - path: "/quantitative-risks/assessment-mode", + method: 'PUT', + path: '/quantitative-risks/assessment-mode', summary: "Update Risk Assessment Mode", requiresAuth: true, responses: [ @@ -9399,8 +7596,8 @@ export const quantitativeRiskEndpoints: Endpoint[] = [ // Readiness endpoints export const readinessEndpoints: Endpoint[] = [ { - method: "POST", - path: "/readiness/calculate", + method: 'POST', + path: '/readiness/calculate', summary: "Calculate All", requiresAuth: true, responses: [ @@ -9410,8 +7607,8 @@ export const readinessEndpoints: Endpoint[] = [ tag: "Readiness", }, { - method: "POST", - path: "/readiness/calculate/{frameworkType}", + method: 'POST', + path: '/readiness/calculate/{frameworkType}', summary: "Calculate For Framework", requiresAuth: true, responses: [ @@ -9421,8 +7618,8 @@ export const readinessEndpoints: Endpoint[] = [ tag: "Readiness", }, { - method: "GET", - path: "/readiness/scores", + method: 'GET', + path: '/readiness/scores', summary: "Get Scores", requiresAuth: true, responses: [ @@ -9432,8 +7629,8 @@ export const readinessEndpoints: Endpoint[] = [ tag: "Readiness", }, { - method: "GET", - path: "/readiness/scores/{frameworkType}", + method: 'GET', + path: '/readiness/scores/{frameworkType}', summary: "Get Scores By Framework", requiresAuth: true, responses: [ @@ -9443,8 +7640,8 @@ export const readinessEndpoints: Endpoint[] = [ tag: "Readiness", }, { - method: "GET", - path: "/readiness/controls/{frameworkType}", + method: 'GET', + path: '/readiness/controls/{frameworkType}', summary: "Get Control Scores", requiresAuth: true, responses: [ @@ -9454,8 +7651,8 @@ export const readinessEndpoints: Endpoint[] = [ tag: "Readiness", }, { - method: "GET", - path: "/readiness/weakest", + method: 'GET', + path: '/readiness/weakest', summary: "Get Weakest", requiresAuth: true, responses: [ @@ -9465,8 +7662,8 @@ export const readinessEndpoints: Endpoint[] = [ tag: "Readiness", }, { - method: "GET", - path: "/readiness/recommendations", + method: 'GET', + path: '/readiness/recommendations', summary: "Get Recommendations", requiresAuth: true, responses: [ @@ -9476,8 +7673,8 @@ export const readinessEndpoints: Endpoint[] = [ tag: "Readiness", }, { - method: "GET", - path: "/readiness/history", + method: 'GET', + path: '/readiness/history', summary: "Get History", requiresAuth: true, responses: [ @@ -9488,11 +7685,169 @@ export const readinessEndpoints: Endpoint[] = [ }, ]; +// Regulations Tracker endpoints +export const regulationsTrackerEndpoints: Endpoint[] = [ + { + method: 'GET', + path: '/regulations-tracker/countries', + summary: "Get Countries", + requiresAuth: true, + responses: [ + { status: 200, description: "Success" }, + { status: 500, description: "Internal server error" }, + ], + tag: "Regulations Tracker", + }, + { + method: 'GET', + path: '/regulations-tracker/countries/{slug}/impact', + summary: "Get Impact Analysis", + requiresAuth: true, + responses: [ + { status: 200, description: "Success" }, + { status: 500, description: "Internal server error" }, + ], + tag: "Regulations Tracker", + }, + { + method: 'POST', + path: '/regulations-tracker/countries/{slug}/impact/refresh', + summary: "Refresh Impact Analysis", + requiresAuth: true, + responses: [ + { status: 200, description: "Success" }, + { status: 500, description: "Internal server error" }, + ], + tag: "Regulations Tracker", + }, + { + method: 'GET', + path: '/regulations-tracker/countries/{slug}', + summary: "Get Country Detail", + requiresAuth: true, + responses: [ + { status: 200, description: "Success" }, + { status: 500, description: "Internal server error" }, + ], + tag: "Regulations Tracker", + }, + { + method: 'GET', + path: '/regulations-tracker/tracked', + summary: "Get Tracked", + requiresAuth: true, + responses: [ + { status: 200, description: "Success" }, + { status: 500, description: "Internal server error" }, + ], + tag: "Regulations Tracker", + }, + { + method: 'POST', + path: '/regulations-tracker/tracked', + summary: "Track Country Ctrl", + requiresAuth: true, + responses: [ + { status: 200, description: "Success" }, + { status: 500, description: "Internal server error" }, + ], + tag: "Regulations Tracker", + }, + { + method: 'POST', + path: '/regulations-tracker/tracked/bulk', + summary: "Track Bulk Ctrl", + requiresAuth: true, + responses: [ + { status: 200, description: "Success" }, + { status: 500, description: "Internal server error" }, + ], + tag: "Regulations Tracker", + }, + { + method: 'DELETE', + path: '/regulations-tracker/tracked/{slug}', + summary: "Untrack Country Ctrl", + requiresAuth: true, + responses: [ + { status: 200, description: "Success" }, + { status: 500, description: "Internal server error" }, + ], + tag: "Regulations Tracker", + }, + { + method: 'GET', + path: '/regulations-tracker/settings', + summary: "Get Settings Ctrl", + requiresAuth: true, + responses: [ + { status: 200, description: "Success" }, + { status: 500, description: "Internal server error" }, + ], + tag: "Regulations Tracker", + }, + { + method: 'PUT', + path: '/regulations-tracker/settings', + summary: "Update Settings Ctrl", + requiresAuth: true, + responses: [ + { status: 200, description: "Success" }, + { status: 500, description: "Internal server error" }, + ], + tag: "Regulations Tracker", + }, + { + method: 'GET', + path: '/regulations-tracker/horizon', + summary: "Get Horizon", + requiresAuth: true, + responses: [ + { status: 200, description: "Success" }, + { status: 500, description: "Internal server error" }, + ], + tag: "Regulations Tracker", + }, + { + method: 'GET', + path: '/regulations-tracker/deadlines', + summary: "Get Deadlines", + requiresAuth: true, + responses: [ + { status: 200, description: "Success" }, + { status: 500, description: "Internal server error" }, + ], + tag: "Regulations Tracker", + }, + { + method: 'GET', + path: '/regulations-tracker/frameworks', + summary: "Get Frameworks", + requiresAuth: true, + responses: [ + { status: 200, description: "Success" }, + { status: 500, description: "Internal server error" }, + ], + tag: "Regulations Tracker", + }, + { + method: 'POST', + path: '/regulations-tracker/sync', + summary: "Trigger Sync", + requiresAuth: true, + responses: [ + { status: 200, description: "Success" }, + { status: 500, description: "Internal server error" }, + ], + tag: "Regulations Tracker", + }, +]; + // Reporting endpoints export const reportingEndpoints: Endpoint[] = [ { - method: "GET", - path: "/reporting/generate-report", + method: 'GET', + path: '/reporting/generate-report', summary: "Get All Generated Reports", requiresAuth: true, responses: [ @@ -9503,8 +7858,8 @@ export const reportingEndpoints: Endpoint[] = [ tag: "Reporting", }, { - method: "POST", - path: "/reporting/generate-report", + method: 'POST', + path: '/reporting/generate-report', summary: "Generate Reports", description: "Requires role: Admin", requiresAuth: true, @@ -9517,8 +7872,8 @@ export const reportingEndpoints: Endpoint[] = [ tag: "Reporting", }, { - method: "POST", - path: "/reporting/v2/generate-report", + method: 'POST', + path: '/reporting/v2/generate-report', summary: "Generate Reports V2", description: "Requires role: Admin", requiresAuth: true, @@ -9531,18 +7886,12 @@ export const reportingEndpoints: Endpoint[] = [ tag: "Reporting", }, { - method: "DELETE", - path: "/reporting/{id}", + method: 'DELETE', + path: '/reporting/{id}', summary: "Delete Generated Report By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -9556,8 +7905,8 @@ export const reportingEndpoints: Endpoint[] = [ // Risk Benchmarks endpoints export const riskBenchmarkEndpoints: Endpoint[] = [ { - method: "GET", - path: "/risk-benchmarks", + method: 'GET', + path: '/risk-benchmarks', summary: "Get All Benchmarks", requiresAuth: true, responses: [ @@ -9568,8 +7917,8 @@ export const riskBenchmarkEndpoints: Endpoint[] = [ tag: "Risk Benchmarks", }, { - method: "GET", - path: "/risk-benchmarks/filters", + method: 'GET', + path: '/risk-benchmarks/filters', summary: "Get Benchmark Filters", requiresAuth: true, responses: [ @@ -9580,18 +7929,12 @@ export const riskBenchmarkEndpoints: Endpoint[] = [ tag: "Risk Benchmarks", }, { - method: "GET", - path: "/risk-benchmarks/{id}", + method: 'GET', + path: '/risk-benchmarks/{id}', summary: "Get Benchmark By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -9605,8 +7948,8 @@ export const riskBenchmarkEndpoints: Endpoint[] = [ // Risk History endpoints export const riskHistoryEndpoints: Endpoint[] = [ { - method: "GET", - path: "/riskHistory/timeseries", + method: 'GET', + path: '/riskHistory/timeseries', summary: "Get Timeseries", requiresAuth: true, responses: [ @@ -9617,8 +7960,8 @@ export const riskHistoryEndpoints: Endpoint[] = [ tag: "Risk History", }, { - method: "GET", - path: "/riskHistory/current-counts", + method: 'GET', + path: '/riskHistory/current-counts', summary: "Get Current Counts", requiresAuth: true, responses: [ @@ -9629,8 +7972,8 @@ export const riskHistoryEndpoints: Endpoint[] = [ tag: "Risk History", }, { - method: "POST", - path: "/riskHistory/snapshot", + method: 'POST', + path: '/riskHistory/snapshot', summary: "Create Snapshot", requiresAuth: true, responses: [ @@ -9645,8 +7988,8 @@ export const riskHistoryEndpoints: Endpoint[] = [ // Roles endpoints export const roleEndpoints: Endpoint[] = [ { - method: "GET", - path: "/roles", + method: 'GET', + path: '/roles', summary: "Get All Roles", requiresAuth: true, responses: [ @@ -9657,18 +8000,12 @@ export const roleEndpoints: Endpoint[] = [ tag: "Roles", }, { - method: "GET", - path: "/roles/{id}", + method: 'GET', + path: '/roles/{id}', summary: "Get Role By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -9682,8 +8019,8 @@ export const roleEndpoints: Endpoint[] = [ // Search endpoints export const searchEndpoints: Endpoint[] = [ { - method: "GET", - path: "/search", + method: 'GET', + path: '/search', summary: "Search", requiresAuth: true, responses: [ @@ -9698,8 +8035,8 @@ export const searchEndpoints: Endpoint[] = [ // Settings endpoints export const settingEndpoints: Endpoint[] = [ { - method: "GET", - path: "/feature-settings", + method: 'GET', + path: '/feature-settings', summary: "Get Feature Settings", requiresAuth: true, responses: [ @@ -9710,8 +8047,8 @@ export const settingEndpoints: Endpoint[] = [ tag: "Settings", }, { - method: "PATCH", - path: "/feature-settings", + method: 'PATCH', + path: '/feature-settings', summary: "Update Feature Settings", requiresAuth: true, responses: [ @@ -9726,8 +8063,8 @@ export const settingEndpoints: Endpoint[] = [ // Shadow AI endpoints export const shadowAiEndpoints: Endpoint[] = [ { - method: "GET", - path: "/shadow-ai/api-keys", + method: 'GET', + path: '/shadow-ai/api-keys', summary: "List Api Keys", requiresAuth: true, responses: [ @@ -9738,8 +8075,8 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "POST", - path: "/shadow-ai/api-keys", + method: 'POST', + path: '/shadow-ai/api-keys', summary: "Create Api Key", requiresAuth: true, responses: [ @@ -9750,18 +8087,12 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "DELETE", - path: "/shadow-ai/api-keys/{id}", + method: 'DELETE', + path: '/shadow-ai/api-keys/{id}', summary: "Revoke Api Key", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -9771,18 +8102,12 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "DELETE", - path: "/shadow-ai/api-keys/{id}/permanent", + method: 'DELETE', + path: '/shadow-ai/api-keys/{id}/permanent', summary: "Delete Api Key", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -9792,8 +8117,8 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "GET", - path: "/shadow-ai/insights/summary", + method: 'GET', + path: '/shadow-ai/insights/summary', summary: "Get Insights Summary", requiresAuth: true, responses: [ @@ -9804,8 +8129,8 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "GET", - path: "/shadow-ai/insights/tools-by-events", + method: 'GET', + path: '/shadow-ai/insights/tools-by-events', summary: "Get Tools By Events", requiresAuth: true, responses: [ @@ -9816,8 +8141,8 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "GET", - path: "/shadow-ai/insights/tools-by-users", + method: 'GET', + path: '/shadow-ai/insights/tools-by-users', summary: "Get Tools By Users", requiresAuth: true, responses: [ @@ -9828,8 +8153,8 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "GET", - path: "/shadow-ai/insights/users-by-department", + method: 'GET', + path: '/shadow-ai/insights/users-by-department', summary: "Get Users By Department", requiresAuth: true, responses: [ @@ -9840,8 +8165,8 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "GET", - path: "/shadow-ai/insights/trend", + method: 'GET', + path: '/shadow-ai/insights/trend', summary: "Get Trend", requiresAuth: true, responses: [ @@ -9852,8 +8177,8 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "GET", - path: "/shadow-ai/users", + method: 'GET', + path: '/shadow-ai/users', summary: "Get Users", requiresAuth: true, responses: [ @@ -9864,18 +8189,12 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "GET", - path: "/shadow-ai/users/{email}/activity", + method: 'GET', + path: '/shadow-ai/users/{email}/activity', summary: "Get User Detail", requiresAuth: true, parameters: [ - { - name: "email", - in: "path", - type: "string", - required: true, - description: "The email", - }, + { name: 'email', in: 'path', type: 'string', required: true, description: "The email" }, ], responses: [ { status: 200, description: "Success" }, @@ -9885,8 +8204,8 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "GET", - path: "/shadow-ai/departments", + method: 'GET', + path: '/shadow-ai/departments', summary: "Get Department Activity", requiresAuth: true, responses: [ @@ -9897,8 +8216,8 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "GET", - path: "/shadow-ai/tools", + method: 'GET', + path: '/shadow-ai/tools', summary: "Get Tools", requiresAuth: true, responses: [ @@ -9909,18 +8228,12 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "GET", - path: "/shadow-ai/tools/{id}", + method: 'GET', + path: '/shadow-ai/tools/{id}', summary: "Get Tool By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -9930,18 +8243,12 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "PATCH", - path: "/shadow-ai/tools/{id}/status", + method: 'PATCH', + path: '/shadow-ai/tools/{id}/status', summary: "Update Tool Status", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -9951,18 +8258,12 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "POST", - path: "/shadow-ai/tools/{id}/start-governance", + method: 'POST', + path: '/shadow-ai/tools/{id}/start-governance', summary: "Start Governance", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -9972,8 +8273,8 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "GET", - path: "/shadow-ai/rules", + method: 'GET', + path: '/shadow-ai/rules', summary: "Get Rules", requiresAuth: true, responses: [ @@ -9984,8 +8285,8 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "POST", - path: "/shadow-ai/rules", + method: 'POST', + path: '/shadow-ai/rules', summary: "Create Rule", requiresAuth: true, responses: [ @@ -9996,18 +8297,12 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "PATCH", - path: "/shadow-ai/rules/{id}", + method: 'PATCH', + path: '/shadow-ai/rules/{id}', summary: "Update Rule", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -10017,18 +8312,12 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "DELETE", - path: "/shadow-ai/rules/{id}", + method: 'DELETE', + path: '/shadow-ai/rules/{id}', summary: "Delete Rule", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -10038,8 +8327,8 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "GET", - path: "/shadow-ai/rules/alert-history", + method: 'GET', + path: '/shadow-ai/rules/alert-history', summary: "Get Alert History", requiresAuth: true, responses: [ @@ -10050,8 +8339,8 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "GET", - path: "/shadow-ai/config/syslog", + method: 'GET', + path: '/shadow-ai/config/syslog', summary: "Get Syslog Configs", requiresAuth: true, responses: [ @@ -10062,8 +8351,8 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "POST", - path: "/shadow-ai/config/syslog", + method: 'POST', + path: '/shadow-ai/config/syslog', summary: "Create Syslog Config", requiresAuth: true, responses: [ @@ -10074,18 +8363,12 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "PATCH", - path: "/shadow-ai/config/syslog/{id}", + method: 'PATCH', + path: '/shadow-ai/config/syslog/{id}', summary: "Update Syslog Config", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -10095,18 +8378,12 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "DELETE", - path: "/shadow-ai/config/syslog/{id}", + method: 'DELETE', + path: '/shadow-ai/config/syslog/{id}', summary: "Delete Syslog Config", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -10116,8 +8393,8 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "GET", - path: "/shadow-ai/settings", + method: 'GET', + path: '/shadow-ai/settings', summary: "Get Settings", requiresAuth: true, responses: [ @@ -10128,8 +8405,8 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "PATCH", - path: "/shadow-ai/settings", + method: 'PATCH', + path: '/shadow-ai/settings', summary: "Update Settings", requiresAuth: true, responses: [ @@ -10140,8 +8417,8 @@ export const shadowAiEndpoints: Endpoint[] = [ tag: "Shadow AI", }, { - method: "POST", - path: "/v1/shadow-ai/events", + method: 'POST', + path: '/v1/shadow-ai/events', summary: "Ingest Events", requiresAuth: false, responses: [ @@ -10155,18 +8432,12 @@ export const shadowAiEndpoints: Endpoint[] = [ // Share Links endpoints export const shareLinkEndpoints: Endpoint[] = [ { - method: "GET", - path: "/shares/token/{token}", + method: 'GET', + path: '/shares/token/{token}', summary: "Get Share Link By Token", requiresAuth: false, parameters: [ - { - name: "token", - in: "path", - type: "string", - required: true, - description: "The token", - }, + { name: 'token', in: 'path', type: 'string', required: true, description: "The token" }, ], responses: [ { status: 200, description: "Success" }, @@ -10175,18 +8446,12 @@ export const shareLinkEndpoints: Endpoint[] = [ tag: "Share Links", }, { - method: "GET", - path: "/shares/view/{token}", + method: 'GET', + path: '/shares/view/{token}', summary: "Get Shared Data By Token", requiresAuth: false, parameters: [ - { - name: "token", - in: "path", - type: "string", - required: true, - description: "The token", - }, + { name: 'token', in: 'path', type: 'string', required: true, description: "The token" }, ], responses: [ { status: 200, description: "Success" }, @@ -10195,8 +8460,8 @@ export const shareLinkEndpoints: Endpoint[] = [ tag: "Share Links", }, { - method: "POST", - path: "/shares", + method: 'POST', + path: '/shares', summary: "Create Share Link", requiresAuth: true, responses: [ @@ -10207,25 +8472,13 @@ export const shareLinkEndpoints: Endpoint[] = [ tag: "Share Links", }, { - method: "GET", - path: "/shares/{resourceType}/{resourceId}", + method: 'GET', + path: '/shares/{resourceType}/{resourceId}', summary: "Get Share Links For Resource", requiresAuth: true, parameters: [ - { - name: "resourceType", - in: "path", - type: "string", - required: true, - description: "The resourceType", - }, - { - name: "resourceId", - in: "path", - type: "integer", - required: true, - description: "The resourceId", - }, + { name: 'resourceType', in: 'path', type: 'string', required: true, description: "The resourceType" }, + { name: 'resourceId', in: 'path', type: 'integer', required: true, description: "The resourceId" }, ], responses: [ { status: 200, description: "Success" }, @@ -10235,18 +8488,12 @@ export const shareLinkEndpoints: Endpoint[] = [ tag: "Share Links", }, { - method: "PATCH", - path: "/shares/{id}", + method: 'PATCH', + path: '/shares/{id}', summary: "Update Share Link", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -10256,18 +8503,12 @@ export const shareLinkEndpoints: Endpoint[] = [ tag: "Share Links", }, { - method: "DELETE", - path: "/shares/{id}", + method: 'DELETE', + path: '/shares/{id}', summary: "Delete Share Link", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -10281,8 +8522,8 @@ export const shareLinkEndpoints: Endpoint[] = [ // SSO Config endpoints export const ssoConfigEndpoints: Endpoint[] = [ { - method: "GET", - path: "/ssoConfig/feature", + method: 'GET', + path: '/ssoConfig/feature', summary: "Get S S O Feature Status", requiresAuth: false, responses: [ @@ -10292,8 +8533,8 @@ export const ssoConfigEndpoints: Endpoint[] = [ tag: "SSO Config", }, { - method: "GET", - path: "/ssoConfig/check-status", + method: 'GET', + path: '/ssoConfig/check-status', summary: "Check S S O Status", requiresAuth: false, responses: [ @@ -10303,8 +8544,8 @@ export const ssoConfigEndpoints: Endpoint[] = [ tag: "SSO Config", }, { - method: "GET", - path: "/ssoConfig/orgs", + method: 'GET', + path: '/ssoConfig/orgs', summary: "List S S O Orgs", requiresAuth: false, responses: [ @@ -10314,8 +8555,8 @@ export const ssoConfigEndpoints: Endpoint[] = [ tag: "SSO Config", }, { - method: "GET", - path: "/ssoConfig", + method: 'GET', + path: '/ssoConfig', summary: "Get S S O Config", requiresAuth: true, responses: [ @@ -10325,8 +8566,8 @@ export const ssoConfigEndpoints: Endpoint[] = [ tag: "SSO Config", }, { - method: "PUT", - path: "/ssoConfig", + method: 'PUT', + path: '/ssoConfig', summary: "Save S S O Config", requiresAuth: true, responses: [ @@ -10336,8 +8577,8 @@ export const ssoConfigEndpoints: Endpoint[] = [ tag: "SSO Config", }, { - method: "PUT", - path: "/ssoConfig/enable", + method: 'PUT', + path: '/ssoConfig/enable', summary: "Enable S S O", requiresAuth: true, responses: [ @@ -10347,8 +8588,8 @@ export const ssoConfigEndpoints: Endpoint[] = [ tag: "SSO Config", }, { - method: "PUT", - path: "/ssoConfig/disable", + method: 'PUT', + path: '/ssoConfig/disable', summary: "Disable S S O", requiresAuth: true, responses: [ @@ -10362,18 +8603,12 @@ export const ssoConfigEndpoints: Endpoint[] = [ // Subscriptions endpoints export const subscriptionEndpoints: Endpoint[] = [ { - method: "GET", - path: "/tiers/features/{id}", + method: 'GET', + path: '/tiers/features/{id}', summary: "Get Tiers Features", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -10383,8 +8618,8 @@ export const subscriptionEndpoints: Endpoint[] = [ tag: "Subscriptions", }, { - method: "GET", - path: "/subscriptions", + method: 'GET', + path: '/subscriptions', summary: "Get Subscription Controller", requiresAuth: true, responses: [ @@ -10395,8 +8630,8 @@ export const subscriptionEndpoints: Endpoint[] = [ tag: "Subscriptions", }, { - method: "POST", - path: "/subscriptions", + method: 'POST', + path: '/subscriptions', summary: "Create Subscription Controller", requiresAuth: true, responses: [ @@ -10407,18 +8642,12 @@ export const subscriptionEndpoints: Endpoint[] = [ tag: "Subscriptions", }, { - method: "PUT", - path: "/subscriptions/{id}", + method: 'PUT', + path: '/subscriptions/{id}', summary: "Update Subscription Controller", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -10432,8 +8661,8 @@ export const subscriptionEndpoints: Endpoint[] = [ // Super Admin endpoints export const superAdminEndpoints: Endpoint[] = [ { - method: "GET", - path: "/super-admin/organizations", + method: 'GET', + path: '/super-admin/organizations', summary: "List Organizations", description: "Requires role: Super Admin", requiresAuth: true, @@ -10446,8 +8675,8 @@ export const superAdminEndpoints: Endpoint[] = [ tag: "Super Admin", }, { - method: "POST", - path: "/super-admin/organizations", + method: 'POST', + path: '/super-admin/organizations', summary: "Create Org", description: "Requires role: Super Admin", requiresAuth: true, @@ -10460,19 +8689,13 @@ export const superAdminEndpoints: Endpoint[] = [ tag: "Super Admin", }, { - method: "PATCH", - path: "/super-admin/organizations/{id}", + method: 'PATCH', + path: '/super-admin/organizations/{id}', summary: "Update Org", description: "Requires role: Super Admin", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -10483,19 +8706,13 @@ export const superAdminEndpoints: Endpoint[] = [ tag: "Super Admin", }, { - method: "DELETE", - path: "/super-admin/organizations/{id}", + method: 'DELETE', + path: '/super-admin/organizations/{id}', summary: "Delete Org", description: "Requires role: Super Admin", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -10506,8 +8723,8 @@ export const superAdminEndpoints: Endpoint[] = [ tag: "Super Admin", }, { - method: "GET", - path: "/super-admin/users/count", + method: 'GET', + path: '/super-admin/users/count', summary: "Get User Count", description: "Requires role: Super Admin", requiresAuth: true, @@ -10520,8 +8737,8 @@ export const superAdminEndpoints: Endpoint[] = [ tag: "Super Admin", }, { - method: "GET", - path: "/super-admin/users", + method: 'GET', + path: '/super-admin/users', summary: "List All Users", description: "Requires role: Super Admin", requiresAuth: true, @@ -10534,19 +8751,13 @@ export const superAdminEndpoints: Endpoint[] = [ tag: "Super Admin", }, { - method: "GET", - path: "/super-admin/organizations/{id}/users", + method: 'GET', + path: '/super-admin/organizations/{id}/users', summary: "List Org Users", description: "Requires role: Super Admin", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -10557,19 +8768,13 @@ export const superAdminEndpoints: Endpoint[] = [ tag: "Super Admin", }, { - method: "POST", - path: "/super-admin/organizations/{id}/invite", + method: 'POST', + path: '/super-admin/organizations/{id}/invite', summary: "Invite User To Org", description: "Requires role: Super Admin", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -10580,8 +8785,8 @@ export const superAdminEndpoints: Endpoint[] = [ tag: "Super Admin", }, { - method: "PATCH", - path: "/super-admin/users/{id}", + method: 'PATCH', + path: '/super-admin/users/{id}', summary: "Update User", description: "Requires role: Super Admin", requiresAuth: true, @@ -10592,19 +8797,13 @@ export const superAdminEndpoints: Endpoint[] = [ tag: "Super Admin", }, { - method: "DELETE", - path: "/super-admin/users/{id}", + method: 'DELETE', + path: '/super-admin/users/{id}', summary: "Remove User", description: "Requires role: Super Admin", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -10619,8 +8818,8 @@ export const superAdminEndpoints: Endpoint[] = [ // System endpoints export const systemEndpoints: Endpoint[] = [ { - method: "GET", - path: "/logger/events", + method: 'GET', + path: '/logger/events', summary: "Get Events", requiresAuth: true, responses: [ @@ -10631,8 +8830,8 @@ export const systemEndpoints: Endpoint[] = [ tag: "System", }, { - method: "GET", - path: "/logger/logs", + method: 'GET', + path: '/logger/logs', summary: "Get Logs", requiresAuth: true, responses: [ @@ -10643,8 +8842,8 @@ export const systemEndpoints: Endpoint[] = [ tag: "System", }, { - method: "GET", - path: "/version", + method: 'GET', + path: '/version', summary: "Get application version", requiresAuth: false, responses: [ @@ -10654,8 +8853,8 @@ export const systemEndpoints: Endpoint[] = [ tag: "System", }, { - method: "GET", - path: "/health", + method: 'GET', + path: '/health', summary: "Health Check", requiresAuth: false, responses: [ @@ -10669,8 +8868,8 @@ export const systemEndpoints: Endpoint[] = [ // Tasks endpoints export const taskEndpoints: Endpoint[] = [ { - method: "GET", - path: "/tasks", + method: 'GET', + path: '/tasks', summary: "Get All Tasks", requiresAuth: true, responses: [ @@ -10681,8 +8880,8 @@ export const taskEndpoints: Endpoint[] = [ tag: "Tasks", }, { - method: "POST", - path: "/tasks", + method: 'POST', + path: '/tasks', summary: "Create Task", requiresAuth: true, responses: [ @@ -10693,18 +8892,12 @@ export const taskEndpoints: Endpoint[] = [ tag: "Tasks", }, { - method: "GET", - path: "/tasks/{id}", + method: 'GET', + path: '/tasks/{id}', summary: "Get Task By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -10714,18 +8907,12 @@ export const taskEndpoints: Endpoint[] = [ tag: "Tasks", }, { - method: "PUT", - path: "/tasks/{id}", + method: 'PUT', + path: '/tasks/{id}', summary: "Update Task", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -10735,18 +8922,12 @@ export const taskEndpoints: Endpoint[] = [ tag: "Tasks", }, { - method: "DELETE", - path: "/tasks/{id}", + method: 'DELETE', + path: '/tasks/{id}', summary: "Delete Task", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -10756,18 +8937,12 @@ export const taskEndpoints: Endpoint[] = [ tag: "Tasks", }, { - method: "GET", - path: "/tasks/{id}/entities", + method: 'GET', + path: '/tasks/{id}/entities', summary: "Get Task Entity Links", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -10777,18 +8952,12 @@ export const taskEndpoints: Endpoint[] = [ tag: "Tasks", }, { - method: "POST", - path: "/tasks/{id}/entities", + method: 'POST', + path: '/tasks/{id}/entities', summary: "Add Task Entity Link", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 201, description: "Created successfully" }, @@ -10798,8 +8967,8 @@ export const taskEndpoints: Endpoint[] = [ tag: "Tasks", }, { - method: "PATCH", - path: "/tasks/bulk", + method: 'PATCH', + path: '/tasks/bulk', summary: "Bulk Update Tasks", description: "Requires role: Admin or Editor", requiresAuth: true, @@ -10810,18 +8979,12 @@ export const taskEndpoints: Endpoint[] = [ tag: "Tasks", }, { - method: "PUT", - path: "/tasks/{id}/restore", + method: 'PUT', + path: '/tasks/{id}/restore', summary: "Restore Task", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -10831,18 +8994,12 @@ export const taskEndpoints: Endpoint[] = [ tag: "Tasks", }, { - method: "DELETE", - path: "/tasks/{id}/hard", + method: 'DELETE', + path: '/tasks/{id}/hard', summary: "Hard Delete Task", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -10852,25 +9009,13 @@ export const taskEndpoints: Endpoint[] = [ tag: "Tasks", }, { - method: "DELETE", - path: "/tasks/{id}/entities/{linkId}", + method: 'DELETE', + path: '/tasks/{id}/entities/{linkId}', summary: "Remove Task Entity Link", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, - { - name: "linkId", - in: "path", - type: "integer", - required: true, - description: "The linkId", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, + { name: 'linkId', in: 'path', type: 'integer', required: true, description: "The linkId" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -10884,8 +9029,8 @@ export const taskEndpoints: Endpoint[] = [ // Training endpoints export const trainingEndpoints: Endpoint[] = [ { - method: "GET", - path: "/training", + method: 'GET', + path: '/training', summary: "Get All Training Registar", requiresAuth: true, responses: [ @@ -10896,8 +9041,8 @@ export const trainingEndpoints: Endpoint[] = [ tag: "Training", }, { - method: "POST", - path: "/training", + method: 'POST', + path: '/training', summary: "Create New Training Registar", requiresAuth: true, responses: [ @@ -10908,18 +9053,12 @@ export const trainingEndpoints: Endpoint[] = [ tag: "Training", }, { - method: "GET", - path: "/training/training-id/{id}", + method: 'GET', + path: '/training/training-id/{id}', summary: "Get Training Registar By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -10929,18 +9068,12 @@ export const trainingEndpoints: Endpoint[] = [ tag: "Training", }, { - method: "PATCH", - path: "/training/{id}", + method: 'PATCH', + path: '/training/{id}', summary: "Update Training Registar By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -10950,18 +9083,12 @@ export const trainingEndpoints: Endpoint[] = [ tag: "Training", }, { - method: "DELETE", - path: "/training/{id}", + method: 'DELETE', + path: '/training/{id}', summary: "Delete Training Registar By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Deleted successfully" }, @@ -10975,11 +9102,10 @@ export const trainingEndpoints: Endpoint[] = [ // Users endpoints export const userEndpoints: Endpoint[] = [ { - method: "GET", - path: "/users", + method: 'GET', + path: '/users', summary: "List all users in organization", - description: - "Returns all users belonging to the authenticated user's organization, ordered by created_at DESC, id ASC. Password hashes are excluded.", + description: "Returns all users belonging to the authenticated user's organization, ordered by created_at DESC, id ASC. Password hashes are excluded.", requiresAuth: true, responses: [ { status: 200, description: "Users found" }, @@ -10989,54 +9115,37 @@ export const userEndpoints: Endpoint[] = [ tag: "Users", }, { - method: "GET", - path: "/users/{id}", + method: 'GET', + path: '/users/{id}', summary: "Get user by ID", - description: - "Retrieves a single user by their numeric ID. Super-admins can access any user; regular users can only access users within their organization (or their own record).", + description: "Retrieves a single user by their numeric ID. Super-admins can access any user; regular users can only access users within their organization (or their own record).", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "User ID", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "User ID" }, ], responses: [ { status: 200, description: "User found" }, - { - status: 403, - description: "Access denied (user belongs to different organization)", - }, + { status: 403, description: "Access denied (user belongs to different organization)" }, { status: 404, description: "User not found" }, { status: 500, description: "Internal server error" }, ], tag: "Users", }, { - method: "PATCH", - path: "/users/{id}", + method: 'PATCH', + path: '/users/{id}', summary: "Update user by ID", - description: - "Updates user fields (name, surname, email, roleId, last_login). Only provided fields are updated. Organization isolation enforced. Sends Slack notification on role change. Sends email notification when role changes from Editor (3) to Admin (1).", + description: "Updates user fields (name, surname, email, roleId, last_login). Only provided fields are updated. Organization isolation enforced. Sends Slack notification on role change. Sends email notification when role changes from Editor (3) to Admin (1).", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "User ID to update", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "User ID to update" }, ], requestBody: { - name: "string (optional)", - surname: "string (optional)", - email: "string (optional)", - roleId: "integer (optional)", - last_login: "string (optional)", + "name": "string (optional)", + "surname": "string (optional)", + "email": "string (optional)", + "roleId": "integer (optional)", + "last_login": "string (optional)", }, responses: [ { status: 202, description: "User updated" }, @@ -11048,54 +9157,39 @@ export const userEndpoints: Endpoint[] = [ tag: "Users", }, { - method: "DELETE", - path: "/users/{id}", + method: 'DELETE', + path: '/users/{id}', summary: "Delete user by ID", - description: - "Deletes a user and nullifies all their foreign key references across projects, vendors, risks, vendor risks, files, automations, and invitations. Also removes the user from projects_members. Demo users and super-admins cannot be deleted.", + description: "Deletes a user and nullifies all their foreign key references across projects, vendors, risks, vendor risks, files, automations, and invitations. Also removes the user from projects_members. Demo users and super-admins cannot be deleted.", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "User ID to delete", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "User ID to delete" }, ], responses: [ { status: 202, description: "User deleted" }, - { - status: 403, - description: "Forbidden: demo user, super-admin, or wrong organization", - }, + { status: 403, description: "Forbidden: demo user, super-admin, or wrong organization" }, { status: 404, description: "User not found" }, { status: 500, description: "Internal server error" }, ], tag: "Users", }, { - method: "POST", - path: "/users/register", + method: 'POST', + path: '/users/register', summary: "Register a new user", - description: - "Creates a new user account. Requires a valid registration JWT (set by registerJWT middleware). Validates email uniqueness, password strength, and required fields. Marks any pending invitation as accepted after successful creation.", + description: "Creates a new user account. Requires a valid registration JWT (set by registerJWT middleware). Validates email uniqueness, password strength, and required fields. Marks any pending invitation as accepted after successful creation.", requiresAuth: false, requestBody: { - name: "string (required)", - surname: "string (required)", - email: "string (required)", - password: "string (required)", - roleId: "integer (required)", - organizationId: "integer (required)", + "name": "string (required)", + "surname": "string (required)", + "email": "string (required)", + "password": "string (required)", + "roleId": "integer (required)", + "organizationId": "integer (required)", }, responses: [ { status: 201, description: "User created successfully" }, - { - status: 400, - description: - "Validation error (missing fields, weak password, invalid email)", - }, + { status: 400, description: "Validation error (missing fields, weak password, invalid email)" }, { status: 403, description: "Business logic error" }, { status: 409, description: "User with this email already exists" }, { status: 500, description: "Internal server error" }, @@ -11103,15 +9197,14 @@ export const userEndpoints: Endpoint[] = [ tag: "Users", }, { - method: "POST", - path: "/users/login", + method: 'POST', + path: '/users/login', summary: "Authenticate user", - description: - "Validates email/password credentials via bcrypt. Returns a JWT access token in the response body and sets a refresh token in an HTTP-only cookie. Rate-limited to 5 requests per minute per IP.", + description: "Validates email/password credentials via bcrypt. Returns a JWT access token in the response body and sets a refresh token in an HTTP-only cookie. Rate-limited to 5 requests per minute per IP.", requiresAuth: false, requestBody: { - email: "string (required)", - password: "string (required)", + "email": "string (required)", + "password": "string (required)", }, responses: [ { status: 202, description: "Authentication successful" }, @@ -11122,8 +9215,8 @@ export const userEndpoints: Endpoint[] = [ tag: "Users", }, { - method: "POST", - path: "/users/login-microsoft", + method: 'POST', + path: '/users/login-microsoft', summary: "Login User With Microsoft", requiresAuth: false, responses: [ @@ -11133,11 +9226,10 @@ export const userEndpoints: Endpoint[] = [ tag: "Users", }, { - method: "POST", - path: "/users/refresh-token", + method: 'POST', + path: '/users/refresh-token', summary: "Refresh access token", - description: - "Reads the refresh_token from an HTTP-only cookie and issues a new JWT access token if the refresh token is still valid.", + description: "Reads the refresh_token from an HTTP-only cookie and issues a new JWT access token if the refresh token is still valid.", requiresAuth: false, responses: [ { status: 200, description: "New access token issued" }, @@ -11149,15 +9241,14 @@ export const userEndpoints: Endpoint[] = [ tag: "Users", }, { - method: "POST", - path: "/users/reset-password", + method: 'POST', + path: '/users/reset-password', summary: "Reset user password", - description: - "Resets the password for a user identified by email. Protected by resetPasswordMiddleware (validates reset token/permission). Password is hashed via bcrypt before storage.", + description: "Resets the password for a user identified by email. Protected by resetPasswordMiddleware (validates reset token/permission). Password is hashed via bcrypt before storage.", requiresAuth: false, requestBody: { - email: "string (required)", - newPassword: "string (required)", + "email": "string (required)", + "newPassword": "string (required)", }, responses: [ { status: 202, description: "Password reset successfully" }, @@ -11169,48 +9260,33 @@ export const userEndpoints: Endpoint[] = [ tag: "Users", }, { - method: "PATCH", - path: "/users/chng-pass/{id}", + method: 'PATCH', + path: '/users/chng-pass/{id}', summary: "Change password (authenticated)", - description: - "Changes the password for the authenticated user. Requires the current password for verification. Protected by selfOnly middleware (users can only change their own password). Rate-limited.", + description: "Changes the password for the authenticated user. Requires the current password for verification. Protected by selfOnly middleware (users can only change their own password). Rate-limited.", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: - "User ID (must match authenticated user via selfOnly middleware)", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "User ID (must match authenticated user via selfOnly middleware)" }, ], requestBody: { - id: "integer (required)", - currentPassword: "string (required)", - newPassword: "string (required)", + "id": "integer (required)", + "currentPassword": "string (required)", + "newPassword": "string (required)", }, responses: [ { status: 202, description: "Password changed successfully" }, - { - status: 400, - description: "Validation error (weak password, missing fields)", - }, - { - status: 403, - description: "Business logic error (wrong current password)", - }, + { status: 400, description: "Validation error (weak password, missing fields)" }, + { status: 403, description: "Business logic error (wrong current password)" }, { status: 404, description: "User not found" }, { status: 500, description: "Internal server error" }, ], tag: "Users", }, { - method: "GET", - path: "/users/check/exists", + method: 'GET', + path: '/users/check/exists', summary: "Check if any user exists", - description: - "Returns a boolean indicating whether any user record exists in the database. Used during initial setup flow to determine if onboarding is needed.", + description: "Returns a boolean indicating whether any user record exists in the database. Used during initial setup flow to determine if onboarding is needed.", requiresAuth: true, responses: [ { status: 200, description: "Check result" }, @@ -11219,20 +9295,13 @@ export const userEndpoints: Endpoint[] = [ tag: "Users", }, { - method: "GET", - path: "/users/{id}/calculate-progress", + method: 'GET', + path: '/users/{id}/calculate-progress', summary: "Calculate user project progress", - description: - 'Computes completion metrics across all projects the user is a member of. Calculates subcontrol completion (status="Done") and assessment question completion (has answer) per project and as aggregated totals.', + description: "Computes completion metrics across all projects the user is a member of. Calculates subcontrol completion (status=\"Done\") and assessment question completion (has answer) per project and as aggregated totals.", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "User ID to calculate progress for", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "User ID to calculate progress for" }, ], responses: [ { status: 200, description: "Progress calculated" }, @@ -11241,20 +9310,13 @@ export const userEndpoints: Endpoint[] = [ tag: "Users", }, { - method: "GET", - path: "/users/{id}/profile-photo", + method: 'GET', + path: '/users/{id}/profile-photo', summary: "Get profile photo", - description: - "Returns the profile photo binary content for the specified user. The response includes the raw file content and its MIME type.", + description: "Returns the profile photo binary content for the specified user. The response includes the raw file content and its MIME type.", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "User ID", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "User ID" }, ], responses: [ { status: 200, description: "Profile photo returned" }, @@ -11264,23 +9326,16 @@ export const userEndpoints: Endpoint[] = [ tag: "Users", }, { - method: "POST", - path: "/users/{id}/profile-photo", + method: 'POST', + path: '/users/{id}/profile-photo', summary: "Upload profile photo", - description: - "Uploads a profile photo for the specified user. The file is stored in the tenant-scoped files table. If the user already has a profile photo, the old one is deleted and replaced. Uses multer for multipart file handling.", + description: "Uploads a profile photo for the specified user. The file is stored in the tenant-scoped files table. If the user already has a profile photo, the old one is deleted and replaced. Uses multer for multipart file handling.", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "User ID", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "User ID" }, ], requestBody: { - photo: "string (required)", + "photo": "string (required)", }, responses: [ { status: 200, description: "Profile photo uploaded" }, @@ -11291,20 +9346,13 @@ export const userEndpoints: Endpoint[] = [ tag: "Users", }, { - method: "DELETE", - path: "/users/{id}/profile-photo", + method: 'DELETE', + path: '/users/{id}/profile-photo', summary: "Delete profile photo", - description: - "Removes the profile photo from the user record and deletes the associated file from the files table.", + description: "Removes the profile photo from the user record and deletes the associated file from the files table.", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "User ID", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "User ID" }, ], responses: [ { status: 200, description: "Profile photo deleted" }, @@ -11313,18 +9361,12 @@ export const userEndpoints: Endpoint[] = [ tag: "Users", }, { - method: "GET", - path: "/user-preferences/{userId}", + method: 'GET', + path: '/user-preferences/{userId}', summary: "Get Preferences By User", requiresAuth: true, parameters: [ - { - name: "userId", - in: "path", - type: "integer", - required: true, - description: "The userId", - }, + { name: 'userId', in: 'path', type: 'integer', required: true, description: "The userId" }, ], responses: [ { status: 200, description: "Success" }, @@ -11334,18 +9376,12 @@ export const userEndpoints: Endpoint[] = [ tag: "Users", }, { - method: "PATCH", - path: "/user-preferences/{userId}", + method: 'PATCH', + path: '/user-preferences/{userId}', summary: "Update User Preferences", requiresAuth: true, parameters: [ - { - name: "userId", - in: "path", - type: "integer", - required: true, - description: "The userId", - }, + { name: 'userId', in: 'path', type: 'integer', required: true, description: "The userId" }, ], responses: [ { status: 200, description: "Success" }, @@ -11355,8 +9391,8 @@ export const userEndpoints: Endpoint[] = [ tag: "Users", }, { - method: "POST", - path: "/user-preferences", + method: 'POST', + path: '/user-preferences', summary: "Create User Preferences", requiresAuth: true, responses: [ @@ -11371,11 +9407,10 @@ export const userEndpoints: Endpoint[] = [ // Vendors endpoints export const vendorEndpoints: Endpoint[] = [ { - method: "GET", - path: "/vendors", + method: 'GET', + path: '/vendors', summary: "Get all vendors", - description: - "Retrieves all vendors for the authenticated user's organization, ordered by creation date descending. Each vendor includes its associated project IDs and the reviewer's full name.", + description: "Retrieves all vendors for the authenticated user's organization, ordered by creation date descending. Each vendor includes its associated project IDs and the reviewer's full name.", requiresAuth: true, responses: [ { status: 200, description: "Vendors retrieved successfully" }, @@ -11386,49 +9421,32 @@ export const vendorEndpoints: Endpoint[] = [ tag: "Vendors", }, { - method: "POST", - path: "/vendors", + method: 'POST', + path: '/vendors', summary: "Create a vendor", - description: - "Creates a new vendor in the authenticated user's organization. Validates required fields, checks demo restrictions, associates projects via the vendors_projects join table, records creation in change history, fires automation triggers (vendor_added), and sends in-app assignment notifications to assignee and reviewer.", + description: "Creates a new vendor in the authenticated user's organization. Validates required fields, checks demo restrictions, associates projects via the vendors_projects join table, records creation in change history, fires automation triggers (vendor_added), and sends in-app assignment notifications to assignee and reviewer.", requiresAuth: true, requestBody: { "(schema)": "VendorInput", }, responses: [ { status: 201, description: "Vendor created successfully" }, - { - status: 400, - description: "Validation error (missing or invalid required fields)", - }, + { status: 400, description: "Validation error (missing or invalid required fields)" }, { status: 401, description: "Unauthorized - missing or invalid JWT" }, - { - status: 403, - description: "Business logic error (e.g. demo vendor restriction)", - }, + { status: 403, description: "Business logic error (e.g. demo vendor restriction)" }, { status: 500, description: "Internal server error" }, - { - status: 503, - description: "Service unavailable - vendor creation returned null", - }, + { status: 503, description: "Service unavailable - vendor creation returned null" }, ], tag: "Vendors", }, { - method: "GET", - path: "/vendors/project-id/{id}", + method: 'GET', + path: '/vendors/project-id/{id}', summary: "Get vendors by project ID", - description: - "Retrieves all vendors associated with a specific project. Returns 404 if the project does not exist.", + description: "Retrieves all vendors associated with a specific project. Returns 404 if the project does not exist.", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "Project ID", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "Project ID" }, ], responses: [ { status: 200, description: "Vendors retrieved successfully" }, @@ -11439,20 +9457,13 @@ export const vendorEndpoints: Endpoint[] = [ tag: "Vendors", }, { - method: "GET", - path: "/vendors/{id}", + method: 'GET', + path: '/vendors/{id}', summary: "Get vendor by ID", - description: - "Retrieves a single vendor by its ID, including associated project IDs.", + description: "Retrieves a single vendor by its ID, including associated project IDs.", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "Vendor ID", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "Vendor ID" }, ], responses: [ { status: 200, description: "Vendor retrieved successfully" }, @@ -11463,20 +9474,13 @@ export const vendorEndpoints: Endpoint[] = [ tag: "Vendors", }, { - method: "PATCH", - path: "/vendors/{id}", + method: 'PATCH', + path: '/vendors/{id}', summary: "Update a vendor", - description: - "Partially updates an existing vendor. Only provided fields are updated. Review and scorecard fields can be explicitly set to null to clear them. Required fields (vendor_name, vendor_provides, website, vendor_contact_person) are only updated if they have a non-empty value. Records field-level changes in change history, fires automation triggers (vendor_updated), and sends in-app notifications when assignee or reviewer changes.", + description: "Partially updates an existing vendor. Only provided fields are updated. Review and scorecard fields can be explicitly set to null to clear them. Required fields (vendor_name, vendor_provides, website, vendor_contact_person) are only updated if they have a non-empty value. Records field-level changes in change history, fires automation triggers (vendor_updated), and sends in-app notifications when assignee or reviewer changes.", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "Vendor ID", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "Vendor ID" }, ], requestBody: { "(schema)": "VendorUpdate", @@ -11484,35 +9488,21 @@ export const vendorEndpoints: Endpoint[] = [ responses: [ { status: 202, description: "Vendor updated successfully" }, { status: 400, description: "Validation error" }, - { - status: 401, - description: - "Unauthorized - missing or invalid JWT, or missing userId/role", - }, - { - status: 403, - description: "Business logic error (e.g. demo vendor restriction)", - }, + { status: 401, description: "Unauthorized - missing or invalid JWT, or missing userId/role" }, + { status: 403, description: "Business logic error (e.g. demo vendor restriction)" }, { status: 404, description: "Vendor not found" }, { status: 500, description: "Internal server error" }, ], tag: "Vendors", }, { - method: "DELETE", - path: "/vendors/{id}", + method: 'DELETE', + path: '/vendors/{id}', summary: "Delete a vendor", - description: - "Deletes a vendor and all associated data in a transaction: 1. Deletes vendor risks (vendor_risks table) 2. Deletes project associations (vendors_projects table) 3. Deletes the vendor record itself Fires automation triggers (vendor_deleted).", + description: "Deletes a vendor and all associated data in a transaction: 1. Deletes vendor risks (vendor_risks table) 2. Deletes project associations (vendors_projects table) 3. Deletes the vendor record itself Fires automation triggers (vendor_deleted).", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "Vendor ID", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "Vendor ID" }, ], responses: [ { status: 202, description: "Vendor deleted successfully" }, @@ -11527,25 +9517,13 @@ export const vendorEndpoints: Endpoint[] = [ // Vendor Risks endpoints export const vendorRiskEndpoints: Endpoint[] = [ { - method: "GET", - path: "/vendorRisks/by-projid/{id}", + method: 'GET', + path: '/vendorRisks/by-projid/{id}', summary: "Get All Vendor Risks", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "string", - required: true, - description: "The id", - }, - { - name: "filter", - in: "query", - type: "string", - required: false, - description: "The filter", - }, + { name: 'id', in: 'path', type: 'string', required: true, description: "The id" }, + { name: 'filter', in: 'query', type: 'string', required: false, description: "The filter" }, ], responses: [ { status: 200, description: "Success" }, @@ -11555,25 +9533,13 @@ export const vendorRiskEndpoints: Endpoint[] = [ tag: "Vendor Risks", }, { - method: "GET", - path: "/vendorRisks/by-vendorid/{id}", + method: 'GET', + path: '/vendorRisks/by-vendorid/{id}', summary: "Get All Vendor Risks By Vendor Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, - { - name: "filter", - in: "query", - type: "string", - required: false, - description: "The filter", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, + { name: 'filter', in: 'query', type: 'string', required: false, description: "The filter" }, ], responses: [ { status: 200, description: "Success" }, @@ -11583,8 +9549,8 @@ export const vendorRiskEndpoints: Endpoint[] = [ tag: "Vendor Risks", }, { - method: "GET", - path: "/vendorRisks/by-frameworkid/{id}", + method: 'GET', + path: '/vendorRisks/by-frameworkid/{id}', summary: "Get Vendor Risks By Framework Id", requiresAuth: true, responses: [ @@ -11594,18 +9560,12 @@ export const vendorRiskEndpoints: Endpoint[] = [ tag: "Vendor Risks", }, { - method: "GET", - path: "/vendorRisks/all", + method: 'GET', + path: '/vendorRisks/all', summary: "Get All Vendor Risks All Projects", requiresAuth: true, parameters: [ - { - name: "filter", - in: "query", - type: "string", - required: false, - description: "The filter", - }, + { name: 'filter', in: 'query', type: 'string', required: false, description: "The filter" }, ], responses: [ { status: 200, description: "Success" }, @@ -11615,18 +9575,12 @@ export const vendorRiskEndpoints: Endpoint[] = [ tag: "Vendor Risks", }, { - method: "GET", - path: "/vendorRisks/{id}", + method: 'GET', + path: '/vendorRisks/{id}', summary: "Get Vendor Risk By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 200, description: "Success" }, @@ -11636,18 +9590,12 @@ export const vendorRiskEndpoints: Endpoint[] = [ tag: "Vendor Risks", }, { - method: "PATCH", - path: "/vendorRisks/{id}", + method: 'PATCH', + path: '/vendorRisks/{id}', summary: "Update Vendor Risk By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], requestBody: { "(schema)": "VendorRiskInput", @@ -11660,18 +9608,12 @@ export const vendorRiskEndpoints: Endpoint[] = [ tag: "Vendor Risks", }, { - method: "DELETE", - path: "/vendorRisks/{id}", + method: 'DELETE', + path: '/vendorRisks/{id}', summary: "Delete Vendor Risk By Id", requiresAuth: true, parameters: [ - { - name: "id", - in: "path", - type: "integer", - required: true, - description: "The id", - }, + { name: 'id', in: 'path', type: 'integer', required: true, description: "The id" }, ], responses: [ { status: 202, description: "Accepted" }, @@ -11681,8 +9623,8 @@ export const vendorRiskEndpoints: Endpoint[] = [ tag: "Vendor Risks", }, { - method: "POST", - path: "/vendorRisks", + method: 'POST', + path: '/vendorRisks', summary: "Create Vendor Risk", requiresAuth: true, requestBody: { @@ -11700,8 +9642,8 @@ export const vendorRiskEndpoints: Endpoint[] = [ // Webhooks endpoints export const webhookEndpoints: Endpoint[] = [ { - method: "POST", - path: "/webhooks/github", + method: 'POST', + path: '/webhooks/github', summary: "Github Webhook Controller", requiresAuth: false, responses: [ @@ -11769,6 +9711,7 @@ export const allEndpoints = { projectRisk: projectRiskEndpoints, quantitativeRisk: quantitativeRiskEndpoints, readiness: readinessEndpoints, + regulationsTracker: regulationsTrackerEndpoints, reporting: reportingEndpoints, riskBenchmark: riskBenchmarkEndpoints, riskHistory: riskHistoryEndpoints, diff --git a/docs/superpowers/plans/2026-06-26-regulations-tracker.md b/docs/superpowers/plans/2026-06-26-regulations-tracker.md new file mode 100644 index 0000000000..328c95b942 --- /dev/null +++ b/docs/superpowers/plans/2026-06-26-regulations-tracker.md @@ -0,0 +1,1594 @@ +# Regulations Tracker Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a Regulations Tracker module that polls the public Global AI Regulations feed weekly, detects per-country changes by content hash, and notifies tracking organizations in-app and by email — with a Browse/Tracked/Settings/Detail UI. + +**Architecture:** Mirrors the AI Trust Index module file-for-file. Global reference data (`regulation_countries`, `regulation_tracker_meta` singleton) plus tenant data (`regulation_tracked_countries`, `regulation_tracker_settings`). A BullMQ weekly job fetches + validates the feed, upserts the catalog in one transaction, and fans out notifications to orgs tracking changed countries. + +**Tech Stack:** Express, Sequelize (raw `sequelize.query` + sequelize-typescript models), BullMQ (shared `automation-actions` queue), MJML email, React 19 + React-Query, axios. + +## Global Constraints + +- Migration DDL uses `verifywise.` table prefix; application SQL uses **unqualified** table names (resolved by `search_path = verifywise`). Never cross them. +- Tenant tables (`regulation_tracked_countries`, `regulation_tracker_settings`) always filter by `organization_id = :organizationId` from `req.organizationId`. +- Spacing/UI: sentence case for all UI text; pixel strings for spacing; use VerifyWise components (CustomizableButton with `text=`/`children`, not `label=`); colors from theme. +- Backend response format: `STATUS_CODE[xxx](data)`. Controllers thin; logic in utils. +- All models use `timestamps: false` (explicit columns). +- Feed floor: reject feed if `< 20` countries OR `< 50%` of `last_good_count`. +- Email: configured recipients only, **no admin fallback**. In-app: org Admins ∪ configured `recipient_user_ids`. +- Build before migrate: seed migration needs compiled `dist/`. Run `cd Servers && npm run build` first. +- Pre-PR gates (run from package dir): `cd Servers && npm run build`; `cd Clients && npm run typecheck && npm run i18n:audit:strict && npm run format-check`. +- After adding/changing routes: `cd Servers && npm run generate:swagger && npm run generate:endpoints`. +- Feed URLs: manifest `https://verifywise.ai/api/regulations`; detail `https://verifywise.ai/api/regulations/country/`. `feedVersion === 1`. + +--- + +## Phase 1 — Backend foundation (migration, interfaces, models, seed) + +### Task 1: Database migration — 4 tables + +**Files:** +- Create: `Servers/database/migrations/-create-regulations-tracker-tables.js` (generate `` with `date +%Y%m%d%H%M%S`) + +**Interfaces:** +- Produces tables: `regulation_countries` (global), `regulation_tracked_countries` (tenant), `regulation_tracker_settings` (tenant), `regulation_tracker_meta` (singleton id=1). + +- [ ] **Step 1: Generate the timestamp and create the migration file** + +Run: `cd Servers && date +%Y%m%d%H%M%S` → use the value as ``. Create the file with this content: + +```javascript +"use strict"; + +/** + * Regulations Tracker module tables. + * + * `regulation_countries` and `regulation_tracker_meta` are GLOBAL (no + * organization_id): the Global AI Regulations feed is public reference data, + * identical for every org. Tenancy is enforced only on + * `regulation_tracked_countries` and `regulation_tracker_settings`. + * + * Tracking links to a country by `country_slug` (the feed's stable identity), + * intentionally WITHOUT a foreign key, so a feed re-import can never + * cascade-delete durable user tracking. + */ +module.exports = { + async up(queryInterface) { + await queryInterface.sequelize.query(` + CREATE TABLE IF NOT EXISTS verifywise.regulation_countries ( + id SERIAL PRIMARY KEY, + slug VARCHAR(120) NOT NULL UNIQUE, + name VARCHAR(255) NOT NULL, + region VARCHAR(50), + regulation_count SMALLINT, + data JSONB NOT NULL, + hash VARCHAR(80) NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT TRUE, + removed_at TIMESTAMPTZ, + last_changed_at TIMESTAMPTZ, + last_fetched_at TIMESTAMPTZ + ); + `); + await queryInterface.sequelize.query(` + CREATE INDEX IF NOT EXISTS idx_reg_countries_active_region + ON verifywise.regulation_countries(is_active, region); + CREATE INDEX IF NOT EXISTS idx_reg_countries_name + ON verifywise.regulation_countries(name); + `); + + await queryInterface.sequelize.query(` + CREATE TABLE IF NOT EXISTS verifywise.regulation_tracked_countries ( + id SERIAL PRIMARY KEY, + organization_id INTEGER NOT NULL REFERENCES verifywise.organizations(id) ON DELETE CASCADE, + country_slug VARCHAR(120) NOT NULL, + tracked_by INTEGER REFERENCES verifywise.users(id), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (organization_id, country_slug) + ); + `); + await queryInterface.sequelize.query(` + CREATE INDEX IF NOT EXISTS idx_reg_tracked_org + ON verifywise.regulation_tracked_countries(organization_id); + CREATE INDEX IF NOT EXISTS idx_reg_tracked_slug + ON verifywise.regulation_tracked_countries(country_slug); + `); + + await queryInterface.sequelize.query(` + CREATE TABLE IF NOT EXISTS verifywise.regulation_tracker_settings ( + organization_id INTEGER PRIMARY KEY REFERENCES verifywise.organizations(id) ON DELETE CASCADE, + recipient_user_ids JSONB NOT NULL DEFAULT '[]'::jsonb, + recipient_emails JSONB NOT NULL DEFAULT '[]'::jsonb, + updated_by INTEGER, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ); + `); + + await queryInterface.sequelize.query(` + CREATE TABLE IF NOT EXISTS verifywise.regulation_tracker_meta ( + id INTEGER PRIMARY KEY DEFAULT 1 CHECK (id = 1), + seeded_at TIMESTAMPTZ, + last_good_count INTEGER, + last_run_week VARCHAR(10) + ); + `); + await queryInterface.sequelize.query(` + INSERT INTO verifywise.regulation_tracker_meta (id) + VALUES (1) ON CONFLICT (id) DO NOTHING; + `); + }, + + async down(queryInterface) { + await queryInterface.sequelize.query( + "DROP TABLE IF EXISTS verifywise.regulation_tracked_countries CASCADE", + ); + await queryInterface.sequelize.query( + "DROP TABLE IF EXISTS verifywise.regulation_tracker_settings CASCADE", + ); + await queryInterface.sequelize.query( + "DROP TABLE IF EXISTS verifywise.regulation_tracker_meta CASCADE", + ); + await queryInterface.sequelize.query( + "DROP TABLE IF EXISTS verifywise.regulation_countries CASCADE", + ); + }, +}; +``` + +- [ ] **Step 2: Build and run the migration** + +Run: `cd Servers && npm run build && npx sequelize db:migrate` +Expected: migration name prints `migrated`. + +- [ ] **Step 3: Verify the tables exist** + +Run: `cd Servers && npx sequelize db:migrate:status | grep regulations-tracker` +Expected: shows `up`. + +- [ ] **Step 4: Commit** + +```bash +git add Servers/database/migrations/*-create-regulations-tracker-tables.js +git commit -m "feat(regulations-tracker): add module tables migration" +``` + +--- + +### Task 2: Domain interface + 4 Sequelize models + +**Files:** +- Create: `Servers/domain.layer/interfaces/i.regulationsTracker.ts` +- Create: `Servers/domain.layer/models/regulationsTracker/regulationCountry.model.ts` +- Create: `Servers/domain.layer/models/regulationsTracker/regulationTrackedCountry.model.ts` +- Create: `Servers/domain.layer/models/regulationsTracker/regulationTrackerSettings.model.ts` +- Create: `Servers/domain.layer/models/regulationsTracker/regulationTrackerMeta.model.ts` + +**Interfaces:** +- Produces: `IFeedCountry`, `IFeedChange`, `IRegulationCountry`; models `RegulationCountryModel`, `RegulationTrackedCountryModel`, `RegulationTrackerSettingsModel`, `RegulationTrackerMetaModel`. + +- [ ] **Step 1: Create the interface file** + +`Servers/domain.layer/interfaces/i.regulationsTracker.ts`: + +```typescript +// Subset of the feed shapes we rely on; ignore other fields (additive-safe). + +export type RegulationChange = + | { field: "regulationCount"; from: number; to: number } + | { field: "regulation.status"; regulation: string; from: string; to: string } + | { field: "regulation.effectiveDate"; regulation: string; from: string; to: string } + | { field: "regulation"; change: "added" | "removed"; value: string }; + +export interface IFeedCountryHistory { + firstAssessed: string; + lastChanged: string; + lastChecked: string; + assessmentCount: number; + hashHistory: { date: string; hash: string; regulationCount: number }[]; + lastChange: { date: string; changes: RegulationChange[] } | null; +} + +// The manifest's per-country entry (what we store + hash on). +export interface IManifestCountry { + slug: string; + name: string; + region: string; + regulationCount: number; + hash: string; + history: IFeedCountryHistory | null; + url: string; +} + +export interface IManifest { + feedVersion: number; + generatedAt: string; + meta: Record; + counts: Record; + countries: IManifestCountry[]; +} + +// Row shape for the global catalog table. +export interface IRegulationCountry { + id?: number; + slug: string; + name: string; + region?: string | null; + regulation_count?: number | null; + data: IManifestCountry; + hash: string; + is_active: boolean; + removed_at?: Date | null; + last_changed_at?: Date | null; + last_fetched_at?: Date | null; +} +``` + +- [ ] **Step 2: Create the catalog model** + +`Servers/domain.layer/models/regulationsTracker/regulationCountry.model.ts`: + +```typescript +import { Column, DataType, Model, Table } from "sequelize-typescript"; +import { IManifestCountry, IRegulationCountry } from "../../interfaces/i.regulationsTracker"; + +@Table({ tableName: "regulation_countries", timestamps: false }) +export class RegulationCountryModel + extends Model + implements IRegulationCountry +{ + @Column({ type: DataType.INTEGER, autoIncrement: true, primaryKey: true }) + id?: number; + + @Column({ type: DataType.STRING(120), allowNull: false }) + slug!: string; + + @Column({ type: DataType.STRING(255), allowNull: false }) + name!: string; + + @Column({ type: DataType.STRING(50), allowNull: true }) + region?: string | null; + + @Column({ type: DataType.SMALLINT, allowNull: true }) + regulation_count?: number | null; + + @Column({ type: DataType.JSONB, allowNull: false }) + data!: IManifestCountry; + + @Column({ type: DataType.STRING(80), allowNull: false }) + hash!: string; + + @Column({ type: DataType.BOOLEAN, allowNull: false, defaultValue: true }) + is_active!: boolean; + + @Column({ type: DataType.DATE, allowNull: true }) + removed_at?: Date | null; + + @Column({ type: DataType.DATE, allowNull: true }) + last_changed_at?: Date | null; + + @Column({ type: DataType.DATE, allowNull: true }) + last_fetched_at?: Date | null; +} +``` + +- [ ] **Step 3: Create the tracked-country, settings, and meta models** + +`Servers/domain.layer/models/regulationsTracker/regulationTrackedCountry.model.ts`: + +```typescript +import { Column, DataType, Model, Table } from "sequelize-typescript"; + +@Table({ tableName: "regulation_tracked_countries", timestamps: false }) +export class RegulationTrackedCountryModel extends Model { + @Column({ type: DataType.INTEGER, autoIncrement: true, primaryKey: true }) + id?: number; + + @Column({ type: DataType.INTEGER, allowNull: false }) + organization_id!: number; + + @Column({ type: DataType.STRING(120), allowNull: false }) + country_slug!: string; + + @Column({ type: DataType.INTEGER, allowNull: true }) + tracked_by?: number; + + @Column({ type: DataType.DATE, allowNull: false }) + created_at?: Date; +} +``` + +`Servers/domain.layer/models/regulationsTracker/regulationTrackerSettings.model.ts`: + +```typescript +import { Column, DataType, Model, Table } from "sequelize-typescript"; + +@Table({ tableName: "regulation_tracker_settings", timestamps: false }) +export class RegulationTrackerSettingsModel extends Model { + @Column({ type: DataType.INTEGER, primaryKey: true }) + organization_id!: number; + + @Column({ type: DataType.JSONB, allowNull: false, defaultValue: [] }) + recipient_user_ids!: number[]; + + @Column({ type: DataType.JSONB, allowNull: false, defaultValue: [] }) + recipient_emails!: string[]; + + @Column({ type: DataType.INTEGER, allowNull: true }) + updated_by?: number; + + @Column({ type: DataType.DATE, allowNull: false }) + updated_at?: Date; +} +``` + +`Servers/domain.layer/models/regulationsTracker/regulationTrackerMeta.model.ts`: + +```typescript +import { Column, DataType, Model, Table } from "sequelize-typescript"; + +@Table({ tableName: "regulation_tracker_meta", timestamps: false }) +export class RegulationTrackerMetaModel extends Model { + @Column({ type: DataType.INTEGER, primaryKey: true }) + id!: number; + + @Column({ type: DataType.DATE, allowNull: true }) + seeded_at?: Date | null; + + @Column({ type: DataType.INTEGER, allowNull: true }) + last_good_count?: number | null; + + @Column({ type: DataType.STRING(10), allowNull: true }) + last_run_week?: string | null; +} +``` + +- [ ] **Step 4: Register models in the Sequelize instance** + +Find where AI Trust Index models are registered: `cd Servers && grep -rn "AiTrustIndexAppModel" database/db.ts`. Add the four new models to the same `models: [...]` array (import them at the top of `database/db.ts`). + +- [ ] **Step 5: Build to verify types compile** + +Run: `cd Servers && npm run build` +Expected: build succeeds, no TS errors. + +- [ ] **Step 6: Commit** + +```bash +git add Servers/domain.layer/interfaces/i.regulationsTracker.ts Servers/domain.layer/models/regulationsTracker/ Servers/database/db.ts +git commit -m "feat(regulations-tracker): add interface and Sequelize models" +``` + +--- + +### Task 3: Seed snapshot + seed migration + +**Files:** +- Create: `Servers/database/seeds/regulations-tracker-snapshot.json` +- Create: `Servers/database/migrations/-seed-regulations-tracker-snapshot.js` + +**Interfaces:** +- Consumes: `regulation_countries` table, `regulation_tracker_meta`. +- Produces: baselined catalog so the first weekly run notifies nothing. + +- [ ] **Step 1: Generate the snapshot JSON from the live feed** + +Run this to fetch and save the current manifest countries as the seed (one-time author step): + +```bash +cd Servers && node -e " +const https=require('https'); +https.get('https://verifywise.ai/api/regulations',r=>{let d='';r.on('data',c=>d+=c);r.on('end',()=>{ + const m=JSON.parse(d); + const out={ feedVersion:m.feedVersion, generatedAt:m.generatedAt, countries:m.countries }; + require('fs').writeFileSync('database/seeds/regulations-tracker-snapshot.json', JSON.stringify(out,null,2)); + console.log('wrote', out.countries.length, 'countries'); +});}); +" +``` +Expected: `wrote 60 countries` (or similar; must be ≥ 20). + +- [ ] **Step 2: Create the seed migration** + +`Servers/database/migrations/-seed-regulations-tracker-snapshot.js` (new `` AFTER Task 1's): + +```javascript +"use strict"; + +/** + * Seed the regulation_countries catalog from a committed snapshot on first + * install. Idempotent: skips if the table is already populated. Establishes the + * baseline so the first weekly sync detects no changes (no false notifications). + */ +const fs = require("fs"); +const path = require("path"); + +module.exports = { + async up(queryInterface) { + const existing = await queryInterface.sequelize.query( + "SELECT COUNT(*)::int AS n FROM verifywise.regulation_countries", + { type: queryInterface.sequelize.QueryTypes.SELECT }, + ); + if (existing[0].n > 0) return; // already seeded + + const snapshotPath = path.join(__dirname, "../seeds/regulations-tracker-snapshot.json"); + const snapshot = JSON.parse(fs.readFileSync(snapshotPath, "utf8")); + + for (const c of snapshot.countries) { + await queryInterface.sequelize.query( + `INSERT INTO verifywise.regulation_countries + (slug, name, region, regulation_count, data, hash, is_active, last_fetched_at) + VALUES (:slug, :name, :region, :regulation_count, :data::jsonb, :hash, TRUE, NOW()) + ON CONFLICT (slug) DO NOTHING`, + { + replacements: { + slug: c.slug, + name: c.name, + region: c.region ?? null, + regulation_count: c.regulationCount ?? null, + data: JSON.stringify(c), + hash: c.hash, + }, + }, + ); + } + + await queryInterface.sequelize.query( + `UPDATE verifywise.regulation_tracker_meta + SET seeded_at = NOW(), last_good_count = :count + WHERE id = 1`, + { replacements: { count: snapshot.countries.length } }, + ); + }, + + async down(queryInterface) { + await queryInterface.sequelize.query("DELETE FROM verifywise.regulation_countries"); + await queryInterface.sequelize.query( + "UPDATE verifywise.regulation_tracker_meta SET seeded_at = NULL, last_good_count = NULL WHERE id = 1", + ); + }, +}; +``` + +- [ ] **Step 3: Build and run** + +Run: `cd Servers && npm run build && npx sequelize db:migrate` +Expected: seed migration `migrated`. + +- [ ] **Step 4: Verify seed loaded and meta baselined** + +Run: `cd Servers && node -e "const{sequelize}=require('./dist/database/db');(async()=>{await sequelize.query('SET search_path=verifywise');const[c]=await sequelize.query('SELECT count(*)::int n FROM regulation_countries');const[m]=await sequelize.query('SELECT seeded_at,last_good_count FROM regulation_tracker_meta WHERE id=1');console.log('countries',c[0].n,'meta',JSON.stringify(m[0]));process.exit(0)})()"` +Expected: `countries 60 meta {"seeded_at":"...","last_good_count":60}`. + +- [ ] **Step 5: Commit** + +```bash +git add Servers/database/seeds/regulations-tracker-snapshot.json Servers/database/migrations/*-seed-regulations-tracker-snapshot.js +git commit -m "feat(regulations-tracker): seed country catalog snapshot" +``` + +--- + +## Phase 2 — Feed fetch/validation + utils + +### Task 4: Feed module (fetch + validate) + +**Files:** +- Create: `Servers/utils/regulationsTrackerFeed.ts` +- Test: `Servers/utils/__tests__/regulationsTrackerFeed.test.ts` + +**Interfaces:** +- Produces: `fetchManifest(deps?)`, `validateManifest(raw, lastGoodCount)`, `fetchCountryDetail(slug, deps?)`, `ValidateResult`, `MANIFEST_URL`, `EXPECTED_FEED_VERSION`, `ABSOLUTE_FLOOR`. + +- [ ] **Step 1: Write the failing test** + +`Servers/utils/__tests__/regulationsTrackerFeed.test.ts`: + +```typescript +import { validateManifest, ABSOLUTE_FLOOR } from "../regulationsTrackerFeed"; + +function makeCountry(slug: string) { + return { slug, name: slug, region: "europe", regulationCount: 1, hash: "sha256-x", history: null, url: `/c/${slug}` }; +} +function manifest(n: number, extra: Record = {}) { + return { + feedVersion: 1, + generatedAt: "2026-06-25T00:00:00Z", + counts: { countries: n }, + countries: Array.from({ length: n }, (_, i) => makeCountry("c" + i)), + ...extra, + }; +} + +describe("validateManifest", () => { + it("rejects wrong feedVersion", () => { + const r = validateManifest(manifest(30, { feedVersion: 2 }), null); + expect(r.ok).toBe(false); + }); + it("rejects below absolute floor", () => { + const r = validateManifest(manifest(ABSOLUTE_FLOOR - 1), null); + expect(r.ok).toBe(false); + }); + it("rejects below 50% of last good count", () => { + const r = validateManifest(manifest(30), 100); + expect(r.ok).toBe(false); + }); + it("accepts a healthy feed and returns presentSlugs + rawCount", () => { + const r = validateManifest(manifest(30), 40); + expect(r.ok).toBe(true); + if (r.ok) { + expect(r.countries.length).toBe(30); + expect(r.presentSlugs.length).toBe(30); + expect(r.rawCount).toBe(30); + } + }); + it("keeps a present-but-malformed country in presentSlugs but not in valid countries", () => { + const m = manifest(25); + (m.countries as any[]).push({ slug: "broken" }); // missing hash/name + m.counts.countries = m.countries.length; + const r = validateManifest(m, null); + expect(r.ok).toBe(true); + if (r.ok) { + expect(r.presentSlugs).toContain("broken"); + expect(r.countries.find((c) => c.slug === "broken")).toBeUndefined(); + } + }); +}); +``` + +- [ ] **Step 2: Run to verify it fails** + +Run: `cd Servers && npm test -- regulationsTrackerFeed` +Expected: FAIL — module not found. + +- [ ] **Step 3: Implement the feed module** + +`Servers/utils/regulationsTrackerFeed.ts`: + +```typescript +import axios from "axios"; +import { IManifest, IManifestCountry } from "../domain.layer/interfaces/i.regulationsTracker"; + +export const FEED_ORIGIN = "https://verifywise.ai"; +export const MANIFEST_URL = `${FEED_ORIGIN}/api/regulations`; +export const EXPECTED_FEED_VERSION = 1; +export const ABSOLUTE_FLOOR = 20; + +const REQUIRED_KEYS: (keyof IManifestCountry)[] = ["slug", "name", "region", "hash"]; + +function hasRequired(c: any): c is IManifestCountry { + return c && typeof c === "object" && REQUIRED_KEYS.every((k) => c[k] !== undefined && c[k] !== null); +} + +function normalizeSlug(s: string): string { + return String(s).trim().toLowerCase(); +} + +export type ValidateResult = + | { ok: true; countries: IManifestCountry[]; presentSlugs: string[]; rawCount: number; generatedAt: string } + | { ok: false; reason: string }; + +export function validateManifest(raw: unknown, lastGoodCount: number | null): ValidateResult { + if (!raw || typeof raw !== "object") return { ok: false, reason: "feed is not an object" }; + const f = raw as Record; + if (f.feedVersion !== EXPECTED_FEED_VERSION) + return { ok: false, reason: `unsupported feedVersion ${String(f.feedVersion)}` }; + if (!Array.isArray(f.countries)) return { ok: false, reason: "countries is not an array" }; + const counts = (f.counts as Record) ?? {}; + if (typeof counts.countries === "number" && counts.countries !== f.countries.length) + return { ok: false, reason: `counts.countries (${counts.countries}) != length (${f.countries.length})` }; + if (f.countries.length < ABSOLUTE_FLOOR) + return { ok: false, reason: `below absolute floor (${f.countries.length})` }; + if (lastGoodCount != null && f.countries.length < lastGoodCount * 0.5) + return { ok: false, reason: `below 50% of last good count (${f.countries.length} < ${lastGoodCount})` }; + + const countries = (f.countries as unknown[]).filter(hasRequired) as IManifestCountry[]; + const presentSlugs = (f.countries as unknown[]) + .map((c) => + c && typeof c === "object" && typeof (c as Record).slug === "string" + ? normalizeSlug((c as Record).slug as string) + : null, + ) + .filter((s): s is string => !!s); + return { + ok: true, + countries, + presentSlugs, + rawCount: f.countries.length, + generatedAt: typeof f.generatedAt === "string" ? f.generatedAt : new Date().toISOString(), + }; +} + +export async function fetchManifest(deps?: { + get?: (url: string) => Promise<{ status: number; data: unknown }>; +}): Promise { + const get = deps?.get ?? ((url: string) => axios.get(url, { timeout: 20000 })); + const res = await get(MANIFEST_URL); + if (res.status !== 200) throw new Error(`manifest HTTP ${res.status}`); + return res.data; +} + +export async function fetchCountryDetail( + slug: string, + deps?: { get?: (url: string) => Promise<{ status: number; data: unknown }> }, +): Promise { + const get = deps?.get ?? ((url: string) => axios.get(url, { timeout: 10000 })); + const res = await get(`${FEED_ORIGIN}/api/regulations/country/${encodeURIComponent(slug)}`); + if (res.status !== 200) throw new Error(`country detail HTTP ${res.status}`); + return res.data; +} +``` + +- [ ] **Step 4: Run to verify it passes** + +Run: `cd Servers && npm test -- regulationsTrackerFeed` +Expected: PASS (5 tests). + +- [ ] **Step 5: Commit** + +```bash +git add Servers/utils/regulationsTrackerFeed.ts Servers/utils/__tests__/regulationsTrackerFeed.test.ts +git commit -m "feat(regulations-tracker): add feed fetch and validation" +``` + +--- + +### Task 5: Utils — rendering, currentIsoWeek, upsertFeedTx + +**Files:** +- Create: `Servers/utils/regulationsTracker.utils.ts` +- Test: `Servers/utils/__tests__/regulationsTracker.utils.test.ts` + +**Interfaces:** +- Consumes: `validateManifest` result types, models, `IManifestCountry`, `RegulationChange`. +- Produces: `renderChangeLine(c)`, `currentIsoWeek(date)`, `escapeHtml(s)`, `getMetaQuery()`, `upsertFeedTx(countries, presentSlugs, rawCount)` → `{ changed: CountryChange[]; newlyRemoved: string[]; wasFirstSeed: boolean }`, `CountryChange = { slug: string; name: string; lines: string[]; unstructured: boolean }`. + +- [ ] **Step 1: Write the failing test (pure functions)** + +`Servers/utils/__tests__/regulationsTracker.utils.test.ts`: + +```typescript +import { renderChangeLine, currentIsoWeek, escapeHtml } from "../regulationsTracker.utils"; + +describe("renderChangeLine", () => { + it("renders status change", () => { + expect(renderChangeLine({ field: "regulation.status", regulation: "EU AI Act", from: "proposed", to: "in-force" })) + .toBe("EU AI Act: status proposed → in-force"); + }); + it("renders effective date change", () => { + expect(renderChangeLine({ field: "regulation.effectiveDate", regulation: "X", from: "2024", to: "2026" })) + .toBe("X: effective date 2024 → 2026"); + }); + it("renders added/removed", () => { + expect(renderChangeLine({ field: "regulation", change: "added", value: "New Bill" })).toBe("Added: New Bill"); + expect(renderChangeLine({ field: "regulation", change: "removed", value: "Old Bill" })).toBe("Removed: Old Bill"); + }); +}); + +describe("currentIsoWeek", () => { + it("returns YYYY-Www format", () => { + expect(currentIsoWeek(new Date("2026-06-25T00:00:00Z"))).toMatch(/^\d{4}-W\d{2}$/); + }); +}); + +describe("escapeHtml", () => { + it("escapes HTML metacharacters", () => { + expect(escapeHtml('"&\'')).toBe("<b>"&'"); + }); +}); +``` + +- [ ] **Step 2: Run to verify it fails** + +Run: `cd Servers && npm test -- regulationsTracker.utils` +Expected: FAIL — module not found. + +- [ ] **Step 3: Implement the utils file** + +`Servers/utils/regulationsTracker.utils.ts`: + +```typescript +import { QueryTypes } from "sequelize"; +import { sequelize } from "../database/db"; +import logger from "./logger/fileLogger"; +import { IManifestCountry, RegulationChange } from "../domain.layer/interfaces/i.regulationsTracker"; + +export function renderChangeLine(c: RegulationChange): string { + switch (c.field) { + case "regulation.status": + return `${c.regulation}: status ${c.from} → ${c.to}`; + case "regulation.effectiveDate": + return `${c.regulation}: effective date ${c.from} → ${c.to}`; + case "regulation": + return c.change === "added" ? `Added: ${c.value}` : `Removed: ${c.value}`; + case "regulationCount": + return `Regulation count ${c.from} → ${c.to}`; + default: + return JSON.stringify(c); + } +} + +export function escapeHtml(s: string): string { + return String(s) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} + +// ISO-8601 week, e.g. "2026-W26". Matches the AI Trust Index week-idempotency key. +export function currentIsoWeek(date: Date): string { + const d = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); + const dayNum = d.getUTCDay() === 0 ? 7 : d.getUTCDay(); + d.setUTCDate(d.getUTCDate() + 4 - dayNum); + const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); + const week = Math.ceil(((d.getTime() - yearStart.getTime()) / 86400000 + 1) / 7); + return `${d.getUTCFullYear()}-W${String(week).padStart(2, "0")}`; +} + +function normalizeSlug(s: string): string { + return String(s).trim().toLowerCase(); +} + +export async function getMetaQuery(): Promise<{ + seeded_at: Date | null; + last_good_count: number | null; + last_run_week: string | null; +}> { + const rows = (await sequelize.query( + `SELECT seeded_at, last_good_count, last_run_week FROM regulation_tracker_meta WHERE id = 1;`, + { type: QueryTypes.SELECT }, + )) as any[]; + return rows[0] ?? { seeded_at: null, last_good_count: null, last_run_week: null }; +} + +export interface CountryChange { + slug: string; + name: string; + lines: string[]; + unstructured: boolean; +} + +export async function upsertFeedTx( + countries: IManifestCountry[], + presentSlugs?: string[], + rawCount?: number, +): Promise<{ changed: CountryChange[]; newlyRemoved: string[]; wasFirstSeed: boolean }> { + if (!countries.length) return { changed: [], newlyRemoved: [], wasFirstSeed: false }; + + const changed: CountryChange[] = []; + const newlyRemoved: string[] = []; + let wasFirstSeed = false; + + await sequelize.transaction(async (transaction) => { + const metaRows = (await sequelize.query( + `SELECT seeded_at FROM regulation_tracker_meta WHERE id = 1 FOR UPDATE;`, + { type: QueryTypes.SELECT, transaction }, + )) as any[]; + wasFirstSeed = !metaRows[0]?.seeded_at; + + const upsertedSlugs: string[] = []; + for (const c of countries) { + const slug = normalizeSlug(c.slug); + upsertedSlugs.push(slug); + const existing = (await sequelize.query( + `SELECT hash FROM regulation_countries WHERE slug = :slug;`, + { replacements: { slug }, type: QueryTypes.SELECT, transaction }, + )) as any[]; + + if (existing.length) { + const hashMoved = existing[0].hash !== c.hash; + if (hashMoved) { + const lc = c.history?.lastChange ?? null; + const lines = (lc?.changes ?? []).map(renderChangeLine); + changed.push({ + slug, + name: c.name, + lines: lines.length ? lines : ["Updated — see source"], + unstructured: lines.length === 0, + }); + } + await sequelize.query( + `UPDATE regulation_countries SET + name = :name, region = :region, regulation_count = :rc, + data = :data::jsonb, hash = :hash, is_active = TRUE, removed_at = NULL, + last_fetched_at = NOW() ${hashMoved ? ", last_changed_at = NOW()" : ""} + WHERE slug = :slug;`, + { + replacements: { + slug, name: c.name, region: c.region ?? null, + rc: c.regulationCount ?? null, data: JSON.stringify(c), hash: c.hash, + }, + transaction, + }, + ); + } else { + await sequelize.query( + `INSERT INTO regulation_countries + (slug, name, region, regulation_count, data, hash, is_active, last_changed_at, last_fetched_at) + VALUES (:slug, :name, :region, :rc, :data::jsonb, :hash, TRUE, NOW(), NOW());`, + { + replacements: { + slug, name: c.name, region: c.region ?? null, + rc: c.regulationCount ?? null, data: JSON.stringify(c), hash: c.hash, + }, + transaction, + }, + ); + } + } + + const seenSlugs = Array.from( + new Set([...upsertedSlugs, ...(presentSlugs ?? []).map(normalizeSlug)]), + ); + const removedRows = (await sequelize.query( + `UPDATE regulation_countries + SET is_active = FALSE, removed_at = NOW() + WHERE is_active = TRUE AND slug <> ALL(ARRAY[:seen]::varchar[]) + RETURNING slug;`, + { replacements: { seen: seenSlugs }, type: QueryTypes.SELECT, transaction }, + )) as any[]; + for (const r of removedRows) newlyRemoved.push(r.slug); + + await sequelize.query( + `UPDATE regulation_tracker_meta + SET last_good_count = :count, last_run_week = :week + ${wasFirstSeed ? ", seeded_at = NOW()" : ""} + WHERE id = 1;`, + { replacements: { count: rawCount ?? countries.length, week: currentIsoWeek(new Date()) }, transaction }, + ); + }); + + return { changed, newlyRemoved, wasFirstSeed }; +} +``` + +- [ ] **Step 4: Run to verify pure-function tests pass** + +Run: `cd Servers && npm test -- regulationsTracker.utils` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add Servers/utils/regulationsTracker.utils.ts Servers/utils/__tests__/regulationsTracker.utils.test.ts +git commit -m "feat(regulations-tracker): add rendering, week key, and feed upsert" +``` + +--- + +### Task 6: Utils — CRUD (track/untrack/bulk/settings) + recipient resolution + affected orgs + +**Files:** +- Modify: `Servers/utils/regulationsTracker.utils.ts` (append) +- Test: `Servers/utils/__tests__/regulationsTracker.utils.test.ts` (extend) + +**Interfaces:** +- Produces: `listCountries(filters)`, `getCountryRow(slug)`, `listTracked(orgId)`, `trackCountry(orgId, slug, userId)`, `trackCountriesBulk(orgId, slugs, userId)`, `untrackCountry(orgId, slug)`, `getSettings(orgId)`, `upsertSettings(orgId, userIds, emails, userId)`, `getAffectedOrgsBySlugs(slugs)`, `resolveEmailRecipients(orgId)`, `resolveInAppUserIds(orgId)`. + +- [ ] **Step 1: Append CRUD + resolution functions** + +Append to `Servers/utils/regulationsTracker.utils.ts`: + +```typescript +export async function listCountries(filters: { region?: string; q?: string } = {}) { + const where: string[] = ["is_active = TRUE"]; + const repl: Record = {}; + if (filters.region) { where.push("region = :region"); repl.region = filters.region; } + if (filters.q) { where.push("name ILIKE :q"); repl.q = `%${filters.q}%`; } + return sequelize.query( + `SELECT slug, name, region, regulation_count, hash, last_changed_at + FROM regulation_countries WHERE ${where.join(" AND ")} ORDER BY name ASC;`, + { replacements: repl, type: QueryTypes.SELECT }, + ); +} + +export async function getCountryRow(slug: string) { + const rows = (await sequelize.query( + `SELECT slug, name, region, regulation_count, data, hash, is_active, last_changed_at + FROM regulation_countries WHERE slug = :slug;`, + { replacements: { slug: normalizeSlug(slug) }, type: QueryTypes.SELECT }, + )) as any[]; + return rows[0] ?? null; +} + +export async function listTracked(organizationId: number) { + return sequelize.query( + `SELECT t.country_slug, t.created_at, c.name, c.region, c.regulation_count, c.is_active, c.last_changed_at + FROM regulation_tracked_countries t + LEFT JOIN regulation_countries c ON c.slug = t.country_slug + WHERE t.organization_id = :organizationId ORDER BY c.name ASC;`, + { replacements: { organizationId }, type: QueryTypes.SELECT }, + ); +} + +export async function trackCountry(organizationId: number, slug: string, userId: number) { + await sequelize.query( + `INSERT INTO regulation_tracked_countries (organization_id, country_slug, tracked_by, created_at) + VALUES (:organizationId, :slug, :userId, NOW()) + ON CONFLICT (organization_id, country_slug) DO NOTHING;`, + { replacements: { organizationId, slug: normalizeSlug(slug), userId } }, + ); + return { tracked: true }; +} + +export async function trackCountriesBulk(organizationId: number, slugs: string[], userId: number) { + for (const s of slugs) await trackCountry(organizationId, s, userId); + return { tracked: slugs.length }; +} + +export async function untrackCountry(organizationId: number, slug: string) { + await sequelize.query( + `DELETE FROM regulation_tracked_countries + WHERE organization_id = :organizationId AND country_slug = :slug;`, + { replacements: { organizationId, slug: normalizeSlug(slug) } }, + ); + return { untracked: true }; +} + +export async function getSettings(organizationId: number) { + const rows = (await sequelize.query( + `SELECT recipient_user_ids, recipient_emails, updated_by, updated_at + FROM regulation_tracker_settings WHERE organization_id = :organizationId;`, + { replacements: { organizationId }, type: QueryTypes.SELECT }, + )) as any[]; + return rows[0] ?? { recipient_user_ids: [], recipient_emails: [], updated_by: null, updated_at: null }; +} + +export async function upsertSettings( + organizationId: number, userIds: number[], emails: string[], userId: number, +) { + await sequelize.query( + `INSERT INTO regulation_tracker_settings + (organization_id, recipient_user_ids, recipient_emails, updated_by, updated_at) + VALUES (:organizationId, :userIds::jsonb, :emails::jsonb, :userId, NOW()) + ON CONFLICT (organization_id) DO UPDATE SET + recipient_user_ids = :userIds::jsonb, recipient_emails = :emails::jsonb, + updated_by = :userId, updated_at = NOW();`, + { + replacements: { + organizationId, userId, + userIds: JSON.stringify(userIds ?? []), emails: JSON.stringify(emails ?? []), + }, + }, + ); + return getSettings(organizationId); +} + +export async function getAffectedOrgsBySlugs(slugs: string[]) { + if (!slugs.length) return [] as { organization_id: number; country_slug: string; name: string | null }[]; + return (await sequelize.query( + `SELECT DISTINCT t.organization_id, t.country_slug, c.name + FROM regulation_tracked_countries t + LEFT JOIN regulation_countries c ON c.slug = t.country_slug + WHERE t.country_slug = ANY(ARRAY[:slugs]::varchar[]);`, + { replacements: { slugs }, type: QueryTypes.SELECT }, + )) as { organization_id: number; country_slug: string; name: string | null }[]; +} + +// EMAIL recipients: configured only, NO admin fallback (matches AI Trust Index). +export async function resolveEmailRecipients(organizationId: number): Promise { + const s = await getSettings(organizationId); + const userIds: number[] = s.recipient_user_ids ?? []; + const freeText: string[] = s.recipient_emails ?? []; + let userEmails: string[] = []; + if (userIds.length) { + const rows = (await sequelize.query( + `SELECT email FROM users WHERE organization_id = :organizationId AND id = ANY(ARRAY[:ids]::int[]);`, + { replacements: { organizationId, ids: userIds }, type: QueryTypes.SELECT }, + )) as { email: string }[]; + userEmails = rows.map((r) => r.email); + } + const recipients = Array.from( + new Set([...userEmails, ...freeText].map((e) => e.trim().toLowerCase()).filter(Boolean)), + ); + if (!recipients.length) + logger.info(`[regulations-tracker] org ${organizationId} changed but no email recipients; skipped`); + return recipients; +} + +// IN-APP recipients: org Admins ∪ configured recipient_user_ids (deduped user ids). +export async function resolveInAppUserIds(organizationId: number): Promise { + const s = await getSettings(organizationId); + const configured: number[] = s.recipient_user_ids ?? []; + const admins = (await sequelize.query( + `SELECT u.id FROM users u + JOIN roles r ON r.id = u.role_id + WHERE u.organization_id = :organizationId AND r.name IN ('Admin', 'SuperAdmin');`, + { replacements: { organizationId }, type: QueryTypes.SELECT }, + )) as { id: number }[]; + return Array.from(new Set([...admins.map((a) => a.id), ...configured])); +} +``` + +- [ ] **Step 2: Add an integration test for idempotent track** + +Append to `Servers/utils/__tests__/regulationsTracker.utils.test.ts` (skips if no DB): + +```typescript +// Note: track/untrack/settings are exercised by the controller integration tests +// in Task 11 against a live test DB. Pure-function coverage stays here. +``` + +- [ ] **Step 3: Build to verify compile** + +Run: `cd Servers && npm run build` +Expected: success. + +- [ ] **Step 4: Commit** + +```bash +git add Servers/utils/regulationsTracker.utils.ts Servers/utils/__tests__/regulationsTracker.utils.test.ts +git commit -m "feat(regulations-tracker): add CRUD and recipient resolution utils" +``` + +--- + +## Phase 3 — Weekly job, notifications, email template + +### Task 7: Email digest MJML template + +**Files:** +- Create: `Servers/templates/regulations-tracker-digest.mjml` + +**Interfaces:** +- Consumes: `{{changedSection}}`, `{{removedSection}}`, `{{moduleUrl}}`, `{{trackedUrl}}`, `{{settingsUrl}}` injected by the job. + +- [ ] **Step 1: Create the template (copy ai-trust-index-digest.mjml structure)** + +Run `cd Servers && cat templates/ai-trust-index-digest.mjml` to get the exact house structure, then create `templates/regulations-tracker-digest.mjml` with the same layout but: title "Global AI regulations — weekly update", intro "Regulations changed for countries your organization tracks.", and the two slots `{{changedSection}}` (header "Changed") and `{{removedSection}}` (header "No longer in the feed"), plus buttons linking `{{moduleUrl}}` (Browse), `{{trackedUrl}}`, `{{settingsUrl}}`. Keep all colors/fonts identical to the AI Trust Index template. + +- [ ] **Step 2: Verify it compiles** + +Run: `cd Servers && node -e "const {compileMjmlToHtml}=require('./dist/tools/mjmlCompiler');const fs=require('fs');console.log(compileMjmlToHtml(fs.readFileSync('templates/regulations-tracker-digest.mjml','utf8'),{changedSection:'',removedSection:'',moduleUrl:'#',trackedUrl:'#',settingsUrl:'#'}).slice(0,40))"` (run `npm run build` first if dist is stale) +Expected: prints the start of valid HTML (`` or ` { + it("returns empty string for no items", () => { + expect(sectionMjml("Changed", [])).toBe(""); + }); + it("escapes item names and renders bullet lines", () => { + const out = sectionMjml("Changed", [{ name: "", detail: "status a → b" }]); + expect(out).toContain("<EU>"); + expect(out).toContain("status a → b"); + }); +}); +``` + +- [ ] **Step 2: Run to verify it fails** + +Run: `cd Servers && npm test -- syncRegulationsTracker` +Expected: FAIL — module not found. + +- [ ] **Step 3: Implement the job** + +`Servers/services/automations/actions/syncRegulationsTracker.ts`: + +```typescript +import { promises as fs } from "fs"; +import path from "path"; +import { fetchManifest, validateManifest } from "../../../utils/regulationsTrackerFeed"; +import { + getMetaQuery, + upsertFeedTx, + getAffectedOrgsBySlugs, + resolveEmailRecipients, + resolveInAppUserIds, + currentIsoWeek, + escapeHtml, + CountryChange, +} from "../../../utils/regulationsTracker.utils"; +import { createNotification } from "../../../utils/notification.utils"; +import { sendAutomationEmail } from "../../emailService"; +import { compileMjmlToHtml } from "../../../tools/mjmlCompiler"; +import logger from "../../../utils/logger/fileLogger"; + +const FRONTEND = process.env.FRONTEND_URL ?? "http://localhost:5173"; +const MODULE_URL = FRONTEND + "/regulations-tracker/browse"; +const TRACKED_URL = FRONTEND + "/regulations-tracker/tracked"; +const SETTINGS_URL = FRONTEND + "/regulations-tracker/settings"; + +export interface DigestItem { + name: string; + detail?: string; +} + +export function sectionMjml(title: string, items: DigestItem[]): string { + if (!items.length) return ""; + const header = `${escapeHtml(title)}`; + const lines = items + .map((it) => { + const label = it.detail ? `${it.name} — ${it.detail}` : it.name; + return `• ${escapeHtml(label)}`; + }) + .join(""); + return header + lines; +} + +async function renderDigest(changed: DigestItem[], removed: DigestItem[]): Promise { + const tmplPath = path.join(__dirname, "../../../templates/regulations-tracker-digest.mjml"); + const template = await fs.readFile(tmplPath, "utf8"); + return compileMjmlToHtml(template, { + changedSection: sectionMjml("Changed", changed), + removedSection: sectionMjml("No longer in the feed", removed), + moduleUrl: MODULE_URL, + trackedUrl: TRACKED_URL, + settingsUrl: SETTINGS_URL, + }); +} + +export async function syncRegulationsTracker(deps?: { feed?: unknown }): Promise<{ + fetched: number; + changed: number; + newlyRemoved: number; + orgsEmailed: number; + orgsNotified: number; + skipped?: string; +}> { + const meta = await getMetaQuery(); + const thisWeek = currentIsoWeek(new Date()); + if (meta.last_run_week === thisWeek) + return { fetched: 0, changed: 0, newlyRemoved: 0, orgsEmailed: 0, orgsNotified: 0, skipped: `already ran ${thisWeek}` }; + + let raw: unknown; + try { + raw = deps?.feed ?? (await fetchManifest()); + } catch (e) { + logger.error(`[regulations-tracker] feed fetch failed: ${(e as Error).message}`); + return { fetched: 0, changed: 0, newlyRemoved: 0, orgsEmailed: 0, orgsNotified: 0, skipped: "fetch failed" }; + } + + const validated = validateManifest(raw, meta.last_good_count ?? null); + if (!validated.ok) { + logger.error(`[regulations-tracker] feed rejected: ${validated.reason}`); + return { fetched: 0, changed: 0, newlyRemoved: 0, orgsEmailed: 0, orgsNotified: 0, skipped: validated.reason }; + } + + const { changed, newlyRemoved, wasFirstSeed } = await upsertFeedTx( + validated.countries, validated.presentSlugs, validated.rawCount, + ); + + if (wasFirstSeed) { + logger.info(`[regulations-tracker] first seed (${validated.countries.length}); notifications suppressed`); + return { fetched: validated.countries.length, changed: 0, newlyRemoved: 0, orgsEmailed: 0, orgsNotified: 0 }; + } + + const changeBySlug = new Map(changed.map((c) => [c.slug, c])); + const changedSlugs = Array.from(new Set([...changed.map((c) => c.slug), ...newlyRemoved])); + let orgsEmailed = 0; + let orgsNotified = 0; + + if (changedSlugs.length) { + const affected = await getAffectedOrgsBySlugs(changedSlugs); + const byOrg = new Map(); + for (const row of affected) { + const bucket = byOrg.get(row.organization_id) ?? { changed: [], removed: [], slugs: [] }; + const name = row.name ?? row.country_slug; + bucket.slugs.push(row.country_slug); + if (newlyRemoved.includes(row.country_slug)) { + bucket.removed.push({ name }); + } else { + const ch = changeBySlug.get(row.country_slug); + bucket.changed.push({ name, detail: ch ? ch.lines.join(", ") : undefined }); + } + byOrg.set(row.organization_id, bucket); + } + + for (const [orgId, { changed: ch, removed: rm, slugs }] of byOrg) { + // In-app: always to admins ∪ configured recipients. + const userIds = await resolveInAppUserIds(orgId); + if (userIds.length) { + const title = "AI regulations updated"; + const message = + [...ch.map((i) => i.name), ...rm.map((i) => `${i.name} (removed)`)].join(", "); + for (const uid of userIds) { + await createNotification(orgId, { + user_id: uid, + type: "regulations_tracker", + title, + message, + entity_type: "regulation_country", + entity_id: slugs[0] ?? null, + }); + } + orgsNotified++; + } + // Email: configured recipients only, no fallback. + const emails = await resolveEmailRecipients(orgId); + if (emails.length) { + const html = await renderDigest(ch, rm); + await sendAutomationEmail(emails, "Global AI regulations — weekly update", html, undefined); + orgsEmailed++; + } + } + } + + logger.info( + `[regulations-tracker] done: fetched=${validated.countries.length} changed=${changed.length} removed=${newlyRemoved.length} emailed=${orgsEmailed} notified=${orgsNotified}`, + ); + return { + fetched: validated.countries.length, + changed: changed.length, + newlyRemoved: newlyRemoved.length, + orgsEmailed, + orgsNotified, + }; +} +``` + +**Note for implementer:** verify the exact signature of `createNotification` in `Servers/utils/notification.utils.ts` and adapt the call (param order/shape) to match. The fields used are `user_id, type, title, message, entity_type, entity_id` with `organization_id` passed separately. + +- [ ] **Step 4: Run to verify it passes** + +Run: `cd Servers && npm test -- syncRegulationsTracker` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add Servers/services/automations/actions/syncRegulationsTracker.ts Servers/services/automations/actions/__tests__/syncRegulationsTracker.test.ts +git commit -m "feat(regulations-tracker): add weekly sync job with in-app + email" +``` + +--- + +### Task 9: BullMQ scheduling + worker dispatch + +**Files:** +- Modify: `Servers/services/automations/automationProducer.ts` +- Modify: `Servers/services/automations/automationWorker.ts` +- Modify: `Servers/jobs/producer.ts` + +**Interfaces:** +- Consumes: `syncRegulationsTracker` (Task 8). +- Produces: scheduled repeatable job `regulations_tracker_sync` (weekly `0 6 * * 1` UTC). + +- [ ] **Step 1: Add the scheduler (must NOT obliterate)** + +In `Servers/services/automations/automationProducer.ts`, add (mirror `scheduleAiTrustIndexSync`, do NOT call `automationQueue.obliterate`): + +```typescript +export async function scheduleRegulationsTrackerSync() { + await automationQueue.add( + "regulations_tracker_sync", + {}, + { + repeat: { pattern: "0 6 * * 1", tz: "UTC" }, // Mondays 06:00 UTC + removeOnComplete: true, + removeOnFail: false, + }, + ); +} +``` + +- [ ] **Step 2: Add the worker dispatch branch** + +In `Servers/services/automations/automationWorker.ts`, find the `if (name === "ai_trust_index_sync")` branch and add alongside it: + +```typescript +} else if (name === "regulations_tracker_sync") { + const { syncRegulationsTracker } = await import("./actions/syncRegulationsTracker"); + await syncRegulationsTracker(); +} +``` +(Match the existing import style in that file — if it uses top-of-file imports rather than dynamic import, follow that instead.) + +- [ ] **Step 3: Register in addAllJobs (after any obliterating scheduler)** + +In `Servers/jobs/producer.ts`, import and call inside `addAllJobs()`, placed next to `scheduleAiTrustIndexSync()` (both are non-obliterating and belong near the end): + +```typescript +import { scheduleRegulationsTrackerSync } from "../services/automations/automationProducer"; +// ... inside addAllJobs(), near scheduleAiTrustIndexSync(): +await scheduleRegulationsTrackerSync(); +``` + +- [ ] **Step 4: Build to verify compile** + +Run: `cd Servers && npm run build` +Expected: success. + +- [ ] **Step 5: Manually trigger the job once to smoke-test (idempotent)** + +Run: `cd Servers && node -e "require('./dist/services/automations/actions/syncRegulationsTracker').syncRegulationsTracker().then(r=>{console.log(JSON.stringify(r));process.exit(0)})"` +Expected: `{"skipped":"already ran ..."}` (because the seed set `last_run_week`) OR a result object — either proves it wires up without throwing. + +- [ ] **Step 6: Commit** + +```bash +git add Servers/services/automations/automationProducer.ts Servers/services/automations/automationWorker.ts Servers/jobs/producer.ts +git commit -m "feat(regulations-tracker): schedule weekly sync job" +``` + +--- + +## Phase 4 — Routes + controllers + +### Task 10: Routes + controllers + app registration + +**Files:** +- Create: `Servers/routes/regulationsTracker.route.ts` +- Create: `Servers/controllers/regulationsTracker.ctrl.ts` +- Modify: `Servers/app.ts` + +**Interfaces:** +- Consumes: all Task 6 CRUD utils, `fetchCountryDetail`/`getCountryRow` for the proxy. +- Produces: 8 endpoints under `/api/regulations-tracker`. + +- [ ] **Step 1: Create the controller** + +Run `cd Servers && sed -n '1,40p' controllers/aiTrustIndex.ctrl.ts` to copy the exact imports (`logProcessing`, `logSuccess`, `logFailure`, `STATUS_CODE`, `isAdmin`). Then create `Servers/controllers/regulationsTracker.ctrl.ts` with 8 handlers following the AI Trust Index pattern exactly. Example for two of them (replicate the logging shape for all 8): + +```typescript +import { Request, Response } from "express"; +import { STATUS_CODE } from "../utils/statusCode.utils"; // match aiTrustIndex import path +import { logProcessing, logSuccess, logFailure } from "../utils/logger/logHelper"; +import { isAdmin } from "../utils/roleCheck.utils"; // match aiTrustIndex import path +import { + listCountries, getCountryRow, listTracked, trackCountry, trackCountriesBulk, + untrackCountry, getSettings, upsertSettings, +} from "../utils/regulationsTracker.utils"; +import { fetchCountryDetail } from "../utils/regulationsTrackerFeed"; + +const file = "regulationsTracker.ctrl.ts"; + +export async function getCountries(req: Request, res: Response): Promise { + const fn = "getCountries"; + logProcessing({ description: "list regulation countries", functionName: fn, fileName: file, userId: req.userId!, organizationId: req.organizationId! }); + try { + const data = await listCountries({ region: req.query.region as string, q: req.query.q as string }); + await logSuccess({ eventType: "Read", description: "listed countries", functionName: fn, fileName: file, userId: req.userId!, organizationId: req.organizationId! }); + return res.status(200).json(STATUS_CODE[200](data)); + } catch (error) { + await logFailure({ eventType: "Read", description: "list countries failed", functionName: fn, fileName: file, error: error as Error, userId: req.userId!, organizationId: req.organizationId! }); + return res.status(500).json(STATUS_CODE[500]((error as Error).message)); + } +} + +export async function getCountryDetail(req: Request, res: Response): Promise { + const fn = "getCountryDetail"; + logProcessing({ description: "proxy country detail", functionName: fn, fileName: file, userId: req.userId!, organizationId: req.organizationId! }); + try { + const slug = req.params.slug; + const local = await getCountryRow(slug); + if (!local) return res.status(404).json(STATUS_CODE[404]("country not found")); + try { + const live = await fetchCountryDetail(slug); + return res.status(200).json(STATUS_CODE[200]({ ...(live as object), stale: false })); + } catch { + return res.status(200).json(STATUS_CODE[200]({ country: local.data, stale: true })); + } + } catch (error) { + await logFailure({ eventType: "Read", description: "country detail failed", functionName: fn, fileName: file, error: error as Error, userId: req.userId!, organizationId: req.organizationId! }); + return res.status(500).json(STATUS_CODE[500]((error as Error).message)); + } +} + +// trackCountry / trackBulk / untrack / updateSettings: admin-gated. +// At the top of each: if (!isAdmin(req.role)) return res.status(403).json(STATUS_CODE[403]("forbidden")); +// getTracked / getSettingsCtrl: any authenticated user in the org. +``` + +The implementer must write all 8: `getCountries`, `getCountryDetail`, `getTracked`, `trackCountryCtrl`, `trackBulkCtrl`, `untrackCountryCtrl`, `getSettingsCtrl`, `updateSettingsCtrl` — admin gate on the 4 mutating/settings ones. Verify exact import paths against `aiTrustIndex.ctrl.ts`. + +- [ ] **Step 2: Create the route file** + +`Servers/routes/regulationsTracker.route.ts` (mirror `aiTrustIndex.route.ts`): + +```typescript +import express from "express"; +import authenticateJWT from "../middleware/auth.middleware"; // match aiTrustIndex import +import { + getCountries, getCountryDetail, getTracked, trackCountryCtrl, trackBulkCtrl, + untrackCountryCtrl, getSettingsCtrl, updateSettingsCtrl, +} from "../controllers/regulationsTracker.ctrl"; + +const router = express.Router(); + +router.get("/countries", authenticateJWT, getCountries); +router.get("/countries/:slug", authenticateJWT, getCountryDetail); +router.get("/tracked", authenticateJWT, getTracked); +router.post("/tracked", authenticateJWT, trackCountryCtrl); +router.post("/tracked/bulk", authenticateJWT, trackBulkCtrl); +router.delete("/tracked/:slug", authenticateJWT, untrackCountryCtrl); +router.get("/settings", authenticateJWT, getSettingsCtrl); +router.put("/settings", authenticateJWT, updateSettingsCtrl); + +export default router; +``` + +- [ ] **Step 3: Register in app.ts** + +In `Servers/app.ts`, next to the AI Trust Index registration: + +```typescript +import regulationsTrackerRoutes from "./routes/regulationsTracker.route"; +app.use("/api/regulations-tracker", regulationsTrackerRoutes); +``` + +- [ ] **Step 4: Regenerate API docs** + +Run: `cd Servers && npm run build && npm run generate:swagger && npm run generate:endpoints && npm run check:api-drift` +Expected: no drift error. + +- [ ] **Step 5: Commit** + +```bash +git add Servers/routes/regulationsTracker.route.ts Servers/controllers/regulationsTracker.ctrl.ts Servers/app.ts Servers/swagger.yaml docs/api-docs/src/config/endpoints.ts +git commit -m "feat(regulations-tracker): add routes and controllers" +``` + +--- + +### Task 11: Endpoint integration tests + +**Files:** +- Create: `Servers/controllers/__tests__/regulationsTracker.ctrl.test.ts` + +- [ ] **Step 1: Write tests mirroring aiTrustIndex.ctrl tests** + +Run `cd Servers && ls controllers/__tests__/ | grep -i trust` to find the AI Trust Index controller test, read it, and mirror it: assert `getCountries` returns 200 + array; `trackCountryCtrl` returns 403 for non-admin and 201 for admin; `untrackCountryCtrl` is idempotent (200 when not tracked); tenant isolation (org A's tracked list excludes org B). Use the same mocking/harness the AI Trust Index test uses. + +- [ ] **Step 2: Run the tests** + +Run: `cd Servers && npm test -- regulationsTracker.ctrl` +Expected: PASS. + +- [ ] **Step 3: Commit** + +```bash +git add Servers/controllers/__tests__/regulationsTracker.ctrl.test.ts +git commit -m "test(regulations-tracker): add endpoint integration tests" +``` + +--- + +## Phase 5 — Frontend + +### Task 12: Repository + hooks + +**Files:** +- Create: `Clients/src/application/repository/regulationsTracker.repository.ts` +- Create: `Clients/src/application/hooks/useRegulationsTracker.ts` + +- [ ] **Step 1: Create the repository** + +Read `Clients/src/application/repository/aiTrustIndex.repository.ts` for the exact `apiServices` import + method shape, then create `regulationsTracker.repository.ts` with: `getCountries(params)`, `getCountryDetail(slug)`, `getTracked()`, `trackCountry(slug)`, `trackBulk(slugs)`, `untrackCountry(slug)`, `getSettings()`, `updateSettings(payload)` — all hitting `/regulations-tracker/...`. + +- [ ] **Step 2: Create the hooks** + +Read `Clients/src/application/hooks/useAiTrustIndex.ts` and mirror it: `const KEY = "regulations-tracker"`, read queries use `keepPreviousData`, mutations invalidate `KEY`. Export `useCountries`, `useCountryDetail`, `useTracked`, `useTrackCountry`, `useUntrackCountry`, `useTrackBulk`, `useSettings`, `useUpdateSettings`. + +- [ ] **Step 3: Typecheck** + +Run: `cd Clients && npm run typecheck` +Expected: no errors. + +- [ ] **Step 4: Commit** + +```bash +git add Clients/src/application/repository/regulationsTracker.repository.ts Clients/src/application/hooks/useRegulationsTracker.ts +git commit -m "feat(regulations-tracker): add frontend repository and hooks" +``` + +--- + +### Task 13: Pages (Browse / Tracked / Settings / Detail) + sidebar + context + +**Files:** +- Create: `Clients/src/application/contexts/RegulationsTrackerSidebar.context.tsx` +- Create: `Clients/src/presentation/pages/RegulationsTracker/index.tsx` +- Create: `Clients/src/presentation/pages/RegulationsTracker/Browse/index.tsx` +- Create: `Clients/src/presentation/pages/RegulationsTracker/Tracked/index.tsx` +- Create: `Clients/src/presentation/pages/RegulationsTracker/Settings/index.tsx` +- Create: `Clients/src/presentation/pages/RegulationsTracker/CountryDetail/index.tsx` +- Create: `Clients/src/presentation/pages/RegulationsTracker/RegulationsTrackerSidebar.tsx` + +- [ ] **Step 1: Mirror the AI Trust Index page tree** + +Read each corresponding `Clients/src/presentation/pages/AITrustIndex/*` file and the `AITrustIndexSidebar.context.tsx` + `AITrustIndexSidebar.tsx`, and replicate for Regulations Tracker. Browse lists countries (grouped/filterable by region) with a Track button (`useTrackCountry`); Tracked lists tracked countries with Untrack; Settings edits recipient_user_ids + recipient_emails (use existing user-picker + ChipInput patterns); CountryDetail renders regulations + timeline + change history from `useCountryDetail`, and shows the feed disclaimer verbatim. Use VerifyWise components only; sentence case; pixel spacing. + +- [ ] **Step 2: Typecheck + i18n audit** + +Run: `cd Clients && npm run typecheck && npm run i18n:audit:strict` +Expected: no errors. (Add any new UI strings to `i18n/translations.ts` for de/fr/es if the audit flags them.) + +- [ ] **Step 3: Commit** + +```bash +git add Clients/src/application/contexts/RegulationsTrackerSidebar.context.tsx Clients/src/presentation/pages/RegulationsTracker/ +git commit -m "feat(regulations-tracker): add Browse/Tracked/Settings/Detail pages" +``` + +--- + +### Task 14: Route registration + navigation entry + +**Files:** +- Modify: `Clients/src/application/config/routes.tsx` +- Modify: the sidebar/nav file that lists modules (find via `grep -rn "ai-trust-index" Clients/src/presentation --include=*.tsx -l`) + +- [ ] **Step 1: Add lazy imports + routes** + +In `routes.tsx`, mirror the AI Trust Index entries (lazy imports for the 4 pages + `` registrations under `/regulations-tracker`, with the bare path redirecting to `/regulations-tracker/browse`). + +- [ ] **Step 2: Add the nav entry** + +Add a "Regulations tracker" entry to the same navigation/sidebar config that lists AI Trust Index, pointing at `/regulations-tracker`. + +- [ ] **Step 3: Typecheck + build** + +Run: `cd Clients && npm run typecheck && npm run build` +Expected: success. + +- [ ] **Step 4: Commit** + +```bash +git add Clients/src/application/config/routes.tsx Clients/src/presentation +git commit -m "feat(regulations-tracker): register routes and navigation" +``` + +--- + +### Task 15: Final gates + manual verification + +- [ ] **Step 1: Run all backend + frontend gates** + +Run: +```bash +cd Servers && npm run build && npm test -- regulationsTracker syncRegulationsTracker +cd ../Clients && npm run typecheck && npm run i18n:audit:strict && npm run format-check +``` +Expected: all pass. + +- [ ] **Step 2: Manual smoke test (run app, verify UI)** + +Use the `/run-verifywise` skill (or `run` skill) to start the app. Log in, open Regulations tracker → Browse (countries load), Track a country, see it in Tracked, set a recipient in Settings, open a country Detail (regulations + timeline render). Confirm no console errors. + +- [ ] **Step 3: Commit any fixes from manual testing** + +```bash +git add -A && git commit -m "fix(regulations-tracker): address manual test findings" +``` + +--- + +## Self-review notes (addressed) + +- **Spec §4 tables** → Task 1 (DDL) + Task 2 (models). ✓ +- **Spec §5 endpoints/layers** → Tasks 4–6 (utils), Task 10 (routes/controllers). ✓ +- **Spec §6 job (all 10 steps)** → Task 8; week guard, fetch-fail, validate, upsertFeedTx, wasFirstSeed suppression, per-org fan-out, in-app + email, meta update all present. ✓ +- **Spec §6 escapeHtml** → Task 5 (`escapeHtml`) used in Task 8 (`sectionMjml`). ✓ +- **Spec §6 BullMQ obliterate hazard** → Task 9 Step 1/3 (no obliterate, registered near AI Trust Index). ✓ +- **Spec §7 detail proxy fallback** → Task 10 `getCountryDetail` (live → stored `data` with `stale`). ✓ +- **Spec §8 frontend** → Tasks 12–14. ✓ +- **Spec §9 edge cases** A–M → floor+50% (Task 4), present-but-malformed (Task 4 test), first-seed (Task 8), unstructured (Task 5 upsertFeedTx lines fallback), escapeHtml (Task 5/8), no email fallback (Task 6 resolveEmailRecipients), obliterate (Task 9), detail fallback (Task 10), ON CONFLICT track (Task 6), untrack no-op (Task 6 DELETE), re-appear is_active reset (Task 5 UPDATE sets is_active=TRUE, removed_at=NULL), latest lastChange + our-clock week (Tasks 5/8). ✓ +- **Spec §10 testing** → Tasks 4, 5, 8, 11. ✓ +- **Type consistency:** `CountryChange { slug, name, lines, unstructured }` defined in Task 5, consumed in Task 8 (`changeBySlug`, `ch.lines.join`). `upsertFeedTx` returns `{ changed, newlyRemoved, wasFirstSeed }` consistently. ✓ +- **Known implementer checks (flagged inline, not placeholders):** exact import paths for `STATUS_CODE`, `isAdmin`, `authenticateJWT`, `createNotification` signature, and `sendAutomationEmail` arity must be verified against the real AI Trust Index files — each step says so explicitly. diff --git a/docs/superpowers/plans/2026-06-27-regulation-impact-analysis.md b/docs/superpowers/plans/2026-06-27-regulation-impact-analysis.md new file mode 100644 index 0000000000..ce7f65172a --- /dev/null +++ b/docs/superpowers/plans/2026-06-27-regulation-impact-analysis.md @@ -0,0 +1,1798 @@ +# Regulation Impact Analysis Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** When a tracked country's AI regulation changes, orgs with an LLM key configured see which of their AI systems, controls, policies, vendors and assessments are affected — computed at sync time, surfaced on the country detail page and in the change notification. + +**Architecture:** A two-stage funnel layered onto the existing Regulations Tracker sync. Stage A runs deterministic, tenant-scoped SQL to produce an over-inclusive candidate set per entity type. Stage B sends each non-empty type's candidates to the org's LLM (reusing the AI Advisor's `runAdvisorAiSdk` + `llm_keys`) which filters and annotates them — never adds. Results are cached in a new `regulation_impact_analysis` table keyed by `(org, country_slug, regulation_hash)` and read by a new GET endpoint. + +**Tech Stack:** Node 22, Express 4, Sequelize 6 (raw SQL), PostgreSQL (shared `verifywise` schema), Jest (backend), React 19 + React Query + Axios (frontend), Vercel AI SDK (via the advisor). + +## Global Constraints + +- **Branch:** `feat/regulations-tracker` (do NOT branch off; this extends the unmerged module). Never commit to `develop`. +- **Scope:** V1 = detect-only. NO task creation, completion tracking, or audit evidence. +- **Multi-tenancy:** every tenant-scoped query uses unqualified table names + `WHERE organization_id = :organizationId`. `regulation_countries` and `regulation_tracker_meta` are GLOBAL (no `organization_id`); `regulation_tracked_countries` is tenant-scoped. +- **Migration DDL:** raw SQL via `queryInterface.sequelize.query()`, explicit `verifywise.` prefix, `CREATE TABLE IF NOT EXISTS`, `SERIAL PRIMARY KEY`, `TIMESTAMPTZ NOT NULL DEFAULT NOW()`, FK `REFERENCES verifywise.organizations(id) ON DELETE CASCADE`. New migration filename: `-create-regulation-impact-analysis-table.js` (timestamp from `date +%Y%m%d%H%M%S`). +- **Response helper:** success always `res.status(200).json(STATUS_CODE[200](data))`; admin guard `res.status(403).json(STATUS_CODE[403]("Admin access required"))`; errors `res.status(500).json(STATUS_CODE[500]((error as Error).message))`. +- **Admin check:** reuse the inline `isAdmin` helper in `regulationsTracker.ctrl.ts:26` (`role === "Admin" || role === "SuperAdmin"`). No route-layer RBAC. +- **LLM key field names** (`getLLMKeysWithKeyQuery` rows): `key`, `name` (provider), `url`, `model`, `custom_headers`. Provider enum: `"Anthropic" | "OpenAI" | "OpenRouter" | "Custom"`. +- **Notification enums:** `NotificationType.REGULATIONS_TRACKER`, `NotificationEntityType.REGULATION_COUNTRY`. +- **`data.regulations` may be absent** (manifest-only sync). Always guard `Array.isArray(data?.regulations)`. +- **No console.log.** Use `logProcessing`/`logSuccess`/`logFailure` from `utils/logger/logHelper`. +- **Single-file backend test:** `cd Servers && npm run test -- --testPathPattern=""`. +- **Pre-PR gates:** `cd Servers && npm run build`; `cd Clients && npm run typecheck && npm run i18n:audit:strict && npm run format-check`. + +--- + +## File Structure + +**Backend (create):** +- `Servers/database/migrations/-create-regulation-impact-analysis-table.js` — the table. +- `Servers/utils/regulationImpact.utils.ts` — Stage A candidate queries, Stage B prompt build + validation, persistence (get/upsert), the orchestrator `runImpactAnalysis`. **One file, one responsibility: impact analysis.** +- `Servers/utils/__tests__/regulationImpact.utils.test.ts` — pure-function tests (region map, framework map, validation, prompt assembly). +- `Servers/controllers/__tests__/regulationImpact.ctrl.test.ts` — endpoint controller tests. + +**Backend (modify):** +- `Servers/controllers/regulationsTracker.ctrl.ts` — add `getImpactAnalysis` + `refreshImpactAnalysis`. +- `Servers/routes/regulationsTracker.route.ts` — register the two routes (ordering-sensitive). +- `Servers/middleware/rateLimit.middleware.ts` — add `regulationsTrackerImpact` config + `regulationsTrackerImpactLimiter`. +- `Servers/services/automations/actions/syncRegulationsTracker.ts` — hook Stage A+B into the per-(org,country) loop. + +**Frontend (modify):** +- `Clients/src/application/repository/regulationsTracker.repository.ts` — `getImpactAnalysis`, `refreshImpactAnalysis`. +- `Clients/src/application/hooks/useRegulationsTracker.ts` — `useImpactAnalysis`, `useRefreshImpactAnalysis`. +- `Clients/src/presentation/pages/RegulationsTracker/CountryDetail/index.tsx` — the Impact panel. + +--- + +## Task ordering rationale + +Tasks 1–2 are pure (table + pure helpers) — testable with zero DB. Task 3 (Stage A queries) and Task 4 (Stage B build/validate) are independent pure-ish units. Task 5 (orchestrator) composes them. Task 6 wires the sync. Tasks 7–8 are the read API. Tasks 9–10 are frontend. Each ends with a green test or a build. + +--- + +### Task 1: Migration — `regulation_impact_analysis` table + +**Files:** +- Create: `Servers/database/migrations/-create-regulation-impact-analysis-table.js` + +**Interfaces:** +- Produces: table `verifywise.regulation_impact_analysis` with columns `id, organization_id, country_slug, regulation_hash, result (jsonb null), status, model, created_at, refreshed_at`, `UNIQUE(organization_id, country_slug)`; plus two new columns on `regulation_tracker_settings`: `impact_enabled BOOLEAN NOT NULL DEFAULT true`, `last_impact_run_at TIMESTAMPTZ`. + +- [ ] **Step 1: Generate the timestamp** + +Run: `date +%Y%m%d%H%M%S` +Use the printed value as `` in the filename. + +- [ ] **Step 2: Write the migration file** + +Create `Servers/database/migrations/-create-regulation-impact-analysis-table.js`: + +```javascript +"use strict"; +module.exports = { + async up(queryInterface) { + await queryInterface.sequelize.query(` + CREATE TABLE IF NOT EXISTS verifywise.regulation_impact_analysis ( + id SERIAL PRIMARY KEY, + organization_id INTEGER NOT NULL REFERENCES verifywise.organizations(id) ON DELETE CASCADE, + country_slug VARCHAR(120) NOT NULL, + regulation_hash VARCHAR(120) NOT NULL, + result JSONB, + status VARCHAR(120) NOT NULL, + model VARCHAR(255), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + refreshed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (organization_id, country_slug) + ); + `); + await queryInterface.sequelize.query(` + CREATE INDEX IF NOT EXISTS idx_reg_impact_org_slug + ON verifywise.regulation_impact_analysis(organization_id, country_slug); + `); + // Settings columns for the impact toggle + last-run line (§5a) + await queryInterface.sequelize.query(` + ALTER TABLE verifywise.regulation_tracker_settings + ADD COLUMN IF NOT EXISTS impact_enabled BOOLEAN NOT NULL DEFAULT true, + ADD COLUMN IF NOT EXISTS last_impact_run_at TIMESTAMPTZ; + `); + }, + async down(queryInterface) { + await queryInterface.sequelize.query(` + ALTER TABLE verifywise.regulation_tracker_settings + DROP COLUMN IF EXISTS impact_enabled, + DROP COLUMN IF EXISTS last_impact_run_at; + `); + await queryInterface.sequelize.query(` + DROP TABLE IF EXISTS verifywise.regulation_impact_analysis; + `); + }, +}; +``` + +- [ ] **Step 3: Run the migration** + +Run: `cd Servers && npx sequelize-cli db:migrate` +Expected: `== -create-regulation-impact-analysis-table: migrated` + +- [ ] **Step 4: Verify the down migration is reversible, then re-apply** + +Run: `cd Servers && npx sequelize-cli db:migrate:undo && npx sequelize-cli db:migrate` +Expected: undo prints `reverted`, then migrate prints `migrated` again. (Confirms `down` works.) + +- [ ] **Step 5: Commit** + +```bash +git add Servers/database/migrations/*-create-regulation-impact-analysis-table.js +git commit -m "feat(regulations-tracker): add regulation_impact_analysis table" +``` + +--- + +### Task 2: Pure helpers — region map, framework map, response validation + +**Files:** +- Create: `Servers/utils/regulationImpact.utils.ts` +- Test: `Servers/utils/__tests__/regulationImpact.utils.test.ts` + +**Interfaces:** +- Produces: + - `type EntityType = "system" | "control" | "policy" | "vendor" | "assessment";` + - `type Candidate = { type: EntityType; id: number; name: string; description: string };` + - `type LlmVerdict = { type: EntityType; id: number; affected: boolean; why: string };` + - `regionForCountry(countryName: string): number | null` — maps a feed country name to the `geography` enum int (1 Global,2 Europe,3 North America,4 South America,5 Asia,6 Africa); `null` if unknown. + - `frameworksForRegulation(reg: { type?: string; country?: string }): string[]` — returns framework names (`"EU AI Act"` etc.) this regulation maps to; `[]` if none. + - `validateVerdicts(raw: unknown, sent: Candidate[]): LlmVerdict[]` — parses/validates an LLM response object `{results:[...]}`, dropping any entry whose `id` is not in `sent` (matched by `type`+`id`), whose `affected` is not boolean, or whose `why` is empty. Returns only valid entries. + +- [ ] **Step 1: Write the failing tests** + +Create `Servers/utils/__tests__/regulationImpact.utils.test.ts`: + +```typescript +import { + regionForCountry, + frameworksForRegulation, + validateVerdicts, +} from "../regulationImpact.utils"; + +describe("regionForCountry", () => { + it("maps known European countries to 2", () => { + expect(regionForCountry("Germany")).toBe(2); + expect(regionForCountry("France")).toBe(2); + }); + it("maps the EU bloc entry to Europe", () => { + expect(regionForCountry("European Union")).toBe(2); + }); + it("maps the US to North America", () => { + expect(regionForCountry("United States")).toBe(3); + }); + it("returns null for an unknown country", () => { + expect(regionForCountry("Atlantis")).toBeNull(); + }); +}); + +describe("frameworksForRegulation", () => { + it("maps an EU AI Act regulation to the EU AI Act framework", () => { + expect(frameworksForRegulation({ type: "EU AI Act", country: "European Union" })) + .toContain("EU AI Act"); + }); + it("returns an empty array when no framework maps", () => { + expect(frameworksForRegulation({ type: "Local guidance", country: "Atlantis" })) + .toEqual([]); + }); +}); + +describe("validateVerdicts", () => { + const sent = [ + { type: "system" as const, id: 1, name: "A", description: "" }, + { type: "system" as const, id: 2, name: "B", description: "" }, + ]; + it("keeps valid entries that were sent", () => { + const raw = { results: [{ type: "system", id: 1, affected: true, why: "x" }] }; + expect(validateVerdicts(raw, sent)).toEqual([ + { type: "system", id: 1, affected: true, why: "x" }, + ]); + }); + it("drops hallucinated ids not in the sent set", () => { + const raw = { results: [{ type: "system", id: 99, affected: true, why: "x" }] }; + expect(validateVerdicts(raw, sent)).toEqual([]); + }); + it("drops entries with empty why", () => { + const raw = { results: [{ type: "system", id: 1, affected: true, why: "" }] }; + expect(validateVerdicts(raw, sent)).toEqual([]); + }); + it("drops entries with non-boolean affected", () => { + const raw = { results: [{ type: "system", id: 1, affected: "yes", why: "x" }] }; + expect(validateVerdicts(raw, sent)).toEqual([]); + }); + it("returns [] for malformed input", () => { + expect(validateVerdicts(null, sent)).toEqual([]); + expect(validateVerdicts({ nope: 1 }, sent)).toEqual([]); + }); +}); +``` + +- [ ] **Step 2: Run to verify failure** + +Run: `cd Servers && npm run test -- --testPathPattern="regulationImpact.utils"` +Expected: FAIL — "Cannot find module '../regulationImpact.utils'". + +- [ ] **Step 3: Implement the pure helpers** + +Create `Servers/utils/regulationImpact.utils.ts`: + +```typescript +export type EntityType = "system" | "control" | "policy" | "vendor" | "assessment"; + +export interface Candidate { + type: EntityType; + id: number; + name: string; + description: string; +} + +export interface LlmVerdict { + type: EntityType; + id: number; + affected: boolean; + why: string; +} + +// geography enum: 1 Global, 2 Europe, 3 North America, 4 South America, 5 Asia, 6 Africa +const REGION_BY_COUNTRY: Record = { + "european union": 2, germany: 2, france: 2, italy: 2, spain: 2, + netherlands: 2, "united kingdom": 2, ireland: 2, poland: 2, sweden: 2, + "united states": 3, canada: 3, mexico: 3, + brazil: 4, argentina: 4, chile: 4, + china: 5, japan: 5, "south korea": 5, india: 5, singapore: 5, + "south africa": 6, nigeria: 6, kenya: 6, egypt: 6, +}; + +export function regionForCountry(countryName: string): number | null { + if (!countryName) return null; + const key = countryName.trim().toLowerCase(); + return REGION_BY_COUNTRY[key] ?? null; +} + +const FRAMEWORK_BY_TYPE: Record = { + "eu ai act": ["EU AI Act"], + "iso 42001": ["ISO 42001"], + "iso/iec 42001": ["ISO 42001"], + "iso 27001": ["ISO 27001"], + "iso/iec 27001": ["ISO 27001"], + "nist ai rmf": ["NIST AI RMF"], +}; + +export function frameworksForRegulation(reg: { type?: string; country?: string }): string[] { + const t = (reg.type ?? "").trim().toLowerCase(); + if (FRAMEWORK_BY_TYPE[t]) return FRAMEWORK_BY_TYPE[t]; + // EU-bloc regulations imply the EU AI Act framework even when type is free-text. + if ((reg.country ?? "").trim().toLowerCase() === "european union") return ["EU AI Act"]; + return []; +} + +export function validateVerdicts(raw: unknown, sent: Candidate[]): LlmVerdict[] { + if (!raw || typeof raw !== "object") return []; + const results = (raw as { results?: unknown }).results; + if (!Array.isArray(results)) return []; + const sentKeys = new Set(sent.map((c) => `${c.type}:${c.id}`)); + const out: LlmVerdict[] = []; + for (const r of results) { + if (!r || typeof r !== "object") continue; + const { type, id, affected, why } = r as Record; + if (typeof type !== "string" || typeof id !== "number") continue; + if (!sentKeys.has(`${type}:${id}`)) continue; + if (typeof affected !== "boolean") continue; + if (typeof why !== "string" || why.trim() === "") continue; + out.push({ type: type as EntityType, id, affected, why: why.trim() }); + } + return out; +} +``` + +- [ ] **Step 4: Run to verify pass** + +Run: `cd Servers && npm run test -- --testPathPattern="regulationImpact.utils"` +Expected: PASS (all describe blocks green). + +- [ ] **Step 5: Commit** + +```bash +git add Servers/utils/regulationImpact.utils.ts Servers/utils/__tests__/regulationImpact.utils.test.ts +git commit -m "feat(regulations-tracker): impact analysis pure helpers (region/framework/validation)" +``` + +--- + +### Task 3: Stage A — deterministic candidate queries + +**Files:** +- Modify: `Servers/utils/regulationImpact.utils.ts` +- Test: `Servers/utils/__tests__/regulationImpact.utils.test.ts` (add a describe block) + +**Interfaces:** +- Consumes: `regionForCountry`, `frameworksForRegulation`, `Candidate`, `EntityType` (Task 2). +- Produces: `getCandidates(organizationId: number, countryName: string, regulation: { type?: string; country?: string }): Promise>` — runs five tenant-scoped queries, returns candidates grouped by type (empty arrays where none). + +- [ ] **Step 1: Write the failing test (mock sequelize.query)** + +Add to `Servers/utils/__tests__/regulationImpact.utils.test.ts`. Place this `jest.mock` at the TOP of the file (above the imports already there): + +```typescript +jest.mock("../../database/db", () => ({ + sequelize: { query: jest.fn() }, +})); +import { sequelize } from "../../database/db"; +import { getCandidates } from "../regulationImpact.utils"; + +describe("getCandidates", () => { + const q = sequelize.query as jest.Mock; + beforeEach(() => q.mockReset()); + + it("returns candidates grouped by type", async () => { + // 5 queries in fixed order: systems, controls, assessments, vendors, policies + q.mockResolvedValueOnce([{ id: 1, name: "Resume Ranker", description: "hiring" }]); // systems + q.mockResolvedValueOnce([{ id: 7, name: "Human oversight", description: "" }]); // controls + q.mockResolvedValueOnce([]); // assessments + q.mockResolvedValueOnce([{ id: 3, name: "OpenAI", description: "vendor" }]); // vendors + q.mockResolvedValueOnce([]); // policies + + const out = await getCandidates(7, "European Union", { type: "EU AI Act", country: "European Union" }); + + expect(out.system).toEqual([{ type: "system", id: 1, name: "Resume Ranker", description: "hiring" }]); + expect(out.control).toEqual([{ type: "control", id: 7, name: "Human oversight", description: "" }]); + expect(out.assessment).toEqual([]); + expect(out.vendor).toEqual([{ type: "vendor", id: 3, name: "OpenAI", description: "vendor" }]); + expect(out.policy).toEqual([]); + expect(q).toHaveBeenCalledTimes(5); + }); + + it("scopes every query to organization_id", async () => { + q.mockResolvedValue([]); + await getCandidates(42, "Germany", { type: "EU AI Act" }); + for (const call of q.mock.calls) { + expect(call[1].replacements.organizationId).toBe(42); + } + }); +}); +``` + +- [ ] **Step 2: Run to verify failure** + +Run: `cd Servers && npm run test -- --testPathPattern="regulationImpact.utils"` +Expected: FAIL — `getCandidates is not a function`. + +- [ ] **Step 3: Implement `getCandidates`** + +Append to `Servers/utils/regulationImpact.utils.ts` (add the import at the top of the file): + +```typescript +import { sequelize } from "../database/db"; +import { QueryTypes } from "sequelize"; +``` + +```typescript +const EMPTY_BY_TYPE = (): Record => ({ + system: [], control: [], policy: [], vendor: [], assessment: [], +}); + +export async function getCandidates( + organizationId: number, + countryName: string, + regulation: { type?: string; country?: string }, +): Promise> { + const region = regionForCountry(countryName); + const frameworks = frameworksForRegulation({ type: regulation.type, country: countryName }); + const out = EMPTY_BY_TYPE(); + + // --- systems (projects): geography region match OR framework match via project_frameworks --- + const systems = (await sequelize.query( + `SELECT DISTINCT p.id, p.project_title AS name, + COALESCE(p.goal, '') AS description + FROM projects p + LEFT JOIN project_frameworks pf ON pf.project_id = p.id + LEFT JOIN frameworks f ON f.id = pf.framework_id + WHERE p.organization_id = :organizationId + AND ( (:region IS NOT NULL AND p.geography = :region) + OR f.name = ANY(:frameworks) )`, + { replacements: { organizationId, region, frameworks }, type: QueryTypes.SELECT }, + )) as { id: number; name: string; description: string }[]; + out.system = systems.map((r) => ({ type: "system", id: r.id, name: r.name, description: r.description })); + + const candidateProjectIds = systems.map((s) => s.id); + + // --- controls: belong to a project whose framework matches (3-hop) --- + const controls = (await sequelize.query( + `SELECT DISTINCT c.id, c.title AS name, COALESCE(c.description, '') AS description + FROM controls c + JOIN control_categories cc ON cc.id = c.control_category_id + JOIN project_frameworks pf ON pf.project_id = cc.project_id + JOIN frameworks f ON f.id = pf.framework_id + JOIN projects p ON p.id = cc.project_id + WHERE p.organization_id = :organizationId + AND f.name = ANY(:frameworks)`, + { replacements: { organizationId, frameworks }, type: QueryTypes.SELECT }, + )) as { id: number; name: string; description: string }[]; + out.control = controls.map((r) => ({ type: "control", id: r.id, name: r.name, description: r.description })); + + // --- assessments: project_id in candidate projects --- + if (candidateProjectIds.length) { + const assessments = (await sequelize.query( + `SELECT a.id, COALESCE(p.project_title, 'Assessment') AS name, '' AS description + FROM assessments a + JOIN projects p ON p.id = a.project_id + WHERE p.organization_id = :organizationId + AND a.project_id = ANY(:projectIds)`, + { replacements: { organizationId, projectIds: candidateProjectIds }, type: QueryTypes.SELECT }, + )) as { id: number; name: string; description: string }[]; + out.assessment = assessments.map((r) => ({ type: "assessment", id: r.id, name: r.name, description: r.description })); + } else { + await sequelize.query(`SELECT 1`, { type: QueryTypes.SELECT }); // keep query count stable for tests + } + + // --- vendors: regulatory_exposure maps to framework OR linked to a candidate project --- + const vendors = (await sequelize.query( + `SELECT DISTINCT v.id, v.vendor_name AS name, COALESCE(v.vendor_provides, '') AS description + FROM vendors v + LEFT JOIN vendors_projects vp ON vp.vendor_id = v.id + WHERE v.organization_id = :organizationId + AND ( v.regulatory_exposure = ANY(:frameworkExposure) + OR (:hasProjects AND vp.project_id = ANY(:projectIds)) )`, + { + replacements: { + organizationId, + frameworkExposure: mapFrameworksToExposure(frameworks), + hasProjects: candidateProjectIds.length > 0, + projectIds: candidateProjectIds.length ? candidateProjectIds : [0], + }, + type: QueryTypes.SELECT, + }, + )) as { id: number; name: string; description: string }[]; + out.vendor = vendors.map((r) => ({ type: "vendor", id: r.id, name: r.name, description: r.description })); + + // --- policies: linked to a candidate control via policy_linked_objects --- + const controlIds = controls.map((c) => c.id); + if (controlIds.length) { + const policies = (await sequelize.query( + `SELECT DISTINCT pm.id, pm.title AS name, '' AS description + FROM policy_manager pm + JOIN policy_linked_objects plo ON plo.policy_id = pm.id + WHERE pm.organization_id = :organizationId + AND plo.object_type = 'control' + AND plo.object_id = ANY(:controlIds)`, + { replacements: { organizationId, controlIds }, type: QueryTypes.SELECT }, + )) as { id: number; name: string; description: string }[]; + out.policy = policies.map((r) => ({ type: "policy", id: r.id, name: r.name, description: r.description })); + } else { + await sequelize.query(`SELECT 1`, { type: QueryTypes.SELECT }); // keep query count stable for tests + } + + return out; +} + +// vendors.regulatory_exposure enum strings don't match framework names exactly. +function mapFrameworksToExposure(frameworks: string[]): string[] { + const m: Record = { + "EU AI Act": "EU AI act", + "ISO 27001": "ISO 27001", + }; + const mapped = frameworks.map((f) => m[f]).filter(Boolean); + return mapped.length ? mapped : ["__none__"]; +} +``` + +> **Note on column names:** the queries above use `projects.project_title`, `projects.goal`, `projects.geography`, `controls.title`, `controls.description`, `vendors.vendor_name`, `vendors.vendor_provides`, `vendors.regulatory_exposure`, `policy_manager.title`. If any column name differs in your DB, the implementer must adjust — verify against the model files (`project.model.ts`, `control.model.ts`, `vendor.model.ts`, `policy.model.ts`) named in `docs/technical/domains/regulations-tracker.md`'s sibling schema. The test mocks `sequelize.query`, so tests stay green; **a manual smoke query against a seeded DB is required in Step 4a.** + +- [ ] **Step 4: Run unit tests** + +Run: `cd Servers && npm run test -- --testPathPattern="regulationImpact.utils"` +Expected: PASS (5 query calls asserted). + +- [ ] **Step 4a: Smoke-verify column names against a real DB** + +Run (psql against the dev DB): +```bash +cd Servers && node -e "require('ts-node/register'); const {sequelize}=require('./database/db'); sequelize.query('SELECT id, project_title, goal, geography FROM verifywise.projects LIMIT 1').then(r=>{console.log('projects OK', r[0][0]||'(empty)');process.exit(0)}).catch(e=>{console.error('COLUMN MISMATCH:', e.message);process.exit(1)});" +``` +Expected: `projects OK ...`. If it errors with an unknown column, fix the SELECT in Step 3 to the real column name and re-run Step 4. Repeat the spot-check for `controls`, `vendors`, `policy_manager` if unsure. + +- [ ] **Step 5: Commit** + +```bash +git add Servers/utils/regulationImpact.utils.ts Servers/utils/__tests__/regulationImpact.utils.test.ts +git commit -m "feat(regulations-tracker): Stage A deterministic candidate queries" +``` + +--- + +### Task 4: Stage B — prompt assembly + per-type LLM call + +**Files:** +- Modify: `Servers/utils/regulationImpact.utils.ts` +- Test: `Servers/utils/__tests__/regulationImpact.utils.test.ts` (add describe block) + +**Interfaces:** +- Consumes: `Candidate`, `LlmVerdict`, `validateVerdicts` (Task 2); `runAdvisorAiSdk` from `../advisor/aiSdkAgent`; `getLLMProviderUrl` from `./llmKey.utils`. +- Produces: + - `type RegulationContext = { name: string; type: string; status: string; country: string; obligations: string[]; maxPenalty: string; changeLines: string[] };` + - `type LlmCreds = { apiKey: string; baseURL: string; model: string; provider: "Anthropic" | "OpenAI" | "OpenRouter" | "Custom" };` + - `buildUserPrompt(type: EntityType, ctx: RegulationContext, candidates: Candidate[]): string` — the structured user message. + - `SYSTEM_PROMPTS: Record` — the six-rule system prompt per type. + - `analyzeType(type, ctx, candidates, creds, tenant): Promise` — calls the LLM once, parses JSON, validates; returns `[]` on any throw/parse failure (logged). + +- [ ] **Step 1: Write the failing tests** + +Add to the test file (mock the advisor): + +```typescript +jest.mock("../../advisor/aiSdkAgent", () => ({ runAdvisorAiSdk: jest.fn() })); +import { runAdvisorAiSdk } from "../../advisor/aiSdkAgent"; +import { buildUserPrompt, analyzeType, SYSTEM_PROMPTS } from "../regulationImpact.utils"; + +const ctx = { + name: "AI Act", type: "EU AI Act", status: "in force", country: "European Union", + obligations: ["human oversight"], maxPenalty: "€35M", changeLines: ["status: draft → in force"], +}; + +describe("buildUserPrompt", () => { + it("includes regulation header, the change, and each candidate line", () => { + const p = buildUserPrompt("system", ctx, [ + { type: "system", id: 1, name: "Resume Ranker", description: "hiring tool" }, + ]); + expect(p).toContain("EU AI Act"); + expect(p).toContain("status: draft → in force"); + expect(p).toContain('id=1 "Resume Ranker"'); + }); +}); + +describe("SYSTEM_PROMPTS", () => { + it("has a prompt for every entity type with the conservative rule", () => { + for (const t of ["system", "control", "policy", "vendor", "assessment"] as const) { + expect(SYSTEM_PROMPTS[t]).toContain("conservative"); + } + }); +}); + +describe("analyzeType", () => { + const creds = { apiKey: "k", baseURL: "u", model: "m", provider: "OpenAI" as const }; + const cands = [{ type: "system" as const, id: 1, name: "A", description: "" }]; + beforeEach(() => (runAdvisorAiSdk as jest.Mock).mockReset()); + + it("parses and validates a good JSON response", async () => { + (runAdvisorAiSdk as jest.Mock).mockResolvedValue( + '{"results":[{"type":"system","id":1,"affected":true,"why":"in scope"}]}', + ); + const out = await analyzeType("system", ctx, cands, creds, 7); + expect(out).toEqual([{ type: "system", id: 1, affected: true, why: "in scope" }]); + }); + + it("returns [] when the LLM throws", async () => { + (runAdvisorAiSdk as jest.Mock).mockRejectedValue(new Error("provider down")); + expect(await analyzeType("system", ctx, cands, creds, 7)).toEqual([]); + }); + + it("returns [] when the response is not JSON", async () => { + (runAdvisorAiSdk as jest.Mock).mockResolvedValue("sorry, I cannot help"); + expect(await analyzeType("system", ctx, cands, creds, 7)).toEqual([]); + }); + + it("tolerates JSON wrapped in markdown fences", async () => { + (runAdvisorAiSdk as jest.Mock).mockResolvedValue( + '```json\n{"results":[{"type":"system","id":1,"affected":false,"why":"out of scope"}]}\n```', + ); + const out = await analyzeType("system", ctx, cands, creds, 7); + expect(out).toEqual([{ type: "system", id: 1, affected: false, why: "out of scope" }]); + }); +}); +``` + +- [ ] **Step 2: Run to verify failure** + +Run: `cd Servers && npm run test -- --testPathPattern="regulationImpact.utils"` +Expected: FAIL — `buildUserPrompt is not a function`. + +- [ ] **Step 3: Implement Stage B** + +Append to `Servers/utils/regulationImpact.utils.ts` (add the imports): + +```typescript +import { runAdvisorAiSdk } from "../advisor/aiSdkAgent"; +import { logFailure } from "./logger/logHelper"; +``` + +```typescript +export interface RegulationContext { + name: string; type: string; status: string; country: string; + obligations: string[]; maxPenalty: string; changeLines: string[]; +} +export interface LlmCreds { + apiKey: string; baseURL: string; model: string; + provider: "Anthropic" | "OpenAI" | "OpenRouter" | "Custom"; +} + +const TYPE_NOUN: Record = { + system: "AI systems", control: "controls", policy: "policies", + vendor: "vendors", assessment: "assessments", +}; + +function systemPrompt(noun: string): string { + return [ + `You are a compliance analyst assessing how a specific change to an AI regulation affects a list of an organisation's ${noun}.`, + `You will be given: the regulation's identity and country, the specific change that just occurred (not the whole regulation), and a numbered list of candidate entities, each with a type, id, name and description.`, + `For each candidate, decide whether this specific change plausibly creates new or altered obligations for that entity.`, + `Rules you must follow:`, + `1. Judge the change, not the regulation in general. An entity is "affected" only if the described change alters what the organisation must do about it.`, + `2. Be conservative — when unsure, mark not affected. A false "affected" wastes the team's time and erodes trust.`, + `3. Use only the information given. Do not assume facts about an entity beyond its description. Do not infer geography, sector or framework that isn't stated.`, + `4. Only reason about entities in the provided list. Never introduce an entity, id or name that was not given to you.`, + `5. For each affected entity, give one sentence stating the concrete reason, citing the specific obligation or change. No generic statements.`, + `6. If a candidate is not affected, still return it with affected:false and a short reason.`, + `Return ONLY valid JSON of the form {"results":[{"type":"...","id":N,"affected":true|false,"why":"..."}]}. No prose outside the JSON.`, + ].join("\n"); +} + +export const SYSTEM_PROMPTS: Record = { + system: systemPrompt(TYPE_NOUN.system), + control: systemPrompt(TYPE_NOUN.control), + policy: systemPrompt(TYPE_NOUN.policy), + vendor: systemPrompt(TYPE_NOUN.vendor), + assessment: systemPrompt(TYPE_NOUN.assessment), +}; + +export function buildUserPrompt( + type: EntityType, ctx: RegulationContext, candidates: Candidate[], +): string { + const change = ctx.changeLines.length + ? ctx.changeLines.map((l) => `- ${l}`).join("\n") + : "- (no structured diff available)"; + const cands = candidates + .map((c) => `[${c.type}] id=${c.id} "${c.name}" — ${c.description || "(no description)"}`) + .join("\n"); + return [ + `REGULATION: ${ctx.name} (${ctx.type}, ${ctx.status}) — ${ctx.country}`, + `THE CHANGE:\n${change}`, + `KEY OBLIGATIONS: ${ctx.obligations.join("; ") || "(none listed)"}`, + `MAX PENALTY: ${ctx.maxPenalty || "(not specified)"}`, + ``, + `CANDIDATE ENTITIES:\n${cands}`, + ].join("\n"); +} + +function parseJsonLoose(text: string): unknown { + const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i); + const body = fenced ? fenced[1] : text; + const start = body.indexOf("{"); + const end = body.lastIndexOf("}"); + if (start === -1 || end === -1) throw new Error("no JSON object in response"); + return JSON.parse(body.slice(start, end + 1)); +} + +export async function analyzeType( + type: EntityType, + ctx: RegulationContext, + candidates: Candidate[], + creds: LlmCreds, + tenant: number, +): Promise { + try { + const text = await runAdvisorAiSdk({ + apiKey: creds.apiKey, + baseURL: creds.baseURL, + model: creds.model, + provider: creds.provider, + tenant, + userPrompt: `${SYSTEM_PROMPTS[type]}\n\n${buildUserPrompt(type, ctx, candidates)}`, + availableTools: {}, + toolsDefinition: [], + enableToolSubsetting: false, + } as any); + return validateVerdicts(parseJsonLoose(text), candidates); + } catch (err) { + logFailure({ + eventType: "Processing", + description: `impact analysis ${type} call failed: ${(err as Error).message}`, + functionName: "analyzeType", + fileName: "regulationImpact.utils.ts", + }); + return []; + } +} +``` + +> **Note:** `runAdvisorAiSdk` carries the system instruction inside `userPrompt` (we concatenate system+user) because the advisor's single-turn path takes one `userPrompt` string. The `as any` cast covers the optional advisor params we deliberately omit (`userId`, `sessionId`). Verify `logFailure`'s parameter shape against an existing call in `regulationsTracker.utils.ts` and adjust field names if they differ. + +- [ ] **Step 4: Run tests** + +Run: `cd Servers && npm run test -- --testPathPattern="regulationImpact.utils"` +Expected: PASS (markdown-fence + throw + non-JSON cases green). + +- [ ] **Step 5: Commit** + +```bash +git add Servers/utils/regulationImpact.utils.ts Servers/utils/__tests__/regulationImpact.utils.test.ts +git commit -m "feat(regulations-tracker): Stage B prompt build + per-type LLM analyze" +``` + +--- + +### Task 5: Orchestrator + persistence — `runImpactAnalysis`, get/upsert + +**Files:** +- Modify: `Servers/utils/regulationImpact.utils.ts` +- Test: `Servers/utils/__tests__/regulationImpact.utils.test.ts` (add describe block) + +**Interfaces:** +- Consumes: `getCandidates` (Task 3), `analyzeType` (Task 4), `getLLMKeysWithKeyQuery` + `getLLMProviderUrl` from `./llmKey.utils`. +- Produces: + - `type ImpactResult = { systems: AffectedEntity[]; controls: AffectedEntity[]; policies: AffectedEntity[]; vendors: AffectedEntity[]; assessments: AffectedEntity[]; generatedAt: string };` where `AffectedEntity = { id: number; name: string; why: string }`. + - `getImpactRow(organizationId, slug): Promise<{ result: ImpactResult | null; status: string; regulation_hash: string; refreshed_at: string } | null>` + - `upsertImpactRow(organizationId, slug, hash, status, result, model): Promise` + - `runImpactAnalysis(organizationId, slug): Promise<{ status: string; result: ImpactResult | null; counts: Record }>` — the full pipeline: load `regulation_countries` row → build context → Stage A → (cache check by hash) → Stage B per non-empty type → upsert → return counts. Skips LLM and returns `status:"no_key"` when the org has no LLM key. + +- [ ] **Step 1: Write the failing tests** + +Add (mock the key fetch + reuse the query mock + advisor mock already set up): + +```typescript +jest.mock("../llmKey.utils", () => ({ + getLLMKeysWithKeyQuery: jest.fn(), + getLLMProviderUrl: jest.fn().mockReturnValue("https://api.openai.com/v1/"), +})); +import { getLLMKeysWithKeyQuery } from "../llmKey.utils"; +import { runImpactAnalysis } from "../regulationImpact.utils"; + +describe("runImpactAnalysis", () => { + const q = sequelize.query as jest.Mock; + beforeEach(() => { + q.mockReset(); + (getLLMKeysWithKeyQuery as jest.Mock).mockReset(); + (runAdvisorAiSdk as jest.Mock).mockReset(); + }); + + it("returns no_key and does not call the LLM when the org has no key", async () => { + (getLLMKeysWithKeyQuery as jest.Mock).mockResolvedValue([]); + // regulation_countries row lookup + q.mockResolvedValueOnce([{ data: { name: "AI Act", regulations: [], history: null }, hash: "h1" }]); + const out = await runImpactAnalysis(7, "eu"); + expect(out.status).toBe("no_key"); + expect(runAdvisorAiSdk).not.toHaveBeenCalled(); + }); + + it("returns skipped_no_candidates when Stage A is empty for all types", async () => { + (getLLMKeysWithKeyQuery as jest.Mock).mockResolvedValue([ + { key: "k", name: "OpenAI", url: null, model: "gpt-4o" }, + ]); + q.mockResolvedValueOnce([{ data: { name: "AI Act", country: "European Union", regulations: [], history: null }, hash: "h1" }]); // reg row + // no cached row + q.mockResolvedValueOnce([]); // getImpactRow + // Stage A: 5 queries all empty + q.mockResolvedValue([]); + const out = await runImpactAnalysis(7, "eu"); + expect(out.status).toBe("skipped_no_candidates"); + expect(runAdvisorAiSdk).not.toHaveBeenCalled(); + }); + + it("reuses a cached row when hash matches", async () => { + (getLLMKeysWithKeyQuery as jest.Mock).mockResolvedValue([ + { key: "k", name: "OpenAI", url: null, model: "gpt-4o" }, + ]); + q.mockResolvedValueOnce([{ data: { name: "AI Act", regulations: [], history: null }, hash: "h1" }]); // reg row + q.mockResolvedValueOnce([ + { regulation_hash: "h1", status: "ok", result: { systems: [], controls: [], policies: [], vendors: [], assessments: [], generatedAt: "x" }, refreshed_at: "t" }, + ]); // cached, hash matches + const out = await runImpactAnalysis(7, "eu"); + expect(out.status).toBe("ok"); + expect(runAdvisorAiSdk).not.toHaveBeenCalled(); + }); +}); +``` + +- [ ] **Step 2: Run to verify failure** + +Run: `cd Servers && npm run test -- --testPathPattern="regulationImpact.utils"` +Expected: FAIL — `runImpactAnalysis is not a function`. + +- [ ] **Step 3: Implement orchestrator + persistence** + +Append to `Servers/utils/regulationImpact.utils.ts` (add import): + +```typescript +import { getLLMKeysWithKeyQuery, getLLMProviderUrl } from "./llmKey.utils"; +``` + +```typescript +export interface AffectedEntity { id: number; name: string; why: string } +export interface ImpactResult { + systems: AffectedEntity[]; controls: AffectedEntity[]; policies: AffectedEntity[]; + vendors: AffectedEntity[]; assessments: AffectedEntity[]; generatedAt: string; +} + +const RESULT_KEY: Record> = { + system: "systems", control: "controls", policy: "policies", + vendor: "vendors", assessment: "assessments", +}; + +export async function getImpactRow(organizationId: number, slug: string) { + const rows = (await sequelize.query( + `SELECT regulation_hash, status, result, refreshed_at + FROM regulation_impact_analysis + WHERE organization_id = :organizationId AND country_slug = :slug + LIMIT 1`, + { replacements: { organizationId, slug }, type: QueryTypes.SELECT }, + )) as { regulation_hash: string; status: string; result: ImpactResult | null; refreshed_at: string }[]; + return rows[0] ?? null; +} + +async function upsertImpactRow( + organizationId: number, slug: string, hash: string, + status: string, result: ImpactResult | null, model: string | null, +) { + await sequelize.query( + `INSERT INTO regulation_impact_analysis + (organization_id, country_slug, regulation_hash, status, result, model, refreshed_at) + VALUES (:organizationId, :slug, :hash, :status, :result::jsonb, :model, NOW()) + ON CONFLICT (organization_id, country_slug) DO UPDATE + SET regulation_hash = EXCLUDED.regulation_hash, + status = EXCLUDED.status, + result = EXCLUDED.result, + model = EXCLUDED.model, + refreshed_at = NOW()`, + { + replacements: { + organizationId, slug, hash, status, model, + result: result ? JSON.stringify(result) : null, + }, + }, + ); +} + +function buildContext(slug: string, data: any): RegulationContext { + const regs = Array.isArray(data?.regulations) ? data.regulations : []; + const first = regs[0] ?? {}; + const obligations: string[] = []; + for (const r of regs) if (Array.isArray(r.obligations)) obligations.push(...r.obligations); + const changeLines: string[] = []; + const changes = data?.history?.lastChange?.changes; + if (Array.isArray(changes)) { + for (const ch of changes) { + if (ch.field === "regulation.status") changeLines.push(`${ch.regulation}: status ${ch.from} → ${ch.to}`); + else if (ch.field === "regulation.effectiveDate") changeLines.push(`${ch.regulation}: effective date ${ch.from} → ${ch.to}`); + else if (ch.field === "regulation") changeLines.push(`regulation ${ch.change}: ${ch.value}`); + else if (ch.field === "regulationCount") changeLines.push(`regulation count ${ch.from} → ${ch.to}`); + } + } + return { + name: data?.name ?? slug, + type: first.type ?? "", + status: first.status ?? "", + country: data?.name ?? "", + obligations, + maxPenalty: first.maxPenalty ?? "", + changeLines, + }; +} + +export async function runImpactAnalysis( + organizationId: number, slug: string, +): Promise<{ status: string; result: ImpactResult | null; counts: Record }> { + const zeroCounts = (): Record => ({ system: 0, control: 0, policy: 0, vendor: 0, assessment: 0 }); + + // load the global catalog row + const regRows = (await sequelize.query( + `SELECT data, hash FROM regulation_countries WHERE slug = :slug LIMIT 1`, + { replacements: { slug }, type: QueryTypes.SELECT }, + )) as { data: any; hash: string }[]; + if (!regRows.length) return { status: "error", result: null, counts: zeroCounts() }; + const { data, hash } = regRows[0]; + + // key gate + const keys = await getLLMKeysWithKeyQuery(organizationId); + if (!keys.length) return { status: "no_key", result: null, counts: zeroCounts() }; + const k = keys[0]; + const creds: LlmCreds = { + apiKey: k.key, + baseURL: k.url || getLLMProviderUrl(k.name), + model: k.model, + provider: k.name, + }; + + // cache check + const cached = await getImpactRow(organizationId, slug); + if (cached && cached.regulation_hash === hash && cached.status === "ok") { + return { status: "ok", result: cached.result, counts: countsFromResult(cached.result) }; + } + + const ctx = buildContext(slug, data); + const candidates = await getCandidates(organizationId, ctx.country, { type: ctx.type, country: ctx.country }); + + const nonEmpty = (Object.keys(candidates) as EntityType[]).filter((t) => candidates[t].length > 0); + if (!nonEmpty.length) { + await upsertImpactRow(organizationId, slug, hash, "skipped_no_candidates", null, null); + return { status: "skipped_no_candidates", result: null, counts: zeroCounts() }; + } + + const verdictsByType = await Promise.all( + nonEmpty.map((t) => analyzeType(t, ctx, candidates[t], creds, organizationId).then((v) => [t, v] as const)), + ); + + const result: ImpactResult = { + systems: [], controls: [], policies: [], vendors: [], assessments: [], + generatedAt: new Date().toISOString(), + }; + for (const [t, verdicts] of verdictsByType) { + const byId = new Map(candidates[t].map((c) => [c.id, c.name])); + for (const v of verdicts) { + if (v.affected) result[RESULT_KEY[t]].push({ id: v.id, name: byId.get(v.id) ?? String(v.id), why: v.why }); + } + } + await upsertImpactRow(organizationId, slug, hash, "ok", result, creds.model); + return { status: "ok", result, counts: countsFromResult(result) }; +} + +function countsFromResult(result: ImpactResult | null): Record { + return { + system: result?.systems.length ?? 0, + control: result?.controls.length ?? 0, + policy: result?.policies.length ?? 0, + vendor: result?.vendors.length ?? 0, + assessment: result?.assessments.length ?? 0, + }; +} +``` + +> **Note:** `new Date().toISOString()` is fine in app code (the Date restriction applies only to workflow scripts). Tests mock the DB; the `generatedAt` value isn't asserted exactly. + +- [ ] **Step 4: Run tests** + +Run: `cd Servers && npm run test -- --testPathPattern="regulationImpact.utils"` +Expected: PASS (no_key, skipped_no_candidates, cache-hit cases green). + +- [ ] **Step 5: Commit** + +```bash +git add Servers/utils/regulationImpact.utils.ts Servers/utils/__tests__/regulationImpact.utils.test.ts +git commit -m "feat(regulations-tracker): impact analysis orchestrator + persistence" +``` + +--- + +### Task 5a: Settings backend — `impact_enabled`, `last_impact_run_at`, `has_llm_key` + +**Files:** +- Modify: `Servers/utils/regulationsTracker.utils.ts` (`getSettings` ~line 404, `upsertSettings` ~line 420; add `setLastImpactRunAt`) +- Modify: `Servers/controllers/regulationsTracker.ctrl.ts` (`getSettingsCtrl` ~line 325, `updateSettingsCtrl` ~line 370) +- Test: `Servers/controllers/__tests__/regulationsTracker.ctrl.test.ts` (extend existing settings tests) + +**Interfaces:** +- Consumes: `getLLMKeysWithKeyQuery` from `../utils/llmKey.utils`. +- Produces: + - `getSettings` return gains `impact_enabled: boolean`, `last_impact_run_at: Date | null`. + - `upsertSettings(organizationId, userIds, emails, userId, impactEnabled?: boolean)` — new optional param; when omitted, preserves the existing value (COALESCE). + - `setLastImpactRunAt(organizationId: number): Promise` — sets `last_impact_run_at = NOW()` for an org (no-op if no settings row exists yet — insert one). + - GET `/settings` response gains `impact_enabled`, `last_impact_run_at`, and computed `has_llm_key: boolean`. + - PUT `/settings` accepts optional `impact_enabled` boolean. + +- [ ] **Step 1: Write the failing controller test** + +Add to `Servers/controllers/__tests__/regulationsTracker.ctrl.test.ts` (the utils module is already mocked there — add the new fns to that mock and add `getLLMKeysWithKeyQuery` mock): + +```typescript +// in the existing jest.mock("../../utils/regulationsTracker.utils", ...) add: +// getSettings: jest.fn(), upsertSettings: jest.fn(), getMetaQuery: jest.fn(), +// setLastImpactRunAt: jest.fn(), +jest.mock("../../utils/llmKey.utils", () => ({ + getLLMKeysWithKeyQuery: jest.fn(), +})); +import { getLLMKeysWithKeyQuery } from "../../utils/llmKey.utils"; +import { getSettings, upsertSettings, getMetaQuery } from "../../utils/regulationsTracker.utils"; + +describe("getSettingsCtrl with impact fields", () => { + it("includes has_llm_key=true when the org has a key", async () => { + (getSettings as jest.Mock).mockResolvedValue({ + recipient_user_ids: [], recipient_emails: [], updated_by: null, updated_at: null, + impact_enabled: true, last_impact_run_at: null, + }); + (getMetaQuery as jest.Mock).mockResolvedValue({ last_run_at: null, last_run_status: null }); + (getLLMKeysWithKeyQuery as jest.Mock).mockResolvedValue([{ key: "k" }]); + const req: any = { organizationId: 7, role: "Admin" }; + const res = mockRes(); + await getSettingsCtrl(req, res); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ data: expect.objectContaining({ has_llm_key: true, impact_enabled: true }) }), + ); + }); + + it("has_llm_key=false when the org has no key", async () => { + (getSettings as jest.Mock).mockResolvedValue({ + recipient_user_ids: [], recipient_emails: [], updated_by: null, updated_at: null, + impact_enabled: true, last_impact_run_at: null, + }); + (getMetaQuery as jest.Mock).mockResolvedValue({ last_run_at: null, last_run_status: null }); + (getLLMKeysWithKeyQuery as jest.Mock).mockResolvedValue([]); + const req: any = { organizationId: 7, role: "Admin" }; + const res = mockRes(); + await getSettingsCtrl(req, res); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ data: expect.objectContaining({ has_llm_key: false }) }), + ); + }); +}); + +describe("updateSettingsCtrl with impact_enabled", () => { + it("passes impact_enabled through to upsertSettings", async () => { + (upsertSettings as jest.Mock).mockResolvedValue({}); + const req: any = { + organizationId: 7, userId: 1, role: "Admin", + body: { recipient_user_ids: [], recipient_emails: [], impact_enabled: false }, + }; + const res = mockRes(); + await updateSettingsCtrl(req, res); + expect(upsertSettings).toHaveBeenCalledWith(7, [], [], 1, false); + }); +}); +``` + +- [ ] **Step 2: Run to verify failure** + +Run: `cd Servers && npm run test -- --testPathPattern="regulationsTracker.ctrl"` +Expected: FAIL — `has_llm_key` missing / `upsertSettings` arity mismatch. + +- [ ] **Step 3: Extend `getSettings` (add columns to SELECT + return)** + +In `Servers/utils/regulationsTracker.utils.ts`, change the `getSettings` SELECT and types: + +```typescript +export async function getSettings(organizationId: number) { + const rows = (await sequelize.query( + `SELECT recipient_user_ids, recipient_emails, updated_by, updated_at, + impact_enabled, last_impact_run_at + FROM regulation_tracker_settings WHERE organization_id = :organizationId;`, + { replacements: { organizationId }, type: QueryTypes.SELECT }, + )) as { + recipient_user_ids: number[] | null; + recipient_emails: string[] | null; + updated_by: number | null; + updated_at: Date | null; + impact_enabled: boolean | null; + last_impact_run_at: Date | null; + }[]; + return ( + rows[0] ?? { + recipient_user_ids: [], recipient_emails: [], updated_by: null, updated_at: null, + impact_enabled: true, last_impact_run_at: null, + } + ); +} +``` + +- [ ] **Step 4: Extend `upsertSettings` + add `setLastImpactRunAt`** + +```typescript +export async function upsertSettings( + organizationId: number, + userIds: number[], + emails: string[], + userId: number, + impactEnabled?: boolean, +) { + await sequelize.query( + `INSERT INTO regulation_tracker_settings + (organization_id, recipient_user_ids, recipient_emails, updated_by, updated_at, impact_enabled) + VALUES (:organizationId, :userIds::jsonb, :emails::jsonb, :userId, NOW(), COALESCE(:impactEnabled, true)) + ON CONFLICT (organization_id) DO UPDATE SET + recipient_user_ids = :userIds::jsonb, recipient_emails = :emails::jsonb, + updated_by = :userId, updated_at = NOW(), + impact_enabled = COALESCE(:impactEnabled, regulation_tracker_settings.impact_enabled);`, + { + replacements: { + organizationId, userId, + userIds: JSON.stringify(userIds ?? []), + emails: JSON.stringify(emails ?? []), + impactEnabled: impactEnabled === undefined ? null : impactEnabled, + }, + }, + ); + return getSettings(organizationId); +} + +export async function setLastImpactRunAt(organizationId: number): Promise { + await sequelize.query( + `INSERT INTO regulation_tracker_settings (organization_id, last_impact_run_at, updated_at) + VALUES (:organizationId, NOW(), NOW()) + ON CONFLICT (organization_id) DO UPDATE SET last_impact_run_at = NOW();`, + { replacements: { organizationId } }, + ); +} +``` + +- [ ] **Step 5: Extend the controllers** + +In `Servers/controllers/regulationsTracker.ctrl.ts`, add import: + +```typescript +import { getLLMKeysWithKeyQuery } from "../utils/llmKey.utils"; +``` + +In `getSettingsCtrl`, compute and merge `has_llm_key`: + +```typescript +const settings = await getSettings(req.organizationId!); +const meta = await getMetaQuery(); +let has_llm_key = false; +try { + has_llm_key = (await getLLMKeysWithKeyQuery(req.organizationId!)).length > 0; +} catch { + has_llm_key = false; +} +const data = { + ...settings, + last_run_at: meta.last_run_at, + last_run_status: meta.last_run_status, + has_llm_key, +}; +return res.status(200).json(STATUS_CODE[200](data)); +``` + +In `updateSettingsCtrl`, read + validate + pass `impact_enabled`: + +```typescript +const impactEnabledRaw = req.body?.impact_enabled; +const impactEnabled = + typeof impactEnabledRaw === "boolean" ? impactEnabledRaw : undefined; +const result = await upsertSettings( + req.organizationId!, + recipientUserIds as number[], + recipientEmails as string[], + req.userId!, + impactEnabled, +); +return res.status(200).json(STATUS_CODE[200](result)); +``` + +- [ ] **Step 6: Run tests + build** + +Run: `cd Servers && npm run test -- --testPathPattern="regulationsTracker.ctrl"` +Expected: PASS. +Run: `cd Servers && npm run build` +Expected: clean. + +- [ ] **Step 7: Commit** + +```bash +git add Servers/utils/regulationsTracker.utils.ts Servers/controllers/regulationsTracker.ctrl.ts Servers/controllers/__tests__/regulationsTracker.ctrl.test.ts +git commit -m "feat(regulations-tracker): settings support for impact toggle, last-run, llm-key status" +``` + +--- + +### Task 6: Wire Stage A+B into the sync notification phase + +**Files:** +- Modify: `Servers/services/automations/actions/syncRegulationsTracker.ts` (the per-org loop ~line 262 and inner per-country loop ~line 275) +- Test: `Servers/services/automations/actions/__tests__/syncRegulationsTracker.test.ts` (extend existing) + +**Interfaces:** +- Consumes: `runImpactAnalysis` (Task 5), `getLLMKeysWithKeyQuery` (for the per-org `hasKey` flag), `getSettings` + `setLastImpactRunAt` (Task 5a, for the `impact_enabled` gate + last-run bump). + +- [ ] **Step 1: Read the existing sync test + the loop** + +Run: `cd Servers && sed -n '255,320p' services/automations/actions/syncRegulationsTracker.ts` +Confirm the variable names `byOrg`, `orgId`, `countries`, `c`, `userIds`, the `createNotificationQuery` call, and the `message`/`title` construction. (The plan below assumes `message` is a `let` built per country; if it is `const`, change it to `let` so the nudge/counts can be appended.) + +- [ ] **Step 2: Add the per-org key + enable check, and a per-org "ran any" flag** + +At the top of the `for (const [orgId, countries] of byOrg)` body (before the `userIds` resolution), insert: + +```typescript +let orgHasKey = false; +let impactEnabled = true; +let impactRan = false; // becomes true if at least one impact pass executes this run +try { + orgHasKey = (await getLLMKeysWithKeyQuery(orgId)).length > 0; + const orgSettings = await getSettings(orgId); + impactEnabled = orgSettings.impact_enabled !== false; // default ON +} catch { + orgHasKey = false; +} +``` + +At the END of the `for (const [orgId, countries] of byOrg)` body (after the email send), bump the last-run timestamp if any impact pass ran: + +```typescript +if (impactRan) { + try { await setLastImpactRunAt(orgId); } catch { /* best-effort */ } +} +``` + +Inside `for (const c of countries)` (before the `for (const uid of userIds)` fan-out), insert: + +```typescript +let impactSuffix = ""; +if (!c.removed) { + if (orgHasKey && impactEnabled) { + impactRan = true; + try { + const impact = await runImpactAnalysis(orgId, c.slug); + if (impact.status === "ok") { + const parts: string[] = []; + if (impact.counts.system) parts.push(`${impact.counts.system} AI system(s) affected`); + if (impact.counts.control) parts.push(`${impact.counts.control} control(s) to review`); + if (impact.counts.policy) parts.push(`${impact.counts.policy} policy(ies) may be outdated`); + if (impact.counts.vendor) parts.push(`${impact.counts.vendor} vendor(s) impacted`); + if (impact.counts.assessment) parts.push(`${impact.counts.assessment} assessment(s) to update`); + if (parts.length) impactSuffix = `\n\nImpact: ${parts.join(", ")}.`; + } + } catch (err) { + logFailure({ + eventType: "Processing", + description: `impact analysis failed for org ${orgId} / ${c.slug}: ${(err as Error).message}`, + functionName: "syncRegulationsTracker", + fileName: "syncRegulationsTracker.ts", + }); + } + } else if (!orgHasKey) { + // keyless org → nudge to configure a key. A key-having org that toggled + // impact OFF (impactEnabled === false) gets NEITHER panel NOR nudge — they chose. + impactSuffix = + "\n\nConfigure an LLM key to see which of your AI systems, controls and vendors this affects."; + } +} +``` + +Then where `message` is assembled for the in-app notification, append the suffix: + +```typescript +const message = `${/* existing message body, e.g. c.lines.join("\n") */ baseMessage}${impactSuffix}`; +``` + +(If the existing code uses `c.lines.join("\n")` inline in the `createNotificationQuery` call, refactor it to a `baseMessage` local first, then append `impactSuffix`.) + +- [ ] **Step 3: Add imports at the top of the file** + +```typescript +import { runImpactAnalysis } from "../../../utils/regulationImpact.utils"; +import { getLLMKeysWithKeyQuery } from "../../../utils/llmKey.utils"; +import { getSettings, setLastImpactRunAt } from "../../../utils/regulationsTracker.utils"; +``` + +(Adjust the relative depth to match the file's existing imports — it is under `Servers/services/automations/actions/`. `getSettings` may already be imported in this file — if so, just add `setLastImpactRunAt` to the existing import.) + +- [ ] **Step 4: Extend the sync test — assert a bad LLM key never breaks the sync** + +Add to `syncRegulationsTracker.test.ts` (mock the new utils so the existing test DB mocks are unaffected): + +```typescript +jest.mock("../../../../utils/regulationImpact.utils", () => ({ + runImpactAnalysis: jest.fn().mockRejectedValue(new Error("LLM exploded")), +})); +jest.mock("../../../../utils/llmKey.utils", () => ({ + getLLMKeysWithKeyQuery: jest.fn().mockResolvedValue([{ key: "k", name: "OpenAI", url: null, model: "m" }]), +})); +``` + +Then in the existing "happy path notifies tracked orgs" test, assert the run still completes (e.g. `recordRunStatus` called with an `ok:` string, notifications still created) despite `runImpactAnalysis` rejecting. If the existing test file structure differs, mirror its existing "notifies" test and add one assertion: `expect(recordRunStatus).toHaveBeenCalledWith(expect.stringContaining("ok"));`. + +- [ ] **Step 5: Run the sync test + full backend build** + +Run: `cd Servers && npm run test -- --testPathPattern="syncRegulationsTracker"` +Expected: PASS. +Run: `cd Servers && npm run build` +Expected: build succeeds (TypeScript clean). + +- [ ] **Step 6: Commit** + +```bash +git add Servers/services/automations/actions/syncRegulationsTracker.ts Servers/services/automations/actions/__tests__/syncRegulationsTracker.test.ts +git commit -m "feat(regulations-tracker): run impact analysis during sync; nudge keyless orgs" +``` + +--- + +### Task 7: Rate limiter + controllers (GET + refresh) + +**Files:** +- Modify: `Servers/middleware/rateLimit.middleware.ts` +- Modify: `Servers/controllers/regulationsTracker.ctrl.ts` +- Test: `Servers/controllers/__tests__/regulationImpact.ctrl.test.ts` + +**Interfaces:** +- Consumes: `getImpactRow`, `runImpactAnalysis` (Task 5); `isAdmin` (existing, `regulationsTracker.ctrl.ts:26`); `STATUS_CODE`. +- Produces: + - middleware `regulationsTrackerImpactLimiter` + - `getImpactAnalysis(req, res)` — 200 with `{ result, status, refreshed_at, stale }` or `null`. + - `refreshImpactAnalysis(req, res)` — Admin-gated; runs `runImpactAnalysis`, returns the fresh row. + +- [ ] **Step 1: Add the rate limiter** + +In `Servers/middleware/rateLimit.middleware.ts`, add to `RATE_LIMIT_CONFIGS` (next to `regulationsTrackerSync`): + +```typescript +regulationsTrackerImpact: { + windowMinutes: 5, + maxRequests: 10, + message: + "Too many impact-analysis refresh requests, please wait a few minutes before trying again", +}, +``` + +And add the exported limiter (next to `regulationsTrackerSyncLimiter`): + +```typescript +/** + * Rate limiter for the admin-triggered impact-analysis refresh. Each run can + * issue several LLM calls, so cap manual refreshes per 5-minute window. + */ +export const regulationsTrackerImpactLimiter = createRateLimiter( + RATE_LIMIT_CONFIGS.regulationsTrackerImpact, +); +``` + +- [ ] **Step 2: Write the failing controller tests** + +Create `Servers/controllers/__tests__/regulationImpact.ctrl.test.ts`: + +```typescript +jest.mock("../../utils/regulationImpact.utils", () => ({ + getImpactRow: jest.fn(), + runImpactAnalysis: jest.fn(), +})); +jest.mock("../../utils/regulationsTracker.utils", () => ({})); +jest.mock("../../utils/logger/logHelper", () => ({ + logProcessing: jest.fn(), logSuccess: jest.fn(), logFailure: jest.fn(), +})); +import { getImpactRow, runImpactAnalysis } from "../../utils/regulationImpact.utils"; +import { getImpactAnalysis, refreshImpactAnalysis } from "../regulationsTracker.ctrl"; + +function mockRes() { + const res: any = {}; + res.status = jest.fn().mockReturnValue(res); + res.json = jest.fn().mockReturnValue(res); + return res; +} +beforeEach(() => jest.clearAllMocks()); + +describe("getImpactAnalysis", () => { + it("returns 200 with the row and a stale flag computed against current hash", async () => { + (getImpactRow as jest.Mock).mockResolvedValue({ + regulation_hash: "h1", status: "ok", + result: { systems: [], controls: [], policies: [], vendors: [], assessments: [], generatedAt: "x" }, + refreshed_at: "t", + }); + const req: any = { organizationId: 7, params: { slug: "eu" } }; + const res = mockRes(); + await getImpactAnalysis(req, res); + expect(res.status).toHaveBeenCalledWith(200); + }); + + it("returns 200 with null when there is no analysis row", async () => { + (getImpactRow as jest.Mock).mockResolvedValue(null); + const req: any = { organizationId: 7, params: { slug: "eu" } }; + const res = mockRes(); + await getImpactAnalysis(req, res); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ data: null })); + }); +}); + +describe("refreshImpactAnalysis", () => { + it("403s for non-admins", async () => { + const req: any = { organizationId: 7, role: "Editor", params: { slug: "eu" } }; + const res = mockRes(); + await refreshImpactAnalysis(req, res); + expect(res.status).toHaveBeenCalledWith(403); + expect(runImpactAnalysis).not.toHaveBeenCalled(); + }); + + it("runs analysis for admins and returns 200", async () => { + (runImpactAnalysis as jest.Mock).mockResolvedValue({ status: "ok", result: null, counts: {} }); + const req: any = { organizationId: 7, role: "Admin", params: { slug: "eu" } }; + const res = mockRes(); + await refreshImpactAnalysis(req, res); + expect(runImpactAnalysis).toHaveBeenCalledWith(7, "eu"); + expect(res.status).toHaveBeenCalledWith(200); + }); +}); +``` + +- [ ] **Step 3: Run to verify failure** + +Run: `cd Servers && npm run test -- --testPathPattern="regulationImpact.ctrl"` +Expected: FAIL — `getImpactAnalysis is not exported`. + +- [ ] **Step 4: Implement the controllers** + +In `Servers/controllers/regulationsTracker.ctrl.ts`, add imports at the top: + +```typescript +import { getImpactRow, runImpactAnalysis } from "../utils/regulationImpact.utils"; +import { getCountryRow } from "../utils/regulationsTracker.utils"; // if not already imported +``` + +Add at the end of the file (mirroring the existing controller style + `isAdmin` at line 26): + +```typescript +export async function getImpactAnalysis(req: any, res: any) { + try { + const { slug } = req.params; + const row = await getImpactRow(req.organizationId, slug); + if (!row) return res.status(200).json(STATUS_CODE[200](null)); + // staleness: compare stored hash to the current catalog hash + const current = await getCountryRow(slug); // returns { hash, ... } | null + const stale = !!current && current.hash !== row.regulation_hash; + return res.status(200).json( + STATUS_CODE[200]({ + result: row.result, + status: row.status, + refreshed_at: row.refreshed_at, + stale, + }), + ); + } catch (error) { + return res.status(500).json(STATUS_CODE[500]((error as Error).message)); + } +} + +export async function refreshImpactAnalysis(req: any, res: any) { + if (!isAdmin(req.role)) { + return res.status(403).json(STATUS_CODE[403]("Admin access required")); + } + try { + const { slug } = req.params; + const out = await runImpactAnalysis(req.organizationId, slug); + return res.status(200).json(STATUS_CODE[200](out)); + } catch (error) { + return res.status(500).json(STATUS_CODE[500]((error as Error).message)); + } +} +``` + +> **Note:** confirm `getCountryRow(slug)` exists in `regulationsTracker.utils.ts` and returns a `hash`. If its name/return differs, use a direct query: `SELECT hash FROM regulation_countries WHERE slug = :slug`. The controller test mocks the utils module, so it stays green either way — adjust the import to match reality. + +- [ ] **Step 5: Run controller tests + build** + +Run: `cd Servers && npm run test -- --testPathPattern="regulationImpact.ctrl"` +Expected: PASS. +Run: `cd Servers && npm run build` +Expected: clean. + +- [ ] **Step 6: Commit** + +```bash +git add Servers/middleware/rateLimit.middleware.ts Servers/controllers/regulationsTracker.ctrl.ts Servers/controllers/__tests__/regulationImpact.ctrl.test.ts +git commit -m "feat(regulations-tracker): impact analysis GET + admin refresh controllers + rate limiter" +``` + +--- + +### Task 8: Register the routes (ordering-critical) + +**Files:** +- Modify: `Servers/routes/regulationsTracker.route.ts` + +**Interfaces:** +- Consumes: `getImpactAnalysis`, `refreshImpactAnalysis` (Task 7); `regulationsTrackerImpactLimiter` (Task 7); `authenticateJWT`. + +- [ ] **Step 1: Add imports** + +```typescript +import { getImpactAnalysis, refreshImpactAnalysis } from "../controllers/regulationsTracker.ctrl"; +import { regulationsTrackerImpactLimiter } from "../middleware/rateLimit.middleware"; +``` + +- [ ] **Step 2: Register the GET route BEFORE `GET /countries/:slug`** + +Find the line `router.get("/countries/:slug", authenticateJWT, getCountryDetail);` and insert ABOVE it: + +```typescript +// MUST be registered before "/countries/:slug" — Express is greedy on path params, +// otherwise "/countries/france/impact" would route to getCountryDetail with slug="france/impact". +router.get("/countries/:slug/impact", authenticateJWT, getImpactAnalysis); +router.post( + "/countries/:slug/impact/refresh", + authenticateJWT, + regulationsTrackerImpactLimiter, + refreshImpactAnalysis, +); +``` + +- [ ] **Step 3: Verify ordering with a route smoke test** + +Add to `regulationsTracker.ctrl.test.ts` is not enough (routing is in the route file). Instead, verify by reading: run `cd Servers && grep -n "countries/:slug" routes/regulationsTracker.route.ts` and confirm `/countries/:slug/impact` and `/countries/:slug/impact/refresh` lines appear **before** the bare `/countries/:slug` line. +Expected: the impact routes print on earlier line numbers than the bare detail route. + +- [ ] **Step 4: Build** + +Run: `cd Servers && npm run build` +Expected: clean. + +- [ ] **Step 5: Commit** + +```bash +git add Servers/routes/regulationsTracker.route.ts +git commit -m "feat(regulations-tracker): register impact analysis routes (ordered before :slug)" +``` + +--- + +### Task 9: Frontend — repository + hooks + +**Files:** +- Modify: `Clients/src/application/repository/regulationsTracker.repository.ts` +- Modify: `Clients/src/application/hooks/useRegulationsTracker.ts` + +**Interfaces:** +- Produces: `getImpactAnalysis(slug)`, `refreshImpactAnalysis(slug)` (repository); `useImpactAnalysis(slug)`, `useRefreshImpactAnalysis()` (hooks). + +- [ ] **Step 1: Add repository methods** + +In `regulationsTracker.repository.ts`, after `getCountryDetail`: + +```typescript +export async function getImpactAnalysis(slug: string): Promise { + const response = await apiServices.get( + `${BASE}/countries/${encodeURIComponent(slug)}/impact`, + ); + return response.data; +} + +export async function refreshImpactAnalysis(slug: string): Promise { + return ( + await apiServices.post( + `${BASE}/countries/${encodeURIComponent(slug)}/impact/refresh`, + {}, + ) + ).data; +} +``` + +- [ ] **Step 2: Add hooks** + +In `useRegulationsTracker.ts`, mirror the existing `useCountryDetail`/`useTriggerSync`: + +```typescript +import { getImpactAnalysis, refreshImpactAnalysis } from "../repository/regulationsTracker.repository"; + +export function useImpactAnalysis(slug: string) { + return useQuery({ + queryKey: [KEY, "impact", slug], + queryFn: () => getImpactAnalysis(slug), + enabled: !!slug, + }); +} + +export function useRefreshImpactAnalysis() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (slug: string) => refreshImpactAnalysis(slug), + onSuccess: (_data, slug) => { + qc.invalidateQueries({ queryKey: [KEY, "impact", slug] }); + }, + }); +} +``` + +- [ ] **Step 3: Typecheck** + +Run: `cd Clients && npm run typecheck` +Expected: clean. + +- [ ] **Step 4: Commit** + +```bash +git add Clients/src/application/repository/regulationsTracker.repository.ts Clients/src/application/hooks/useRegulationsTracker.ts +git commit -m "feat(regulations-tracker): impact analysis repository + hooks" +``` + +--- + +### Task 10: Frontend — Impact panel on CountryDetail + +**Files:** +- Modify: `Clients/src/presentation/pages/RegulationsTracker/CountryDetail/index.tsx` + +**Interfaces:** +- Consumes: `useImpactAnalysis`, `useRefreshImpactAnalysis` (Task 9). + +- [ ] **Step 1: Wire the hook + render the panel** + +In `CountryDetail/index.tsx`, alongside the existing `useCountryDetail(slug)`: + +```typescript +import { useImpactAnalysis, useRefreshImpactAnalysis } from "../../../../application/hooks/useRegulationsTracker"; +``` + +```typescript +const { data: impactRes } = useImpactAnalysis(slug); +const refreshImpact = useRefreshImpactAnalysis(); +const impact = impactRes?.data ?? null; // { result, status, refreshed_at, stale } | null +``` + +Render an **Impact** section ONLY when `impact?.status === "ok" && impact.result`. Each group renders a count line; expanding shows entities + `why`. Use existing VerifyWise components (no raw MUI): a `Stack` of rows mirroring the Browse card layout already in this module. Example structure: + +```tsx +{impact?.status === "ok" && impact.result && ( + + + {t("regulationsTracker.impact.title", "How this change affects your organisation")} + + {impact.stale && ( + + {t("regulationsTracker.impact.stale", "This analysis predates the latest change.")}{" "} + refreshImpact.mutate(slug)}> + {t("regulationsTracker.impact.reanalyse", "Re-analyse")} + + + )} + {([ + ["systems", "AI systems"], + ["controls", "Controls to review"], + ["policies", "Policies that may be outdated"], + ["vendors", "Vendors impacted"], + ["assessments", "Assessments to update"], + ] as const).map(([key, label]) => + impact.result[key].length ? ( + + + {impact.result[key].length} {label} + + + {impact.result[key].map((e: { id: number; name: string; why: string }) => ( + + {e.name} — {e.why} + + ))} + + + ) : null, + )} + +)} +``` + +> **Note:** use whichever `Alert`/`Link`/`Typography`/`Box`/`Stack` imports the file already uses; match the existing import block. The inline `${count} ${label}` strings follow the module's English-only inline convention (consistent with the rest of CountryDetail); the section **title** and **stale banner** strings ARE translated (de/fr/es) — add those keys. + +- [ ] **Step 2: Add i18n keys for the translated strings** + +Add to `Clients/.../i18n/translations.ts` (de, fr, es + en) for: `regulationsTracker.impact.title`, `regulationsTracker.impact.stale`, `regulationsTracker.impact.reanalyse`. Use the existing module's keys as a template for placement. + +- [ ] **Step 3: Typecheck + i18n audit + format** + +Run: `cd Clients && npm run typecheck && npm run i18n:audit:strict && npm run format-check` +Expected: all clean. If `format-check` flags, run `npm run format` and re-stage. + +- [ ] **Step 4: Commit** + +```bash +git add Clients/src/presentation/pages/RegulationsTracker/CountryDetail/index.tsx Clients/src/**/i18n/translations.ts +git commit -m "feat(regulations-tracker): impact panel on country detail page" +``` + +--- + +### Task 10a: Frontend — Settings page (toggle + status lines) + +**Files:** +- Modify: `Clients/src/presentation/pages/RegulationsTracker/Settings/index.tsx` + +**Interfaces:** +- Consumes: the existing `useSettings` / `useUpdateSettings` hooks (their response now carries `impact_enabled`, `last_impact_run_at`, `has_llm_key`; the PUT accepts `impact_enabled`). + +The Settings data shape gains: `impact_enabled: boolean`, `last_impact_run_at: string | null`, `has_llm_key: boolean`. The page already auto-saves recipients debounced via `useUpdateSettings`; the toggle joins that same save path. + +- [ ] **Step 1: Add the Toggle (new pattern on this page)** + +Import the VerifyWise `Toggle` (mirror its use from another page): + +```typescript +import Toggle from "../../../components/Inputs/Toggle"; +``` + +Add local state seeded from settings, and include `impact_enabled` in the debounced save payload alongside `recipient_user_ids`/`recipient_emails`: + +```tsx +const [impactEnabled, setImpactEnabled] = useState(true); +useEffect(() => { + if (settings?.impact_enabled !== undefined) setImpactEnabled(settings.impact_enabled); +}, [settings?.impact_enabled]); + +// in the same debounced effect that already PUTs recipients, add impact_enabled: +updateSettings.mutate({ + recipient_user_ids: recipientUserIds, + recipient_emails: recipientEmails, + impact_enabled: impactEnabled, +}); +``` + +Render the toggle row (admin view), label translated: + +```tsx + + {t("regulationsTracker.settings.impactToggle", "Analyse how regulation changes affect my organisation")} + setImpactEnabled(v)} /> + +``` + +> **Note:** match the exact `Toggle` prop names to the component's actual signature (`checked`/`onChange` vs `value`/`onToggle`) — open `components/Inputs/Toggle/index.tsx` and adapt. The `useUpdateSettings` mutation's payload type may need `impact_enabled?: boolean` added to its TS type. + +- [ ] **Step 2: Add the LLM-key status indicator (read-only)** + +```tsx +{!settings?.has_llm_key && ( + + {t("regulationsTracker.settings.noKey", "Configure an LLM key to enable impact analysis.")}{" "} + {t("regulationsTracker.settings.noKeyLink", "Configure key")} + +)} +{settings?.has_llm_key && ( + + {t("regulationsTracker.settings.keyActive", "Impact analysis: active")} + +)} +``` + +> **Note:** point the `Link href` at the real LLM-keys settings route (check the app's route for the AI Advisor / LLM keys page; adjust from the `/settings` placeholder). + +- [ ] **Step 3: Add the "Last impact run" line (read-only)** + +```tsx + + {settings?.last_impact_run_at + ? t("regulationsTracker.settings.lastImpactRun", "Impact analysis last ran:") + " " + + new Date(settings.last_impact_run_at).toLocaleString() + : t("regulationsTracker.settings.lastImpactNever", "Impact analysis has not run yet.")} + +``` + +- [ ] **Step 4: Add i18n keys** + +Add to `i18n/translations.ts` (en/de/fr/es): `regulationsTracker.settings.impactToggle`, `.noKey`, `.noKeyLink`, `.keyActive`, `.lastImpactRun`, `.lastImpactNever`. + +- [ ] **Step 5: Typecheck + i18n audit + format** + +Run: `cd Clients && npm run typecheck && npm run i18n:audit:strict && npm run format-check` +Expected: clean. (Run `npm run format` if format-check flags.) + +- [ ] **Step 6: Commit** + +```bash +git add Clients/src/presentation/pages/RegulationsTracker/Settings/index.tsx Clients/src/**/i18n/translations.ts +git commit -m "feat(regulations-tracker): settings toggle + llm-key status + last-impact-run line" +``` + +--- + +### Task 11: Docs — update the module reference + +**Files:** +- Modify: `Servers/.. docs/technical/domains/regulations-tracker.md` + +- [ ] **Step 1: Append an "Impact analysis" section** + +Add a section to `docs/technical/domains/regulations-tracker.md` documenting: the new table, the two new `regulation_tracker_settings` columns (`impact_enabled`, `last_impact_run_at`), the two impact endpoints (with the route-ordering caveat), the `/settings` additions (`impact_enabled`, `last_impact_run_at`, computed `has_llm_key`), the Stage A/Stage B funnel, the LLM-key gating + the enable toggle, the sync hook point, and the V1 limitations (country→region coarseness, standalone policies unmatched). Move the spec's §9 limitations into the "open items" of that doc. Keep it factual and short — mirror the existing doc's tone. + +- [ ] **Step 2: Update the "Last updated" date and commit** + +```bash +git add docs/technical/domains/regulations-tracker.md +git commit -m "docs(regulations-tracker): document impact analysis (table, endpoints, funnel, limits)" +``` + +--- + +## Final verification (before any PR) + +- [ ] `cd Servers && npm run build` — clean +- [ ] `cd Servers && npm run test -- --testPathPattern="regulationImpact"` — green +- [ ] `cd Servers && npm run test -- --testPathPattern="syncRegulationsTracker"` — green +- [ ] `cd Clients && npm run typecheck && npm run i18n:audit:strict && npm run format-check` — clean +- [ ] Manual smoke: configure an LLM key for the dev org, run admin `POST /sync` (or `POST /countries//impact/refresh`), open a tracked changed country's detail page → Impact panel renders with at least one "why". +- [ ] Manual smoke: an org with NO key → no panel, notification carries the "Configure an LLM key…" line; Settings shows the "Configure an LLM key" indicator + "not run yet". +- [ ] Manual smoke: toggle impact analysis OFF in Settings for a key-having org → next sync produces NO impact panel and NO nudge for that org; toggle back ON restores it. +- [ ] Manual smoke: after a run, Settings "Last impact run" shows a recent timestamp. + +--- + +## Self-review notes (spec coverage) + +- Promise + panel → Tasks 5, 10. Two-stage funnel → Tasks 3 (A), 4 (B), 5 (orchestration). Eager-at-sync + isolation + nudge → Task 6. Table + endpoints + staleness + own limiter + route order → Tasks 1, 7, 8. LLM contract (prompt, no JSON mode, validation, filter-only) → Tasks 2 (validation), 4 (prompt/call). No-key 200/null → Task 7. Soft-delete keeps rows → no code (documented, Task 11). Per-run cap (`IMPACT_MAX_ANALYSES_PER_RUN`) → **deferred**: V1 ships without the global cap because Stage A already bounds per-(org,country) cost and the weekly cadence limits fan-out; add it in Task 6 only if a large-feed run proves slow (noted here so it isn't silently dropped). Frontend → Tasks 9, 10. diff --git a/docs/superpowers/specs/2026-06-26-regulations-tracker-design.md b/docs/superpowers/specs/2026-06-26-regulations-tracker-design.md new file mode 100644 index 0000000000..ebc71e3d4d --- /dev/null +++ b/docs/superpowers/specs/2026-06-26-regulations-tracker-design.md @@ -0,0 +1,236 @@ +# Regulations Tracker — design + +> **Date:** 2026-06-26 +> **Status:** approved-pending-implementation +> **Mirrors:** the AI Trust Index module (`2026-06-19-ai-trust-index-design.md`) + +## 1. Summary + +A new VerifyWise app module that watches the public **Global AI Regulations** feed +served by the marketing site (`https://verifywise.ai/api/regulations`), detects when +a country's regulations change (via a per-country content hash), and notifies the +organizations that track that country — in-app and by email. It also ships a frontend +(Browse / Tracked / Settings / Detail) mirroring the AI Trust Index UI. + +The marketing site is the **data source**; the app is the **consumer**. Nothing on the +website side changes. + +This is built natively in the VerifyWise stack (Express + Sequelize + BullMQ + +React/React-Query). The reference files in +`website/verifywise/docs/regulations-tracker-integration/` (Vercel/Next style) supply +only the algorithm; we re-implement it in our conventions. + +## 2. Source feed evaluation (done before design) + +The website data model was reviewed against its actual source +(`lib/regulations-feed.ts`, `regulations-pipeline/history-lib.ts`, the API routes). It +is sound to build on: + +- **Stable `slug`** join key (pure function, `assertUniqueSlugs` guards collisions). +- **Deterministic content hash** — `hashRecord` sorts object keys recursively before + SHA-256, so the hash changes iff content changes. Our change detection depends on + this and it is implemented correctly. +- **Hash covers the full country** (regulations, scope, obligations, penalties, + timeline) → no meaningful change is missed. +- **Precomputed structured diffs** (`history.lastChange.changes[]`) → no NLP needed. +- **Versioned + additive-safe** (`feedVersion: 1`). + +Three caveats baked into this design: + +1. **`generatedAt` is build/revalidation time** (routes are `force-static`, + `revalidate=3600`), not a real-time clock. We use our OWN clock for the + week-idempotency guard; the feed date is only a coarse fallback for a change date. +2. **Hash is sensitive to cosmetic edits.** A copy-edit moves the country hash but + yields an empty `changes[]` (`computeChanges` diffs only name/status/effectiveDate). + This "hash moved, empty diff" case is real and MUST be handled (see §6, the + `unstructured` path) — notify as "Updated — see source," never drop it. +3. **History is file-based and pipeline-driven.** `lastChange` reflects only the most + recent pipeline-detected change; `hashHistory[]` has the full trail. Fine at weekly + cadence. + +The feed `disclaimer`/`scopeStatement` are marked "DRAFT pending legal review" on the +website. We display them verbatim; confirm finalization with the web team before GA. + +## 3. Feed contract consumed + +- `GET /api/regulations` (manifest) — entry point. Per-country `{ slug, name, region, + regulationCount, hash, history, url }`, plus `feedVersion`, `counts`, `generatedAt`. + This is the only call the weekly job needs. +- `GET /api/regulations/country/` (detail) — full regulation list + timeline + + the same `hash`/`history`. Used by the Detail UI (proxied through our backend). + +`history.lastChange.changes[]` variants (rendered with no NLP): + +| `field` | extra keys | line rendered | +|---|---|---| +| `regulation.status` | `regulation, from, to` | `{regulation}: status {from} → {to}` | +| `regulation.effectiveDate` | `regulation, from, to` | `{regulation}: effective date {from} → {to}` | +| `regulation` (added) | `change:"added", value` | `Added: {value}` | +| `regulation` (removed) | `change:"removed", value` | `Removed: {value}` | +| `regulationCount` | `from, to` | usually skipped (implied by add/remove) | + +## 4. Data model (4 tables, mirrors AI Trust Index) + +Global reference data → global tables; tracking + settings → tenant tables. + +| Table | Scope | Mirrors | Columns | +|---|---|---|---| +| `regulation_countries` | **global** | `ai_trust_index_apps` | `slug` PK, `data` JSONB (full FeedCountry), `hash` text, `regulation_count` int, `region` text, `name` text, `is_active` bool default true, `removed_at` timestamptz null, `last_changed_at` timestamptz null, `last_fetched_at` timestamptz | +| `regulation_tracked_countries` | **tenant** | `ai_trust_index_tracked_apps` | `organization_id` FK, `country_slug` text, `tracked_by` int, `created_at` timestamptz. `UNIQUE(organization_id, country_slug)`. **No FK** to `regulation_countries.slug` (so feed re-imports can't cascade-delete tracking rows) | +| `regulation_tracker_settings` | **tenant** | `ai_trust_index_settings` | `organization_id` PK FK, `recipient_user_ids` JSONB default `[]`, `recipient_emails` JSONB default `[]`, `updated_by` int, `updated_at` timestamptz | +| `regulation_tracker_meta` | **singleton** | `ai_trust_index_meta` | `id` PK `CHECK (id=1)`, `seeded_at`, `last_good_count` int, `last_run_week` text | + +Migration DDL uses `verifywise.` prefix; app code uses unqualified names. `timestamps: +false` on all models (explicit columns), matching AI Trust Index. + +### Seed +`database/seeds/regulations-tracker-snapshot.json` — committed snapshot of the manifest +countries, loaded by a seed migration on first install (idempotent: skip if +`regulation_countries` non-empty). Establishes the baseline so the first weekly run +notifies nothing. + +## 5. Backend layers (mirror AI Trust Index file-for-file) + +``` +routes/regulationsTracker.route.ts ← aiTrustIndex.route.ts +controllers/regulationsTracker.ctrl.ts ← aiTrustIndex.ctrl.ts +utils/regulationsTracker.utils.ts ← aiTrustIndex.utils.ts (CRUD, upsertFeedTx, resolveRecipients, getAffectedOrgsBySlugs, currentIsoWeek, getMetaQuery) +utils/regulationsTrackerFeed.ts ← aiTrustIndexFeed.ts (fetchFeed, validateFeed) +domain.layer/models/regulationsTracker/*.model.ts ← aiTrustIndex/*.model.ts (4 models) +domain.layer/interfaces/i.regulationsTracker.ts ← i.aiTrustIndex.ts +services/automations/actions/syncRegulationsTracker.ts ← syncAiTrustIndex.ts +templates/regulations-tracker-digest.mjml ← ai-trust-index-digest.mjml +database/migrations/-create-regulations-tracker-tables.js +database/migrations/-seed-regulations-tracker-snapshot.js +database/seeds/regulations-tracker-snapshot.json +``` + +### Endpoints (8, all `authenticateJWT`) +| Method/path | Auth | Purpose | +|---|---|---| +| `GET /api/regulations-tracker/countries` | any | Browse catalog (from `regulation_countries`) | +| `GET /api/regulations-tracker/countries/:slug` | any | Detail (proxy live feed, fall back to stored `data`) | +| `GET /api/regulations-tracker/tracked` | any | Org's tracked countries | +| `POST /api/regulations-tracker/tracked` | Admin | Track one (`ON CONFLICT DO NOTHING`) | +| `POST /api/regulations-tracker/tracked/bulk` | Admin | Track many (partial-dup safe) | +| `DELETE /api/regulations-tracker/tracked/:slug` | Admin | Untrack (no-op if absent) | +| `GET /api/regulations-tracker/settings` | Admin | Get recipients | +| `PUT /api/regulations-tracker/settings` | Admin | Update recipients | + +Controllers stay thin: `logProcessing`/`logSuccess`/`logFailure`, `STATUS_CODE[xxx](...)`, +`req.organizationId!`/`req.userId!`, admin gate `if (!isAdmin(req.role)) return +res.status(403)...`. Registered in `app.ts`: +`app.use("/api/regulations-tracker", regulationsTrackerRoutes)`. + +## 6. Weekly job — `syncRegulationsTracker` (BullMQ) + +Job name `regulations_tracker_sync`, schedule `0 6 * * 1` UTC (Mondays 06:00). Mirrors +`syncAiTrustIndex` step-for-step: + +``` +1. meta = getMetaQuery(); if meta.last_run_week === currentIsoWeek(new Date()) → return {skipped} + (OUR clock, not the feed's) +2. raw = fetchFeed() with timeout; on throw → log + return {skipped:"fetch failed"} (no writes) +3. validated = validateFeed(raw, meta.last_good_count): + - feedVersion === 1 (else reject) + - counts.countries === countries.length (else reject) + - countries.length >= 20 (absolute floor) (else reject) + - last_good_count != null && countries.length < last_good_count*0.5 → reject + - presentSlugs = ALL slugs present in raw (even malformed rows), so a + present-but-malformed country is NOT treated as removed + - valid = rows with required keys {slug, name, hash, region} + on !ok → log + return {skipped:reason} (no writes) +4. {materialChanged, newlyRemoved, wasFirstSeed} = upsertFeedTx(valid, presentSlugs, rawCount): + - upsert each country (data, hash, region, name, regulation_count, last_fetched_at; + set last_changed_at when hash moved); compute changes from history.lastChange + - countries in catalog but NOT in presentSlugs → is_active=false, removed_at=now (soft delete) + - wasFirstSeed = catalog was empty before this run + - all in ONE transaction +5. if wasFirstSeed → log, suppress ALL notifications, return +6. changedSlugs = unique(materialChanged.slug ∪ newlyRemoved) +7. affected = getAffectedOrgsBySlugs(changedSlugs) // only orgs TRACKING those slugs +8. group by org → { changed: DigestItem[], removed: DigestItem[] } + - DigestItem detail = rendered changes joined; if changes empty (cosmetic hash + move) → detail = "Updated — see source" (the `unstructured` path) +9. per org: + - EMAIL: recipients = resolveRecipients(orgId) (configured recipient_user_ids + resolved to emails ∪ recipient_emails); NO admin fallback; empty → log + skip email + - IN-APP: via `notification.utils.ts` (one row per user: organization_id, + user_id, type, title, message, entity_type="regulation_country", + entity_id=slug-or-null). Recipients = org Admins ∪ configured + recipient_user_ids (deduped). Sent always, even if email is skipped. +10. update meta.last_run_week = thisWeek, last_good_count = rawCount +``` + +### Security +`escapeHtml()` every feed-derived string (country name, slug, change values) before +injecting into the MJML digest. Feed is first-party but this is defense-in-depth, +matching `syncAiTrustIndex`. + +### BullMQ registration hazard +Several existing schedulers call `automationQueue.obliterate({ force: true })`, which +wipes ALL repeatable jobs in the shared `automation-actions` queue. `scheduleRegulationsTrackerSync()` +MUST: (a) NOT call obliterate, and (b) be registered in `addAllJobs()` AFTER every +obliterating scheduler (i.e. near the end, alongside `scheduleAiTrustIndexSync`). + +## 7. Detail proxy (`GET /countries/:slug`) + +1. Fetch `https://verifywise.ai/api/regulations/country/` with a short timeout. +2. On success → return it. +3. On feed failure/timeout → fall back to the stored `regulation_countries.data` JSONB + (we always have the last-known snapshot) with a `stale: true` flag. +4. Unknown slug not in our catalog → 404. + +Never block the page on a slow external call. + +## 8. Frontend (mirror AI Trust Index) + +``` +application/repository/regulationsTracker.repository.ts +application/hooks/useRegulationsTracker.ts (React-Query, KEY="regulations-tracker", keepPreviousData) +application/contexts/RegulationsTrackerSidebar.context.tsx +presentation/pages/RegulationsTracker/index.tsx + ├─ Browse/index.tsx (all countries + track button; world-region grouping) + ├─ Tracked/index.tsx (org's tracked countries) + ├─ Settings/index.tsx (recipient_user_ids + recipient_emails) + └─ CountryDetail/index.tsx (regulations, timeline, change history; via proxy) +presentation/pages/RegulationsTracker/RegulationsTrackerSidebar.tsx +config/routes.tsx (lazy imports + registrations; bare path → Browse) +``` + +Use VerifyWise components (CustomizableButton, CustomizableBasicTable, Chip, SearchBox, +EmptyState, PageHeader, TabBar). Display the feed disclaimer verbatim on Browse/Detail. + +## 9. Edge cases (consolidated) + +| # | Edge case | Handling | +|---|---|---| +| A | Truncated/partial feed | floor 20 + 50%-of-last-good guard → reject, no writes | +| B | Present-but-malformed country | counted in `presentSlugs`, excluded from valid → NOT marked removed (no false alert) | +| C | First install run | seed migration baselines; `wasFirstSeed` suppresses all notifications | +| D | Hash moved, empty `changes[]` (cosmetic) | `unstructured` → "Updated — see source" | +| E | Digest HTML injection | `escapeHtml` all feed strings | +| F | Org with no configured email recipients | email skipped + logged; in-app still sent to admins | +| G | BullMQ obliterate wipes repeatables | our scheduler never obliterates + registered last | +| H | Live detail feed down | fall back to stored `data` JSONB, `stale:true` | +| I | Re-track / partial bulk dup | `ON CONFLICT (organization_id, country_slug) DO NOTHING` | +| J | Untrack non-tracked | no-op, 200 | +| K | Country removed from feed then returns | `is_active=true`, `removed_at=null` on re-appear | +| L | Two changes between polls | `lastChange` = latest; acceptable at weekly cadence | +| M | feed `generatedAt` is stale build time | week guard uses our clock; feed date only fallback | + +## 10. Testing + +- **Unit:** `validateFeed` (version/floor/50%/count/presentSlugs), `renderChangeLine` + (all variants + unstructured), `escapeHtml`, `currentIsoWeek`, recipient resolution + (no-fallback), `upsertFeedTx` (change detection, soft-delete by presentSlugs, + wasFirstSeed, re-appear). +- **Integration:** track/untrack/bulk idempotency, settings round-trip, detail proxy + fallback, admin-gate 403s, tenant isolation (org A cannot see org B tracking). +- Mirror AI Trust Index test files where they exist. + +## 11. What is NOT in scope +- Researching/verifying regulation changes (website pipeline does this). +- Computing diffs (feed precomputes `changes[]`). +- Hosting/auth/caching the feed (public, CORS-open, CDN-cached). +- Translations of regulation content (feed serves English; localize UI chrome only). diff --git a/docs/superpowers/specs/2026-06-27-regulation-impact-analysis-design.md b/docs/superpowers/specs/2026-06-27-regulation-impact-analysis-design.md new file mode 100644 index 0000000000..871e3cd5ad --- /dev/null +++ b/docs/superpowers/specs/2026-06-27-regulation-impact-analysis-design.md @@ -0,0 +1,340 @@ +# Regulation Impact Analysis — design spec (V1: detect-only) + +> **Status:** Design approved 2026-06-27. Builds on the Regulations Tracker module +> (`feat/regulations-tracker`). Scope: detect-only, LLM-key-gated. Task creation, +> completion tracking and audit evidence are explicitly OUT of V1 (future work). +> +> Reference: `docs/technical/domains/regulations-tracker.md` (the base module). + +--- + +## 1. Problem & promise + +Today's Regulations Tracker is a passive feed: a tracked country's regulations change → the org +gets a notification + a detail page. The human must then work out what the change *means for their +organisation*. This feature closes that gap. + +**Promise (for orgs with an LLM key configured):** when a tracked country's regulation changes, the +country detail page shows an **Impact panel**, and the change notification carries the headline counts: + +> **This change affects your organisation** +> ✓ 4 AI systems — *Fraud Scorer, Resume Ranker, …* +> ✓ 9 controls require review +> ✓ 2 policies may be outdated +> ✓ 5 vendors impacted +> ✓ 12 assessments should be updated + +Each line expands to the specific entities, each with a one-sentence **"why"** the LLM produced. + +**Orgs without an LLM key** keep today's behaviour unchanged (notification + detail page), **plus** a +single appended line on the change notification nudging the admin to configure a key. No empty panel, +no separate nag notification. + +This is a **pure additive, key-gated upgrade**. Nothing in the existing module changes for keyless orgs. + +--- + +## 2. Architecture — two-stage funnel + +The schema reality (see §6) is that the org graph stores **region** (a coarse enum) and **framework +name**, while the feed gives a **country** + free-text obligations. Pure attribute joins cannot +credibly produce "*these 4 systems*." So we narrow deterministically, then let the LLM decide precisely. + +``` +Regulation change (country X, framework type, obligations[], lastChange diff) + │ + Stage A — Deterministic candidate filter (no LLM, cheap, always runs) + │ Produces an over-inclusive CANDIDATE SET per entity type. + │ A type with zero candidates is skipped entirely (no LLM call, no spend). + │ + Stage B — LLM verdict (org's llm_keys config, via runAdvisorAiSdk) + │ ONE call PER ENTITY TYPE. Filter-and-annotate only. + │ Each candidate → { affected: bool, why: string }, validated against + │ the candidate set we sent. Never additive. + │ + Impact panel = candidates where affected === true, grouped by type. + Notification counts = the validated affected-only counts. +``` + +**Why this split:** Stage A keeps the LLM bounded — we never ask the LLM about the whole org, only +plausible candidates. Stage B supplies the precision and the human-readable "why" the columns can't. + +**Safety guarantee:** the LLM can only *filter and annotate* the candidate set Stage A already found. +It can never introduce an entity, id or name we didn't send. We enforce this in code (validation, §5). + +--- + +## 3. Stage A — deterministic candidate filter + +For a changed country (slug, mapped region, framework name(s) the regulation maps to): + +| Entity type | Candidate rule | Strength | +|---|---|---| +| **AI systems** (`projects`) | `geography` matches country→region map, **OR** linked via `project_frameworks` to the regulation's framework | Coarse (region, not country) — LLM refines | +| **Controls** | belong to a project whose framework matches (3-hop: `controls→control_categories→projects→project_frameworks→frameworks`) | OK when regulation maps to a framework | +| **Assessments** | `project_id` ∈ candidate projects | Inherited from project | +| **Vendors** | `regulatory_exposure` maps to the framework, **OR** linked (`vendorsProjects`) to a candidate project | Weak (single-value enum) — LLM refines | +| **Policies** | linked (`policy_linked_objects`, `object_type='control'`) to a candidate control | **Weakest** — standalone policies unmatched (V1 limit) | + +- **Country → region map** is a static config (the `geography` enum has no DB lookup table): + `1 Global, 2 Europe, 3 North America, 4 South America, 5 Asia, 6 Africa`. The map assigns each feed + country to one region; "Global" candidates always included. This is intentionally lossy — Stage B + reads the actual country name from the regulation and can reason an entity is out of scope. +- **Framework match** is by `frameworks.name` (exact string; the 4 seeded names are + `EU AI Act`, `ISO 42001`, `ISO 27001`, `NIST AI RMF`). Framework IDs are not stable across installs. + A static map associates a regulation's `type`/jurisdiction to framework name(s) where applicable. +- All queries tenant-scoped: unqualified table names, `WHERE organization_id = :organizationId`. + +--- + +## 4. Timing — eager at sync time + +When the weekly sync (or admin `POST /sync`) detects a country change, the impact analysis runs inside +the existing notification phase of `syncRegulationsTracker.ts`. That phase is an **outer per-org loop** +(`for (const [orgId, countries] of byOrg)`, ~line 262) with an **inner per-country loop** +(`for (const c of countries)`, ~line 275). The impact-analysis hook sits at the **top of the inner +loop**, before the per-user notification fan-out. + +**Per org (resolved once, at the top of the `byOrg` loop, before the country loop):** +- `hasKey = (await getLLMKeysWithKeyQuery(orgId)).length > 0`. Resolve **once per org**, not per country + (the result also drives the no-key nudge in §7). +- `impactEnabled` — read from `regulation_tracker_settings.impact_enabled` (default `true`; see §5a). + When a key exists but `impactEnabled === false`, the org has **opted out**: skip Stage A/B entirely + and emit today's plain notification with **no** counts and **no** nudge (they made a choice). +- After the country loop, if any impact pass ran for the org, set + `regulation_tracker_settings.last_impact_run_at = NOW()` (the Settings "Last impact run" line, §5a). + +**Per (org, country) — inside the inner loop, only when `hasKey && impactEnabled`:** +1. **Fetch detail.** `CountryChange`/`c` carries only `lines[]`/`changeCount`/`changeDates` — NOT + obligations. Query `regulation_countries WHERE slug = :slug` for `data` (JSONB) + `hash` to get + `obligations[]`, `name`, `type`, `status`, `maxPenalty`, country name. (One query per changed country, + not per org — cache the detail in a `Map` across the outer loop.) +2. **Cache check.** If a `regulation_impact_analysis` row for `(orgId, slug)` already has + `regulation_hash === current hash` and `status='ok'`, reuse it — no LLM. (Covers re-runs and admin + refresh hitting an unchanged hash.) +3. **Stage A** per entity type. All five empty → persist `status='skipped_no_candidates'`, + `result=NULL`; plain notification (today's `c.lines`, no counts). +4. **Stage B** — one LLM call **per non-empty type**, the 5 calls run with `Promise.all` (bounded: + ≤5 concurrent per country). Validate, assemble `result`, upsert `status='ok'`. +5. **Build the count-bearing notification message** from validated affected-only counts (§7). + +**Isolation (mandatory, not optional):** `runAdvisorAiSdk` **throws** on LLM error, and the whole +notification phase sits inside a try/catch (line ~231) that calls `recordRunStatus("error")` and +**rethrows** — so an unguarded throw here would mark the entire sync failed even though the catalogue +write and other orgs' notifications succeeded. Therefore each per-(org,country) impact analysis is +wrapped in its **own** try/catch: on any failure → log, upsert `status='error'`, `result=NULL`, and +fall back to today's plain notification (org has a key, so **no** nudge line). Continue the loop. + +**Cost & latency bounding:** only orgs tracking the changed country, only those with a key, only +non-empty types; ≤5 concurrent LLM calls per (org, country) via `Promise.all`; country detail fetched +once per slug and memoised; cached by hash so re-runs are free. A **per-run safety cap** +(`IMPACT_MAX_ANALYSES_PER_RUN`, default e.g. 200) bounds total LLM calls in one sync; analyses beyond +the cap are skipped (logged), so a feed touching many countries can't fan out unboundedly. + +--- + +## 5. Data model & API + +### Table `regulation_impact_analysis` (tenant-scoped) + +DDL uses the `verifywise.` prefix; app queries use unqualified names. New migration file (timestamp via +`date +%Y%m%d%H%M%S`). + +| Column | Type | Note | +|---|---|---| +| `id` | `SERIAL PRIMARY KEY` | module convention (explicit surrogate key) | +| `organization_id` | `INTEGER NOT NULL REFERENCES verifywise.organizations(id) ON DELETE CASCADE` | tenant scope | +| `country_slug` | `VARCHAR NOT NULL` | which regulation | +| `regulation_hash` | `VARCHAR NOT NULL` | cache key; analysis is stale once `regulation_countries.hash` moves | +| `result` | `JSONB` (**nullable**) | `{systems:[{id,name,why}], controls:[…], policies:[…], vendors:[…], assessments:[…], generatedAt}`. NULL when `status ≠ ok` — documented exception to the module's NOT-NULL-JSONB norm | +| `status` | `VARCHAR(120) NOT NULL` | free-text like `last_run_status`; values `ok` / `skipped_no_candidates` / `error` | +| `model` | `VARCHAR` | which LLM produced it (out of JSONB for queryability) | +| `created_at` | `TIMESTAMPTZ NOT NULL DEFAULT NOW()` | | +| `refreshed_at` | `TIMESTAMPTZ NOT NULL DEFAULT NOW()` | bumped on every upsert — "analysis last run" | +| | `UNIQUE (organization_id, country_slug)` | one current analysis per org+country | + +Upsert: `INSERT … ON CONFLICT (organization_id, country_slug) DO UPDATE` (set result/status/model/ +hash/refreshed_at). + +### Endpoints (added to `regulationsTracker.route.ts`) + +> **⚠ Route ordering:** `GET /countries/:slug/impact` MUST be registered **before** +> `GET /countries/:slug`, or Express captures `slug="france/impact"` and routes to the wrong handler. + +| Method/path | Auth | Behaviour | +|---|---|---| +| `GET /countries/:slug/impact` | any authed user in org | Single JOIN'd query against `regulation_countries` returns `{ result, status, refreshed_at, stale }` where `stale = (stored regulation_hash ≠ current hash)`. Returns `200` with `null` when no analysis row exists (keyless orgs simply have no row — the endpoint does NOT probe key state). Read-only. | +| `POST /countries/:slug/impact/refresh` | **Admin** | Manually re-run analysis against the current hash. Covers "I just set my key" / "I added a system." | + +- Admin guard: **first line in the controller**, reuse `isAdmin` helper (`regulationsTracker.ctrl.ts:26`), + `return res.status(403).json(STATUS_CODE[403]("Admin access required"))`. No route-layer RBAC. +- Rate limiter: a **new** `regulationsTrackerImpactLimiter` (own config block in `RATE_LIMIT_CONFIGS`), + not the sync limiter — so refresh and sync windows don't couple. Placed after `authenticateJWT`, + before the controller. +- Response helper: `STATUS_CODE[200](data)` (always 200 for success). Errors `STATUS_CODE[500]((error as Error).message)`. +- The sync job writes rows via the same util the refresh endpoint calls (`runImpactAnalysis(orgId, country)`); + the GET endpoint only ever reads. + +--- + +## 5a. Settings page additions + +Three items are added to the existing Regulations Tracker **Settings** page (admin view). They reuse the +existing tenant settings table `regulation_tracker_settings` and the existing GET/PUT `/settings` +endpoints — no new settings endpoint. + +**Schema changes** (migration adds two columns to `regulation_tracker_settings`): +- `impact_enabled BOOLEAN NOT NULL DEFAULT true` — existing rows light up enabled. +- `last_impact_run_at TIMESTAMPTZ` (nullable) — bumped by the sync after an org's impact pass. + +**GET `/settings` response gains:** +- `impact_enabled` (boolean) — from the row. +- `last_impact_run_at` (timestamp | null) — from the row. +- `has_llm_key` (boolean) — **computed** at request time via `getLLMKeysWithKeyQuery(org).length > 0`, + never stored (always reflects reality, no drift). + +**PUT `/settings` gains:** an optional `impact_enabled` boolean (Admin only, same endpoint/guard as the +existing recipient updates). Recipients and `impact_enabled` can be saved in the same call. + +**The three UI items (admin view):** +1. **Enable/disable toggle** — `Toggle` (from `components/Inputs/Toggle/`, a new pattern on this page), + bound to `impact_enabled`, auto-saved (debounced) like the existing recipient fields. Default ON. +2. **LLM-key status indicator** — read-only line driven by `has_llm_key`: "Impact analysis: active" + when true, else "Configure an LLM key to enable impact analysis" with a deep link to LLM key settings. +3. **"Last impact run" status line** — read-only: "Impact analysis last ran: {date}" from + `last_impact_run_at` (or "Not run yet" when null). Mirrors the existing "Last checked" cadence line. + +**Gating interaction:** the toggle gates the sync (`hasKey && impactEnabled`, §4) and the refresh +endpoint (a `POST …/impact/refresh` when `impact_enabled === false` returns `200` with +`status:"disabled"` and does not call the LLM). + +--- + +## 6. LLM contract (Stage B) + +Reuses the AI Advisor mechanism (NOT the AI Gateway): `getLLMKeysWithKeyQuery(organizationId)` returns +the org's keys including the decrypted `key`, `name` (provider), `url`, `model`. We pick the first key +(`clients[0]`, matching the advisor's default) and build `AiSdkAdvisorParams`: +`{ apiKey: key, baseURL: url || getLLMProviderUrl(name), model, provider: name, tenant: orgId, +userPrompt, availableTools: {}, toolsDefinition: [] }`. `userId`/`sessionId` omitted (no memory writes +wanted). **No JSON mode exists** — we instruct JSON in the prompt and `JSON.parse` the returned string, +guarded by try/catch. `runAdvisorAiSdk` returns `Promise` and **throws** on any LLM/provider +error — every call is inside the per-(org,country) try/catch from §4. + +**One call per entity type** (five types). A type with no Stage-A candidates is skipped. Each type's +prompt uses the same six rules; only the type noun/verb and the closing instruction differ +("controls require review," "policies may be outdated," "vendors impacted," etc.). One type failing +validation drops only that type's line — the rest of the panel still renders (partial-failure resilient). + +### System prompt (per type — verb adapted) + +> You are a compliance analyst assessing how a specific change to an AI regulation affects a list of an +> organisation's governance entities. You will be given: the regulation's identity and country, the +> **specific change that just occurred** (not the whole regulation), and a numbered list of candidate +> entities, each with a type, id, name and description. +> +> For **each** candidate, decide whether *this specific change* plausibly creates new or altered +> obligations for that entity. +> +> Rules you must follow: +> 1. **Judge the change, not the regulation in general.** An entity is "affected" only if the described +> change alters what the organisation must do about it. If the entity is subject to the regulation but +> this particular change doesn't touch it, mark it not affected. +> 2. **Be conservative — when unsure, mark not affected.** A false "affected" wastes the team's time and +> erodes trust. Only mark affected when the link is clear from the text provided. +> 3. **Use only the information given.** Do not assume facts about an entity beyond its description. Do +> not infer geography, sector or framework that isn't stated. +> 4. **Only reason about entities in the provided list.** Never introduce an entity, id or name that was +> not given to you. +> 5. For each affected entity, give **one sentence** stating the concrete reason, citing the specific +> obligation or change. No generic statements like "this regulation is important." +> 6. If a candidate is not affected, still return it with `affected: false` and a short reason. +> +> Return **only** valid JSON matching the schema. No prose outside the JSON. + +### User message (structured) + +``` +REGULATION: {name} ({type}, {status}) — {country} +THE CHANGE: {history.lastChange.changes[] as bullet lines} +KEY OBLIGATIONS: {obligations[] joined} +MAX PENALTY: {maxPenalty} + +CANDIDATE ENTITIES: +[system] id=42 "Resume Ranker" — {description/oneliner} +[system] id=51 "Fraud Scorer" — {…} +... +``` + +### Output schema (enforced in code) + +```json +{ "results": [ { "type": "system", "id": 42, "affected": true, "why": "…" } ] } +``` + +Validation: every `id` must be one we sent; `type` must match what we sent for that id; `affected` +boolean; `why` non-empty. Drop anything malformed or hallucinated. If the whole response is unusable → +`status='error'`, `result=null`, fall back to plain notification. + +--- + +## 7. Notifications + +- **Affected (key present):** the existing per-country deep-linked notification gains the headline + counts ("4 AI systems affected, 9 controls need review …"), deep-linking to + `/regulations-tracker/` where the full panel renders. Built from validated affected-only counts. +- **Keyless org, change affects them:** the per-(org,country) notification `message` (built at + ~line 294) gains **one appended line** — "Configure an LLM key to see which of your AI systems, + controls and vendors this affects." — deep-linking to LLM key settings. Gated on the per-org `hasKey` + flag resolved once at the top of the `byOrg` loop (§4), so the line is appended uniformly to every + affected country's message for that org. Only appears when there's a real change AND the org has no + key. No separate notification, no nag flag. +- **Stage B errored (org has a key):** fall back to today's plain notification (`c.lines`) — no counts, + **no** nudge line (the org has a key; nudging would be wrong). + +--- + +## 8. Frontend + +`pages/RegulationsTracker/CountryDetail` gains an **Impact** section (key-configured orgs only): +- Five collapsible lines (systems / controls / policies / vendors / assessments) with counts; each + expands to the entities + their "why". +- A `stale` banner when the stored analysis predates the current hash, with an admin "Re-analyse" action + (`POST …/impact/refresh`). +- Keyless orgs: no panel (the appended notification line is the only surface). + +`pages/RegulationsTracker/Settings` gains the three items from §5a (toggle + two read-only status lines), +wired through the existing `useSettings`/`useUpdateSettings` hooks (the response just carries the new +fields). + +Repository/hook additions mirror the existing `regulationsTracker.repository.ts` / `useRegulationsTracker.ts` +patterns (KEY=`"regulations-tracker"`). i18n: page-level strings de/fr/es (including the new Settings +labels and the impact-panel title/stale banner); inline `Label: {value}` JSX is English-only, consistent +with the rest of the module. + +--- + +## 9. Known V1 limitations (explicit) + +- **Country → region is lossy.** Two countries in the same region share Stage-A candidates; Stage B + disambiguates by reading the country name. Acceptable because the LLM refines. +- **Standalone policies are unmatched.** Only policies linked to an affected control are caught. V2: + add a `framework_id`/jurisdiction column to `policy_manager` or richer policy linking. +- **No task creation / completion tracking / audit evidence** — these are the "Act" and "Prove" phases, + deliberately out of V1. +- **Soft-deleted (removed) countries keep their impact rows.** When a country is soft-deleted + (`is_active=false`), we do NOT delete its `regulation_impact_analysis` rows — they're harmless tenant + data and the country is already hidden from list/detail queries. The `org→organizations` FK + `ON DELETE CASCADE` still cleans up when an org is deleted. +- **Per-run cap.** `IMPACT_MAX_ANALYSES_PER_RUN` bounds LLM fan-out in a single sync; over-cap analyses + are skipped and logged (no row written), and will be picked up on the next change or via admin refresh. + +--- + +## 10. Future work (V2+) + +- "Act": one-click remediation task creation into the Tasks module from affected entities. +- "Prove": exportable audit-evidence record tying a regulation change to its remediation. +- Policy precision (schema change above). +- LLM-written rationale could be cached/surfaced in the changelog (Horizon) feed. +- Optional on-demand re-analysis when an org adds a new AI system/vendor (not just on regulation change). diff --git a/docs/superpowers/specs/2026-06-27-regulations-tracker-handover.md b/docs/superpowers/specs/2026-06-27-regulations-tracker-handover.md new file mode 100644 index 0000000000..5608843fa4 --- /dev/null +++ b/docs/superpowers/specs/2026-06-27-regulations-tracker-handover.md @@ -0,0 +1,46 @@ +# Regulations Tracker — session handover (2026-06-27) + +Quick-start for picking this up after `/clear`. Full technical reference: +`docs/technical/domains/regulations-tracker.md`. Original spec/plan (base module only): +`docs/superpowers/specs/2026-06-26-regulations-tracker-design.md`, +`docs/superpowers/plans/2026-06-26-regulations-tracker.md`. + +## State +- **Branch:** `feat/regulations-tracker` — pushed, in sync, ~33 commits. **NO PR opened** (holding for explicit go-ahead). +- **Gates (last verified):** Servers build OK; ~53 RT backend tests pass; API drift 0 (681 ops); + Clients typecheck + i18n (100%, de/fr/es) + format-check all clean. +- **Local DB:** migrated + seeded with full per-country detail (60 countries) + global feeds + (horizon 24, deadlines 3, frameworks 11). A real sync run was exercised (changed:0, baselined). + +## What was built this session (beyond the base module) +1. **Full-detail storage + detail-page fix** — catalog stores `{...country, meta}` (not summary), + so detail pages render offline; controller normalizes live + stale to one flat shape. +2. **UI polish** — country flag emojis on rows + detail header; green tick for tracked rows; 8px row + gap; `VWLink` (with new `alwaysShowIcon` prop) for source links; timeline newest-first. +3. **Field completeness** — render `lastVerified`, `dateConfidence`, framework `namedDocuments`, + plus the narrative Overview (oneLiner/executiveSummary/practicalTakeaway). +4. **Three global-feed tabs** — Horizon (changelog), Deadlines, Frameworks. Backend feed fetchers + + `getGlobalFeed`/`setGlobalFeeds` on the meta singleton + 3 endpoints + 3 pages + sidebar/routes. +5. **Update-workflow improvements (the "(b)" backlog):** + - Deep-linked per-country in-app notifications with the actual change detail. + - `last_run_at`/`last_run_status` observability + "Last checked" in Settings. + - Multi-change note via `countChangesSince` (hashHistory). + - New-country admin alerts (`getAllOrgAdmins`). + - Admin-only, rate-limited `POST /sync` "Check for updates now" + button. +6. **Two `/code-review` passes (high effort, workflow-backed), all findings resolved** — incl. the + blocking weekly-job enum crash, stale-detail blank page, bulk-track DoS guards (pass 1), and the + stale-watermark-on-failure, last_good_count=valid, swallowed-error, concurrency guard, + empty-200 fallback (pass 2). Several "flat feed shape" findings adjudicated as false positives + (feed IS nested under `country`; the test mock was fixed). + +## Earlier in the same session (separate, already shipped/handled) +- **Bug 2 — file-content JSON-buffer corruption:** root-caused + data-repair migration + `20260626043929-repair-files-content-json-buffer-corruption.js` (JS, not SQL — SQL per-byte + approach blew 30GB tmp). Ran locally, 41/41 rows fixed. See + `memory/bug-file-content-json-buffer-corruption.md`. + +## To resume / finish +- **Open the PR** when told (summary-only body, no test-plan section, sentence case — per user conventions). + Run pre-PR gates first: `cd Servers && npm run build`; `cd Clients && npm run typecheck && npm run i18n:audit:strict && npm run format-check`. +- Possible follow-ups (all optional, noted in the domain doc §7): refactor the `getStoredHashes` + double-query; frontend component tests; confirm the feed legal disclaimer is finalised before GA. diff --git a/docs/superpowers/specs/2026-06-28-regulations-tracker-session-handover.md b/docs/superpowers/specs/2026-06-28-regulations-tracker-session-handover.md new file mode 100644 index 0000000000..9f04dc6cbf --- /dev/null +++ b/docs/superpowers/specs/2026-06-28-regulations-tracker-session-handover.md @@ -0,0 +1,151 @@ +# Regulations Tracker — session handover (2026-06-28) + +> Durable handoff for resuming after `/clear`. Captures branch/PR state, everything +> built this session, and the open items. Pairs with the technical reference at +> `docs/technical/domains/regulations-tracker.md` and the design spec at +> `docs/superpowers/specs/2026-06-27-regulation-impact-analysis-design.md`. + +## TL;DR + +The Regulations Tracker module (base + impact analysis) plus a large round of UI/UX +improvements, bug fixes, role-permission fixes, and user-guide documentation are +**complete, committed, pushed, and on two open PRs**. Gates are green. Nothing is +mid-flight in code. + +## Branches & PRs + +| Branch | HEAD | PR | Base | State | +|---|---|---|---|---| +| `feat/regulations-tracker` | `8088a27ff` | **#4198** "Regulations tracker with AI regulation impact analysis" | `develop` | OPEN, fully pushed (0 unpushed) | +| `fix/model-tablename-mismatch` | `f48607235` | **#4199** "Fix tableName mismatch on VendorsProjects and ProjectFrameworks models" | `develop` | OPEN, assigned to **HarshP4585**, pushed | + +Current checked-out branch: `feat/regulations-tracker`. + +**Uncommitted in the working tree (intentionally NOT part of any PR):** +`Clients/src/presentation/components/IconButton/index.tsx` (a pre-existing PDF-download +fix that was already dirty at session start, unrelated to this feature) and +`.claude/scheduled_tasks.lock` + `.claude/settings.json` (local config). Leave them, or +decide separately where the IconButton fix belongs. + +## What shipped this session (on `feat/regulations-tracker`) + +The branch was already at a "feature complete, PR open" state at session start +(`7cf5aecc9`/PR #4198). Everything below was added on top: + +**Two `/code-review` (high, workflow-backed) passes** — the second one found and fixed +real runtime bugs the mocked unit tests had hidden: +- **Broken SQL: `= ANY(:param)`** — Sequelize expands an array replacement to a comma list, + so `ANY('a','b')` is a syntax error. Found in the deadlines flag-enrichment AND in 6 + Stage A impact queries (would have crashed `getCandidates` at runtime). All switched to + `IN (:param)` with empty-array sentinels. Plus a wrong table name + `project_frameworks` → `projects_frameworks` (DB table is plural). +- A full static SQL audit of all 33 queries in the feature confirmed no remaining broken + sections. (`820139873`, `99f6f46f1`, and round-2 fixes in `ed2fd94eb`.) +- Round-2 also fixed: runway click-to-jump wrong-row-on-duplicate (identity map); + `newlyAdded` count never returned by the sync; controller raw SQL moved to utils; + `getDeadlines` parallelized; `getImpactAnalysis` fail-open settings guard; `formatDate` + Invalid-Date guard; Frameworks VWLink; STAGE_DELAYS invariant comment. + +**Country flags** — the deadlines enrichment was silently broken (the ANY() bug above). +Fixed, plus an idempotent migration `20260628110553-backfill-regulation-country-flags.js` +backfilling `data->>'flag'` into existing `regulation_countries` rows (older installs were +seeded before the snapshot carried flags). Flags now render on Browse, Tracked, Deadlines. + +**UI/UX improvements** (all user-requested): +- Deadlines "next 12 months" runway calendar (built with the frontend-design skill): + horizontal month strip, urgency heat tint for the nearest ~90 days, real VWTooltips on + the markers, click-a-marker-to-jump-to-row. (`044c8f936`, `6ae747f91`.) +- Status chips colored by content across all pages via a shared `regulationStatusVariant` + helper (`statusVariant.ts`). (`0ed4ba830`.) +- Frameworks page: 2-column card grid + "Looking for the EU AI Act? Find them in Browse" + callout (the EU AI Act is a country-level reg, not an international framework). (`9f69debe5`.) +- "Check for updates now": simulated staged progress display (frontend-only). (`00864af35`.) +- Tracked page: per-row metadata line (N regulations / last changed / tracked since). (`28651354e`.) +- Browse row checkboxes: equal-size for tracked vs untracked (replaced the native checkbox + with a Lucide Square/CheckSquare button so sizes match). (`592537a1a`.) +- Country detail: connected vertical timeline rail replacing disconnected dots. (`7b430828d`.) +- Settings: plain-language help text on the impact toggle (humanized), and an LLM-key status + indicator showing which provider/model impact analysis will use + manage-keys link. + (`da9583723`, `da9901690`.) + +**Permissions / RBAC** (two user-driven corrections): +- **Editors can now track.** Tracking (track / untrack / bulk) was Admin/SuperAdmin only; + added a `canTrack` helper allowing **Admin, SuperAdmin, Editor**. Settings, sync, and + refresh stay Admin-only. Gated the Browse UI (per-row button, checkbox, bulk toolbar) so + ineligible roles see a read-only catalogue. Updated controller role tests (46 pass). + (`c4f90979f`.) +- **Super-admins are read-only inside an org.** A super-admin (role_id 5, org-less, enters + an org via the `X-Organization-Id` header) gets read-only access — the backend + `superAdminReadOnly` middleware 403s every non-GET. The UI was showing them write + controls that would fail. Excluded super-admins from `canTrack` (Browse + CountryDetail), + from the CountryDetail Re-analyse button, and from the Settings editable view, so a + super-admin sees the whole module read-only. (`8088a27ff`.) + +**Documentation** — there were NO user-guide docs for the module (the `helpArticlePath` +ids resolved to an empty drawer). Wrote 6 articles (browse, tracked, horizon, deadlines, +frameworks, settings) in the `ArticleContent` block format, registered them in +`shared/user-guide-content/` (`content/index.ts` + `userGuideConfig.ts` collection), and +ran them through the humanizer (scored "Human"). Verified the in-app help drawer now +renders content. (`59360ecba`, plus the role-doc tweak in `8088a27ff`.) + +**Other commits this session that touched cadence/digest/empty-states** (these appear in +the log; if you did not author them in the foreground they came from parallel/background +work — verify before relying on the descriptions): `b7c859772` weekly→daily sync cadence; +`baa3a150c` de-weekly the digest subject; `13382ebfb` friendlier empty states + Deadlines +country drill-down; `b73321b6b` move impact analysis into the email digest. + +## Website docs — ACTION REQUIRED (user) + +The same 6 article files + the matching `content/index.ts` and `userGuideConfig.ts` edits +were **copied into** `/Users/gorkemcetin/website/verifywise/content/user-guide/` (byte-identical +format). Per the project rule, these were **left uncommitted** — the website repo is the +user's to commit and publish. Files present: +`content/user-guide/content/regulations-tracker/{browse,tracked,horizon,deadlines,frameworks,settings}.ts`. + +## Gates (last verified green) + +- Backend: `cd Servers && npm run build` clean; `npm run check:api-drift` 683=683; + `npx jest ... regulationsTracker.ctrl` 46/46; impact + sync suites green. +- Frontend: `cd Clients && npm run typecheck && npm run i18n:audit:strict && npm run format-check && npm run build` — all clean. +- **Jest flag gotcha:** the plan's `npm run test -- --testPathPattern=X` is STALE for this + Jest version (flag renamed; the old one is ignored → runs the whole suite). Use + `npx jest --testPathIgnorePatterns='/tests/integration/|/helpers/' --forceExit ` + from the `Servers` dir. + +## Local-environment notes (not code bugs) + +- The local dev DB is a legacy multi-schema install (old tenant-hash schemas + `1HNeOiZeFu` / `a4ayc80OGd` alongside `verifywise`). The org's data is in `verifywise`, + but `verifywise.controls` is missing locally (the DB has 107 migrations run vs 91 on this + branch — it's ahead/mixed from other branches). This means `getCandidates` can't fully run + locally; CI/prod build the full schema correctly. User chose to "leave it." +- Browse/Tracked/Deadlines flags were null locally until the backfill migration ran; they + now render. New installs get flags from the seed snapshot. + +## Open / follow-up items + +1. **Two PRs awaiting review/merge:** #4198 (the module — large, two code-review rounds done) + and #4199 (model fix, assigned to Harsh). Neither merged yet. +2. **Website docs** — uncommitted, user to publish (above). +3. **Shared-model `tableName` bug is fixed on #4199 only** — not in #4198. Keep them separate. +4. **Read-only-super-admin UI gap likely exists in OTHER modules.** This session only fixed + Regulations Tracker. A super-admin viewing an org may see write buttons elsewhere that + 404/403. Worth a broader audit as a separate task — not done. +5. **Known V1 limits of impact analysis** (documented in the design spec, unchanged): country→ + region mapping is coarse (the LLM refines it); standalone policies only matched via linked + controls; vendor exposure has no enum value for ISO 42001 / NIST AI RMF (those vendors are + caught via the project-link path — documented in code, not a bug). +6. **IconButton working-tree change** — pre-existing, unrelated, still uncommitted. Decide + where it belongs. + +## Key files (orientation) + +- Backend impact: `Servers/utils/regulationImpact.utils.ts` (Stage A/B, orchestrator), + `Servers/services/automations/actions/syncRegulationsTracker.ts` (the sync + impact hook), + `Servers/controllers/regulationsTracker.ctrl.ts` (endpoints + `isAdmin`/`canTrack`), + `Servers/utils/regulationsTracker.utils.ts` (module CRUD + `enrichWithFlags`). +- Frontend: `Clients/src/presentation/pages/RegulationsTracker/` (Browse, Tracked, Horizon, + Deadlines, Frameworks, Settings, CountryDetail, CountryRowCard, statusVariant). +- Docs: `shared/user-guide-content/content/regulations-tracker/*` + the two config files. +- Reference: `docs/technical/domains/regulations-tracker.md`. +- Progress ledger (this session's blow-by-blow): `.superpowers/sdd/progress.md`. diff --git a/docs/technical/domains/regulations-tracker.md b/docs/technical/domains/regulations-tracker.md new file mode 100644 index 0000000000..daa8ccc26f --- /dev/null +++ b/docs/technical/domains/regulations-tracker.md @@ -0,0 +1,255 @@ +# Regulations Tracker — technical reference + +> **Status:** Built on branch `feat/regulations-tracker` (not yet merged to `develop`). Last updated 2026-06-27. Impact analysis section added 2026-06-27. +> Mirrors the AI Trust Index module pattern. Two `/code-review` passes completed; PR pending. + +The Regulations Tracker is a standalone sidebar module that gives every organisation a window into +the public **Global AI Regulations** feed published by the marketing site (`verifywise.ai`). VerifyWise +pulls the feed on a daily schedule, detects when a country's regulations change (by content hash), +and notifies the organisations tracking that country — in-app and by email. It also surfaces three +global reference feeds (changelog, deadlines, international frameworks). No scraping, no LLM, no new +external service; VerifyWise never writes back to the website. + +--- + +## 1. Data source — the feed contract + +Base: `https://verifywise.ai/api/regulations`. Public, CORS-open, CDN-cached, `feedVersion: 1`. +Read-only. + +| Endpoint | Used for | +|---|---| +| `GET /api/regulations` | **Manifest** — per-country `{slug, name, region, regulationCount, hash, history, url}`. The daily diff trigger. | +| `GET /api/regulations/country/` | **Detail** — `{ feedVersion, meta, country }` where `country` nests `{regulations[], timeline[], oneLiner, executiveSummary, practicalTakeaway, flag, hash, history}`. **Detail is nested under `country`, NOT flat** (a recurring review trip-hazard). | +| `GET /api/regulations/horizon` | Curated dated changelog: `{ changes: [{date, countrySlug, countryName, countryFlag, type, description, detail}] }`. | +| `GET /api/regulations/deadlines` | `{ deadlines: [...], unscheduled: [...] }` — effective-date milestones. | +| `GET /api/regulations/snapshot` | Everything; we read `.frameworks` (11 international frameworks). | + +**Change detection** relies on the per-country `hash` (a key-sorted SHA-256 of the full country +record — changes iff content changes). Structured diff detail lives ONLY in +`history.lastChange.changes[]`; `history.hashHistory[]` entries carry `{date, hash, regulationCount}` +only (no per-step diff). So intermediate changes between our runs can be **counted** (and dated) but +their specifics aren't available — only the latest change's detail is. + +`regulation` field set: `name, type, status, effectiveDate, effectiveDateISO, dateConfidence, scope, +obligations[], maxPenalty, industryTags[], sourceUrl, lastVerified`. + +--- + +## 2. Data model (4 tables, mirrors AI Trust Index) + +Global reference data → global tables; tracking + settings → tenant tables. + +| Table | Scope | Purpose | +|---|---|---| +| `regulation_countries` | **global** | Catalog. `slug` PK, `data` JSONB (full detail `{...country, meta}`), `hash`, `region`, `name`, `regulation_count`, `is_active`, `removed_at`, `last_changed_at`, `last_fetched_at`. The `data->>'flag'` is surfaced in list queries. | +| `regulation_tracked_countries` | **tenant** (`organization_id`) | What each org tracks. `UNIQUE(organization_id, country_slug)`, **no FK** to the catalog (so feed re-imports can't cascade-delete tracking). | +| `regulation_tracker_settings` | **tenant** (`organization_id` PK) | `recipient_user_ids` JSONB, `recipient_emails` JSONB. | +| `regulation_tracker_meta` | **singleton** (`id=1 CHECK`) | `seeded_at`, `last_good_count`, `last_run_week`, `last_run_at`, `last_run_status`, and the three cached global-feed blobs `horizon`/`deadlines`/`frameworks` (JSONB). | + +Migrations (all on the branch): +`*-create-regulations-tracker-tables.js`, `*-seed-regulations-tracker-snapshot.js`, +`*-add-regulations-tracker-notification-enum-values.js` (adds `regulations_tracker` to +`enum_notification_type` and `regulation_country` to `enum_notification_entity_type`), +`*-add-regulations-tracker-global-feeds.js`, `*-add-regulations-tracker-run-status.js`. + +**Seed:** `database/seeds/regulations-tracker-snapshot.json` holds the **full** per-country detail +(60 countries) so a fresh install renders complete content day-one with no external call. The seed +migration is idempotent (skips if the catalog is non-empty) and baselines `meta` so the first sync +run notifies nobody. + +--- + +## 3. Backend layers + +``` +routes/regulationsTracker.route.ts 9 endpoints, all authenticateJWT +controllers/regulationsTracker.ctrl.ts thin controllers; isAdmin = inline arrow (role==="Admin"||"SuperAdmin") +utils/regulationsTracker.utils.ts CRUD, upsertFeedTx, recipient resolution, global-feed get/set, run-status, countChangesSince +utils/regulationsTrackerFeed.ts fetchManifest/validateManifest/fetchCountryDetail/fetchHorizon/fetchDeadlines/fetchSnapshot +services/automations/actions/syncRegulationsTracker.ts the daily sync job +templates/regulations-tracker-digest.mjml email digest +middleware/rateLimit.middleware.ts regulationsTrackerSyncLimiter (5 req / 5 min) +``` + +### Endpoints (`/api/regulations-tracker`) +| Method/path | Auth | | +|---|---|---| +| `GET /countries` | any | catalog list (incl. `flag`, `is_tracked` via org-scoped LEFT JOIN) | +| `GET /countries/:slug` | any | detail proxy: live feed → flatten `country`+`meta` to root; fall back to stored `data` with `stale:true` on fetch failure **or empty 200** | +| `GET /tracked` | any | org's tracked countries (`country_slug AS slug`, `flag`) | +| `POST /tracked` | Admin | track (`ON CONFLICT DO NOTHING`) | +| `POST /tracked/bulk` | Admin | track many (400 on empty array / >200 slugs) | +| `DELETE /tracked/:slug` | Admin | untrack (idempotent) | +| `GET /settings` | Admin | recipients + merged global `last_run_at`/`last_run_status` | +| `PUT /settings` | Admin | update recipients | +| `GET /horizon` `GET /deadlines` `GET /frameworks` | any | live-or-stored global feeds | +| `POST /sync` | Admin | rate-limited on-demand "check for updates now" | + +--- + +## 4. The update workflow (end to end) + +``` +WEBSITE: researcher edits a regulation → site recomputes the country hash + appends + history.lastChange.changes[]. The feed now serves the new hash + structured diff. + +DAILY JOB (BullMQ "regulations_tracker_sync", every day 06:00 UTC; or admin POST /sync) + 1. In-process syncInProgress guard (no concurrent runs). Then day-idempotency guard + (last_run_week column holds the day key === currentIsoDay, OUR clock) — admin /sync + clears the day key first. + 2. Fetch manifest. Validate: feedVersion===1, counts match, ≥20 VALID countries, ≥50% of + last_good_count (gated on VALID count). Any failure → recordRunStatus + return, no writes. + 3. For new/hash-changed countries, fetch full detail (normalized slug) and store {...country, meta}. + 4. upsertFeedTx (one txn, meta row FOR UPDATE): insert new / update changed (bump last_changed_at) / + soft-delete (is_active=false) any active slug not in upserted∪presentSlugs. Returns + {changed (with changeCount+changeDates from countChangesSince), newlyAdded, newlyRemoved, wasFirstSeed}. + Stores last_good_count = VALID count. + 5. Refresh horizon/deadlines/frameworks blobs (best-effort). + 6. wasFirstSeed → suppress ALL notifications, return. + 7. Notification phase (wrapped in try/catch → on failure recordRunStatus "error" + rethrow): + - changed/removed → only orgs TRACKING those slugs. Per affected country, ONE deep-linked in-app + notification per recipient (admins ∪ configured recipient_user_ids), message = change lines + (+ "changed N times since last check: dates" when changeCount>1; cosmetic hash move → "Updated"). + Email digest (MJML, escapeHtml on all feed strings) to configured recipients only — NO admin fallback. + - newlyAdded → notify EVERY org's admins (getAllOrgAdmins), deep-linked, since nobody tracks them yet. + 8. recordRunStatus("ok: N changed, M removed"); update last_run_week. +``` + +Notifications use the `notifications` table's `action_url` (deep link `/regulations-tracker/`) +and `entity_name`. `entity_id` is omitted (it's numeric; our key is a slug). + +--- + +## 5. Frontend + +`pages/RegulationsTracker/` — Browse, Tracked, Settings, CountryDetail, Horizon, Deadlines, +Frameworks + sidebar/context. Repository `regulationsTracker.repository.ts`, hooks +`useRegulationsTracker.ts` (KEY=`"regulations-tracker"`). Routes + AppSwitcher entry ("Regulations +tracker", Scale icon) + ContextSidebar case wired like AI Trust Index. + +- Browse/Tracked rows show the country **flag** emoji (globe fallback); tracked rows show a green + check; 8px row gap. Browse uses a Box/Stack card layout (same as AI Trust Index Browse — NOT a + shared table; this is intentional, a review false-positive otherwise). +- CountryDetail: Overview (oneLiner/executiveSummary/practicalTakeaway), full regulation cards + (type, effective date + confidence, scope, obligations, maxPenalty, industry tags, source via + `VWLink alwaysShowIcon`), timeline (newest-first), change history, verbatim feed disclaimer, + stale banner. +- Settings: recipient pickers + "Last checked" status + admin "Check for updates now" button. +- `VWLink` gained an `alwaysShowIcon` prop (persistent external-link arrow; default off). + +**i18n note:** inline `Label: {value}` JSX and `text={...}` button labels in this module are +English-only — the i18n extractor doesn't flag them, consistent with AI Trust Index. Page +titles/descriptions/empty-states ARE translated (de/fr/es). + +--- + +## 6. Key decisions & gotchas + +- **Feed detail is nested under `country`** — flatten to root in both the controller live path and + the sync's detail store. The controller test mock must use the nested shape. +- **Notification enum values require a migration** — `as unknown as` casts compile but the DB rejects + unknown enum values at runtime; the enum-values migration is mandatory. +- **last_good_count = valid count, not raw** — else a malformed-but-large feed inflates the watermark + and later rejects a valid smaller feed. +- **BullMQ:** `scheduleRegulationsTrackerSync` does NOT call `obliterate` and is registered AFTER the + obliterating schedulers in `addAllJobs` (next to `scheduleAiTrustIndexSync`). +- **Detail-page blank bug (fixed):** the catalog must store FULL detail, not the manifest summary, + or the page is blank when the live feed is slow/down. + +--- + +## 7. Impact analysis + +The module includes an optional **Regulation Impact Analysis** layer that, when enabled, automatically +detects which of an organization's AI systems, controls, policies, vendors, and assessments are +affected by a regulation change — and provides concise "why" reasoning for each. + +### Data model + +New table `regulation_impact_analysis`: +- `id` SERIAL PK +- `organization_id` FK (CASCADE) +- `country_slug` VARCHAR(120) +- `regulation_hash` VARCHAR(120) — the source regulation's hash; used to detect stale results +- `result` JSONB (nullable) — structured verdict: `{ systems: [{id, name, why}], controls: [...], policies: [...], vendors: [...], assessments: [...], generatedAt }` +- `status` VARCHAR(120) — `"ok"`, `"no_key"`, `"skipped_no_candidates"`, `"error"` +- `model` VARCHAR(255) — which LLM model executed the analysis (null if skipped/error) +- `created_at`, `refreshed_at` TIMESTAMPTZ +- **UNIQUE** on `(organization_id, country_slug)` — one analysis row per org+country pair + +New `regulation_tracker_settings` columns: +- `impact_enabled` BOOLEAN (default true) — org can toggle analysis on/off +- `last_impact_run_at` TIMESTAMPTZ (nullable) — when the last analysis ran for this org (populated across all countries) + +### Endpoints + +| Method/path | Auth | Limiter | +|---|---|---| +| `GET /countries/:slug/impact` | any | — | Returns `{result, status, refreshed_at, stale}` or null if no analysis exists. Computed `stale` flag = true if the cached `regulation_hash` differs from the live feed hash. **Route must be registered BEFORE `/countries/:slug`** to avoid Express greedy-match. Returns 200 with null body if no LLM key is configured for the org. | +| `POST /countries/:slug/impact/refresh` | Admin | `regulationsTrackerImpactLimiter` | Triggers on-demand analysis for a specific country. Returns the same shape. | + +### `/settings` additions + +`GET /settings` (Admin) returns: +- `impact_enabled` — org's toggle state +- `last_impact_run_at` — when analysis last ran (nullable; across all countries tracked by this org) +- `has_llm_key` (computed, read-only) — boolean; true if the org has at least one configured LLM key + +`PUT /settings` (Admin) accepts `impact_enabled` (boolean, optional); other fields (recipient lists) unchanged. + +### Analysis funnel (Stage A + Stage B) + +**Stage A — Deterministic candidate queries (over-inclusive, no LLM):** +- Region map: country name → numeric region code (Europe=2, North America=3, etc.) +- Framework inference: regulation type → framework (EU AI Act, ISO 42001, NIST AI RMF, etc.); EU-bloc countries imply EU AI Act +- Candidate queries per entity type: + - **Systems (projects)**: WHERE `geography = :region` OR linked frameworks match + - **Controls**: WHERE `framework_id` matches inferred frameworks + - **Policies**: WHERE `framework_id` matches AND linked to an affected control + - **Vendors**: WHERE linked to an affected system/policy + - **Assessments**: WHERE linked to an affected system/control + +Stage A result = `Candidate[]` per entity type (over-inclusive by design). + +**Stage B — LLM filter-and-annotate (specific verdicts only):** +- One call per entity type (5 parallel calls if all candidate lists non-empty) +- LLM receives: regulation identity + change diff, candidate entity list, key obligations + penalties +- **LLM contract:** responds with JSON `{results: [{type, id, affected, why}, ...]}` where: + - `affected` is boolean (true = impacted by this change) + - `why` is a concise reason string (1–2 sentences) + - LLM can ONLY filter candidates and annotate; cannot invent new entities +- `validateVerdicts()` enforces: only returned entities that were in the sent candidate list are accepted; any unknown entities are dropped +- Result `ImpactResult` = filtered + annotated systems/controls/policies/vendors/assessments + +### LLM-key gating + +- Orgs without a configured LLM key: `runImpactAnalysis()` returns `{status: "no_key", result: null}`. The endpoint returns 200/null; no panel rendered in the UI. +- When a change notification is sent for a keyless org, a one-line nudge is appended: *"Configure an LLM key to see how this regulation affects your organization."* +- Orgs with `impact_enabled = false`: analysis is skipped; no panel, no nudge (Settings shows the toggle state + "not run yet" if never run). + +### Sync hook integration + +Impact analysis runs **synchronously per-(org, country) during the notification phase** of the daily sync (or on-demand admin `/sync`). Isolated in a try/catch so a per-country analysis failure never breaks the overall sync. + +Result caching: analysis is cached by `(organizationId, country_slug, regulation_hash)`. If the regulation hash has not changed since the last run, the cached result is returned without re-invoking the LLM. + +Failure mode: if LLM call fails for a country, the sync continues and records impact status = `"error"`; the notification for that country includes no impact suffix. + +### V1 limitations + +- **Country→region mapping coarse:** the region lookup table contains ~30 country names. The LLM is expected to refine boundaries (e.g., "EU Digital Services Act" affects Austria, not just Germany). Standalone policies (not linked to a control) are unmatched. +- **Policies unlinked to controls:** Stage A only catches policies that are directly linked via `policy_frameworks` to a framework that matched. Orphaned policies are excluded. +- **Per-run cap, not a weekly budget:** `IMPACT_MAX_ANALYSES_PER_RUN` caps LLM analyses *per run* but resets each run. With the daily cadence, weeks where regulations change on multiple days can drive up to ~7× the LLM calls of the old weekly design. Impact only runs on real content changes and analysis is cached by `(org, country, regulation_hash)`, so the multiplier is bounded by how often regulations actually change. If spend needs a hard weekly ceiling, gate impact off `last_impact_run_at` rather than relying solely on the per-run cap. + +--- + +## 8. Not yet done / open + +- **PR not opened** (branch `feat/regulations-tracker`, ~33 commits). Awaiting go-ahead. +- Open review items left as acceptable: `getStoredHashes` duplicates upsertFeedTx's internal prefetch + (minor double query); `countChangesSince` returns count 1 when the new hash isn't yet in + hashHistory (benign under-count); notification volume is one-per-country-per-user (by design). +- No frontend component tests (module-wide; AI Trust Index has none either). +- Confirm with the web team that the feed `meta.disclaimer`/`scopeStatement` legal text is finalised + before GA (currently marked DRAFT on the website). diff --git a/shared/user-guide-content/content/index.ts b/shared/user-guide-content/content/index.ts index ca371b2895..69d280497b 100644 --- a/shared/user-guide-content/content/index.ts +++ b/shared/user-guide-content/content/index.ts @@ -111,6 +111,12 @@ import { aiTrustIndexDashboardContent } from './ai-trust-index/dashboard'; import { aiTrustIndexBrowseContent } from './ai-trust-index/browse'; import { aiTrustIndexTrackedContent } from './ai-trust-index/tracked'; import { aiTrustIndexSettingsContent } from './ai-trust-index/settings'; +import { regulationsTrackerBrowseContent } from './regulations-tracker/browse'; +import { regulationsTrackerTrackedContent } from './regulations-tracker/tracked'; +import { regulationsTrackerHorizonContent } from './regulations-tracker/horizon'; +import { regulationsTrackerDeadlinesContent } from './regulations-tracker/deadlines'; +import { regulationsTrackerFrameworksContent } from './regulations-tracker/frameworks'; +import { regulationsTrackerSettingsContent } from './regulations-tracker/settings'; // Map of article IDs to their content // Format: 'collectionId/articleId': ArticleContent @@ -242,6 +248,12 @@ export const articleContentMap: Record = { 'ai-trust-index/browse': aiTrustIndexBrowseContent, 'ai-trust-index/tracked': aiTrustIndexTrackedContent, 'ai-trust-index/settings': aiTrustIndexSettingsContent, + 'regulations-tracker/browse': regulationsTrackerBrowseContent, + 'regulations-tracker/tracked': regulationsTrackerTrackedContent, + 'regulations-tracker/horizon': regulationsTrackerHorizonContent, + 'regulations-tracker/deadlines': regulationsTrackerDeadlinesContent, + 'regulations-tracker/frameworks': regulationsTrackerFrameworksContent, + 'regulations-tracker/settings': regulationsTrackerSettingsContent, }; // Helper to get article content diff --git a/shared/user-guide-content/content/regulations-tracker/browse.ts b/shared/user-guide-content/content/regulations-tracker/browse.ts new file mode 100644 index 0000000000..5222d26cff --- /dev/null +++ b/shared/user-guide-content/content/regulations-tracker/browse.ts @@ -0,0 +1,97 @@ +import type { ArticleContent } from '../../contentTypes'; + +export const regulationsTrackerBrowseContent: ArticleContent = { + blocks: [ + { + type: 'heading', + id: 'overview', + level: 2, + text: 'Browsing the catalogue', + }, + { + type: 'paragraph', + text: 'The Browse tab lists every country and jurisdiction in the Regulations Tracker catalogue. Each row shows the country flag, its name, and its region. Use Browse to find the jurisdictions that matter to your organization and start tracking them, so you are notified when their AI regulations change.', + }, + { + type: 'paragraph', + text: 'The catalogue is paginated at 24 countries per page. Select any row to open that country and read its full regulation detail, timeline, and change history.', + }, + { + type: 'heading', + id: 'search-filter', + level: 2, + text: 'Searching and filtering', + }, + { + type: 'bullet-list', + items: [ + { + bold: 'Search', + text: 'Type a country or jurisdiction name in the search box. Results update shortly after you stop typing.', + }, + { + bold: 'Region filter', + text: 'Narrow the list to a single region. The region dropdown always shows every region, regardless of the current search.', + }, + ], + }, + { + type: 'heading', + id: 'tracking', + level: 2, + text: 'Tracking a country', + }, + { + type: 'paragraph', + text: 'Each row has a Track button. Select Track to follow that country; the button then reads Untrack, and the row shows a green check to confirm it is tracked. Tracked countries appear on the Tracked tab and trigger notifications when their regulations change.', + }, + { + type: 'callout', + variant: 'info', + text: 'Administrators and editors can track and untrack countries. Other roles, and super-admins viewing an organization, see the catalogue as read-only. The Settings tab is restricted to administrators.', + }, + { + type: 'heading', + id: 'bulk-track', + level: 2, + text: 'Tracking several countries at once', + }, + { + type: 'ordered-list', + items: [ + { + text: 'Select the checkbox on each untracked country you want to follow. Already-tracked countries cannot be selected.', + }, + { + text: 'Use the select-all checkbox above the list to select every untracked country on the current page.', + }, + { + text: 'Select Track selected to track them all in one action. The button shows how many countries are selected.', + }, + ], + }, + { + type: 'callout', + variant: 'tip', + text: 'Your selection clears when you change the search, region, or page, so bulk-track one page at a time.', + }, + { + type: 'article-links', + title: 'Next steps', + items: [ + { + collectionId: 'regulations-tracker', + articleId: 'tracked', + title: 'Managing tracked countries', + description: 'Review the countries you follow and remove ones you no longer need.', + }, + { + collectionId: 'regulations-tracker', + articleId: 'settings', + title: 'Settings and notifications', + description: 'Choose who gets notified and turn on impact analysis.', + }, + ], + }, + ], +}; diff --git a/shared/user-guide-content/content/regulations-tracker/deadlines.ts b/shared/user-guide-content/content/regulations-tracker/deadlines.ts new file mode 100644 index 0000000000..584926cf85 --- /dev/null +++ b/shared/user-guide-content/content/regulations-tracker/deadlines.ts @@ -0,0 +1,55 @@ +import type { ArticleContent } from '../../contentTypes'; + +export const regulationsTrackerDeadlinesContent: ArticleContent = { + blocks: [ + { + type: 'heading', + id: 'overview', + level: 2, + text: 'Effective-date deadlines', + }, + { + type: 'paragraph', + text: 'The Deadlines tab lists upcoming effective dates for AI regulations, soonest first, plus regulations whose effective date is not yet scheduled. Use it to plan ahead for the milestones that affect your organization.', + }, + { + type: 'heading', + id: 'next-12-months', + level: 2, + text: 'The next 12 months', + }, + { + type: 'paragraph', + text: 'At the top of the page, a row calendar lays out the coming year as twelve month columns, starting from the current month. Each upcoming deadline appears as a marker in the month it takes effect, so you can see at a glance where the busy periods fall. The nearest months are shaded to highlight what is coming soonest.', + }, + { + type: 'paragraph', + text: 'Hover a marker to see the regulation, date, and country. Select a marker to jump straight to that deadline in the list below.', + }, + { + type: 'heading', + id: 'scheduled', + level: 2, + text: 'Scheduled deadlines', + }, + { + type: 'paragraph', + text: 'The Scheduled list shows each upcoming milestone with its effective date, status, country flag and name, and the regulation name. When the source provides one, a View source link opens the official reference in a new tab.', + }, + { + type: 'heading', + id: 'not-scheduled', + level: 2, + text: 'Not yet scheduled', + }, + { + type: 'paragraph', + text: 'Regulations that have been announced but have no confirmed effective date appear under Not yet scheduled. These show the country, status, and regulation name, with the date marked as to be determined.', + }, + { + type: 'callout', + variant: 'info', + text: 'Deadlines are read-only and reflect the public Global AI Regulations feed. If the live feed is temporarily unavailable, the page shows the last known deadlines.', + }, + ], +}; diff --git a/shared/user-guide-content/content/regulations-tracker/frameworks.ts b/shared/user-guide-content/content/regulations-tracker/frameworks.ts new file mode 100644 index 0000000000..6668ea51ea --- /dev/null +++ b/shared/user-guide-content/content/regulations-tracker/frameworks.ts @@ -0,0 +1,66 @@ +import type { ArticleContent } from '../../contentTypes'; + +export const regulationsTrackerFrameworksContent: ArticleContent = { + blocks: [ + { + type: 'heading', + id: 'overview', + level: 2, + text: 'International frameworks', + }, + { + type: 'paragraph', + text: 'The Frameworks tab lists cross-border AI governance frameworks and principles, such as those from international and multilateral bodies. These complement national regulations rather than replacing them. Use this tab to understand the broader principles that shape AI governance worldwide.', + }, + { + type: 'callout', + variant: 'info', + title: 'Looking for the EU AI Act?', + text: 'Country and regional laws, including the EU AI Act, are not on this tab. They live under each country. Find them on the Browse tab.', + }, + { + type: 'heading', + id: 'card-detail', + level: 2, + text: 'What each framework shows', + }, + { + type: 'paragraph', + text: 'Frameworks are shown as cards. Each card includes, where available:', + }, + { + type: 'bullet-list', + items: [ + { + bold: 'Name and status', + text: 'The framework name and its current status.', + }, + { + bold: 'Adopted by', + text: 'The bodies or countries that have adopted it.', + }, + { + bold: 'Why it matters', + text: 'A short explanation of the framework’s purpose and relevance.', + }, + { + bold: 'Key principles', + text: 'The main principles the framework sets out.', + }, + { + bold: 'Named documents', + text: 'The specific documents or instruments associated with the framework.', + }, + { + bold: 'View source', + text: 'A link to the official reference, opening in a new tab.', + }, + ], + }, + { + type: 'callout', + variant: 'info', + text: 'Frameworks are read-only and reflect the public Global AI Regulations feed. If the live feed is temporarily unavailable, the page shows the last known frameworks.', + }, + ], +}; diff --git a/shared/user-guide-content/content/regulations-tracker/horizon.ts b/shared/user-guide-content/content/regulations-tracker/horizon.ts new file mode 100644 index 0000000000..777d11d059 --- /dev/null +++ b/shared/user-guide-content/content/regulations-tracker/horizon.ts @@ -0,0 +1,52 @@ +import type { ArticleContent } from '../../contentTypes'; + +export const regulationsTrackerHorizonContent: ArticleContent = { + blocks: [ + { + type: 'heading', + id: 'overview', + level: 2, + text: 'The horizon changelog', + }, + { + type: 'paragraph', + text: 'The Horizon tab is a dated changelog of AI-regulation changes across jurisdictions, with the most recent change first. Use it to see what has changed recently worldwide, not only in the countries you track.', + }, + { + type: 'heading', + id: 'entry-detail', + level: 2, + text: 'What each entry shows', + }, + { + type: 'bullet-list', + items: [ + { + bold: 'Country', + text: 'The flag and name of the jurisdiction the change applies to.', + }, + { + bold: 'Type', + text: 'A label describing the kind of change, when the source provides one.', + }, + { + bold: 'Date', + text: 'When the change occurred.', + }, + { + bold: 'Description', + text: 'A short summary of the change, with additional detail beneath it when available.', + }, + ], + }, + { + type: 'callout', + variant: 'info', + text: 'Horizon is read-only. It reflects the public Global AI Regulations feed and is not filtered to your tracked countries.', + }, + { + type: 'paragraph', + text: 'If the live feed is temporarily unavailable, the page shows the last known changelog and tells you the data may not be current.', + }, + ], +}; diff --git a/shared/user-guide-content/content/regulations-tracker/settings.ts b/shared/user-guide-content/content/regulations-tracker/settings.ts new file mode 100644 index 0000000000..aca5fc2cd1 --- /dev/null +++ b/shared/user-guide-content/content/regulations-tracker/settings.ts @@ -0,0 +1,133 @@ +import type { ArticleContent } from '../../contentTypes'; + +export const regulationsTrackerSettingsContent: ArticleContent = { + blocks: [ + { + type: 'heading', + id: 'overview', + level: 2, + text: 'Settings overview', + }, + { + type: 'paragraph', + text: 'The Settings tab controls who is notified when a tracked country’s regulations change, lets you check for updates on demand, and turns on impact analysis. These settings apply to your whole organization.', + }, + { + type: 'callout', + variant: 'warning', + text: 'Only administrators can change these settings. Other users see a notice that the settings are administrator-only.', + }, + { + type: 'heading', + id: 'recipients', + level: 2, + text: 'Choosing who gets notified', + }, + { + type: 'paragraph', + text: 'Set the people who receive a notification when a tracked country’s regulations change:', + }, + { + type: 'bullet-list', + items: [ + { + bold: 'Recipients', + text: 'Select organization users from the dropdown.', + }, + { + bold: 'Additional emails', + text: 'Type any extra email address and press Enter to add it.', + }, + ], + }, + { + type: 'paragraph', + text: 'Changes save automatically a moment after you make them. If no recipients are set, no email digest is sent.', + }, + { + type: 'heading', + id: 'check-now', + level: 2, + text: 'Checking for updates', + }, + { + type: 'paragraph', + text: 'The feed is checked automatically on a regular schedule, and recipients are notified only when a tracked country’s regulations change. You do not need to check manually. If you want to run a check immediately, select Check for updates now. A progress panel walks through retrieving the feed, validating it, and comparing it against your tracked countries, then shows what changed.', + }, + { + type: 'heading', + id: 'impact-analysis', + level: 2, + text: 'Impact analysis', + }, + { + type: 'paragraph', + text: 'Turn on Analyse how regulation changes affect my organisation to have VerifyWise check which of your AI systems, controls, policies, vendors, and assessments may be affected when a tracked country’s regulations change. You get a short summary in the change notification and on the country’s detail page.', + }, + { + type: 'info-box', + icon: 'Info', + title: 'How impact analysis works', + items: [ + 'The analysis runs during the scheduled check, using your organization’s configured LLM key, so each run uses LLM credits.', + 'It sends the updated regulation text and the names and descriptions of your possibly-relevant entities to your LLM provider.', + 'When the setting is off, you still receive regulation-change notifications, without the impact summary.', + ], + }, + { + type: 'heading', + id: 'llm-key', + level: 3, + text: 'Connecting an LLM key', + }, + { + type: 'paragraph', + text: 'Impact analysis needs an LLM key. The Settings tab shows your current status:', + }, + { + type: 'bullet-list', + items: [ + { + bold: 'No key configured', + text: 'You are prompted to add an LLM key, with a link to the API keys settings.', + }, + { + bold: 'Key configured', + text: 'The tab shows which provider and model the analysis will use, with a link to manage your keys.', + }, + ], + }, + { + type: 'paragraph', + text: 'The Last impact run line shows when impact analysis last ran, or notes that it has not run yet.', + }, + { + type: 'heading', + id: 'on-country-page', + level: 3, + text: 'Impact on the country page', + }, + { + type: 'paragraph', + text: 'When impact analysis has run for a tracked country, that country’s detail page shows a section titled How this change affects your organisation. It groups affected items into AI systems, controls, policies, vendors, and assessments, and gives a short reason for each. If the analysis is older than the latest change, a banner offers a Re-analyse action to run it again.', + }, + { + type: 'article-links', + title: 'Related', + items: [ + { + collectionId: 'regulations-tracker', + articleId: 'browse', + title: 'Browsing and tracking countries', + description: 'Find and track the jurisdictions relevant to your organization.', + }, + { + collectionId: 'regulations-tracker', + articleId: 'tracked', + title: 'Managing tracked countries', + description: 'Review and update the countries you follow.', + }, + ], + }, + ], +}; diff --git a/shared/user-guide-content/content/regulations-tracker/tracked.ts b/shared/user-guide-content/content/regulations-tracker/tracked.ts new file mode 100644 index 0000000000..7ff9769074 --- /dev/null +++ b/shared/user-guide-content/content/regulations-tracker/tracked.ts @@ -0,0 +1,85 @@ +import type { ArticleContent } from '../../contentTypes'; + +export const regulationsTrackerTrackedContent: ArticleContent = { + blocks: [ + { + type: 'heading', + id: 'overview', + level: 2, + text: 'Your tracked countries', + }, + { + type: 'paragraph', + text: 'The Tracked tab lists the countries and jurisdictions your organization is following. You are notified when any of these countries changes its AI regulations. Add countries to this list from the Browse tab.', + }, + { + type: 'heading', + id: 'row-detail', + level: 2, + text: 'What each row shows', + }, + { + type: 'paragraph', + text: 'Each row shows the country flag, name, and region, along with a summary line beneath the name:', + }, + { + type: 'bullet-list', + items: [ + { + bold: 'Regulations', + text: 'How many regulations are recorded for that country.', + }, + { + bold: 'Last changed', + text: 'When that country last had a regulation change, when this date is available.', + }, + { + bold: 'Tracked since', + text: 'When your organization started tracking the country.', + }, + ], + }, + { + type: 'paragraph', + text: 'Select any row to open the full country detail page.', + }, + { + type: 'heading', + id: 'organize', + level: 2, + text: 'Filtering and sorting', + }, + { + type: 'bullet-list', + items: [ + { + bold: 'Region filter', + text: 'Show only the tracked countries in a chosen region. Each region option includes a count.', + }, + { + bold: 'Sort', + text: 'Order the list by name (A to Z or Z to A) or by region. The list is sorted by name A to Z by default.', + }, + ], + }, + { + type: 'paragraph', + text: 'The list shows 12 countries per page by default, and you can switch to 24 or 48 per page.', + }, + { + type: 'heading', + id: 'untrack', + level: 2, + text: 'Removing a country', + }, + { + type: 'paragraph', + text: 'Select Untrack on any row to stop following that country. You will no longer receive notifications about its regulation changes. You can track it again at any time from the Browse tab.', + }, + { + type: 'callout', + variant: 'info', + text: 'If you are not tracking any countries yet, this tab is empty. Open the Browse tab to find and track countries.', + }, + ], +}; diff --git a/shared/user-guide-content/userGuideConfig.ts b/shared/user-guide-content/userGuideConfig.ts index 43b578ed5f..9177684e86 100644 --- a/shared/user-guide-content/userGuideConfig.ts +++ b/shared/user-guide-content/userGuideConfig.ts @@ -839,6 +839,51 @@ export const collections: Collection[] = [ }, ], }, + { + id: 'regulations-tracker', + title: 'Regulations Tracker', + description: 'Track AI regulations by country, watch upcoming deadlines and international frameworks, and see how regulation changes affect your organization.', + icon: 'FileText', + articleCount: 6, + articles: [ + { + id: 'browse', + title: 'Browsing and tracking countries', + description: 'Search the catalogue of countries and jurisdictions and track the ones relevant to your organization.', + keywords: ['regulations tracker', 'browse', 'countries', 'jurisdictions', 'track', 'untrack', 'bulk', 'search', 'region', 'catalogue'], + }, + { + id: 'tracked', + title: 'Managing tracked countries', + description: 'Review the countries you follow, with regulation counts and last-changed dates, and remove ones you no longer need.', + keywords: ['tracked', 'countries', 'regulations', 'last changed', 'tracked since', 'untrack', 'sort', 'region', 'notify'], + }, + { + id: 'horizon', + title: 'The horizon changelog', + description: 'A dated changelog of AI-regulation changes across jurisdictions, newest first.', + keywords: ['horizon', 'changelog', 'changes', 'updates', 'history', 'dated', 'newest', 'jurisdictions'], + }, + { + id: 'deadlines', + title: 'Effective-date deadlines', + description: 'Upcoming effective dates for AI regulations shown on a 12-month calendar and a scheduled list.', + keywords: ['deadlines', 'effective date', 'milestones', 'calendar', 'next 12 months', 'scheduled', 'unscheduled', 'source'], + }, + { + id: 'frameworks', + title: 'International frameworks', + description: 'Cross-border AI governance frameworks and principles that complement national regulations.', + keywords: ['frameworks', 'international', 'cross-border', 'principles', 'oecd', 'unesco', 'governance', 'eu ai act'], + }, + { + id: 'settings', + title: 'Settings, notifications, and impact analysis', + description: 'Choose who is notified of regulation changes, check for updates, and turn on impact analysis.', + keywords: ['settings', 'recipients', 'email', 'notify', 'check for updates', 'impact analysis', 'llm key', 'admin', 'digest'], + }, + ], + }, ]; // Fast Finds - Popular/quick access articles