// Typed client for the SAFE /ai/v1 surfaces (PR C2.5). // Never accepts or exposes raw features, thresholds, capabilities, signatures, keys or full IBANs. // The agentic demo is gated by a frontend flag AND authoritative backend gating. import { apiRequest } from './client' export const AGENTIC_DEMO_ENABLED = (import.meta.env.VITE_AGENTIC_DEMO_ENABLED as string | undefined) !== 'false' export interface ModelStatus { behavioural_model: string model_version: string feature_version: string reason_codes_version: string shadow_only: boolean affects_payment: boolean label: string } export interface RiskView { band: 'low' | 'uncertain' | 'elevated' | 'high' | 'model_unavailable' recommend_step_up: boolean reason_codes: string[] confidence: string model_version: string feature_version: string shadow: boolean } export interface AuthRecommendation { recommended_method: string meets_pdp_minimum: boolean } export interface AuditSummary { verified: boolean length: number protection_level: string } // Deliberately omits capability/signature/key/approval fields — the UI types cannot carry them. export interface OrchestrateResult { decision: 'allow' | 'deny' | 'step_up' reason_codes: string[] required_auth: string[] agent_state: string risk: RiskView | null auth_recommendation: AuthRecommendation | null rails: { ranked: string[]; eligible_only: boolean } | null explanation: { locale: string; messages: string[]; reason_codes: string[] } | null payment: { id: string; status: string } | null audit: AuditSummary | null fallback: string | null labels: Record } export interface ScenarioInput { payee_ref: string amount_minor: number known_payees?: string[] history_payments?: number recent_24h?: number consent?: boolean approve?: boolean locale?: string now?: number } // ---- C4: consent, profile deletion, federated status (advisory only, no payment authority) ---- // // Identity is the authenticated principal (the session token's subject) — the client never sends a // user_id. Every mutation carries a fresh request_id (idempotency) and the last-known record_version // (optimistic concurrency); a 409 means a newer decision exists, so the caller refetches. export type ConsentPurpose = 'service_essential' | 'optional_personalization' | 'optional_federation' export type PurposeKind = 'required_processing' | 'optional_consent' export type ConsentStatus = 'not_granted' | 'granted' | 'withdrawn' export interface PurposeMeta { purpose_kind: PurposeKind withdrawable: boolean version: number updated_at: number status?: ConsentStatus // optional_consent only state?: string // required_processing only ('active') } export interface ProfileStatus { subject: string policy_version: string record_version: number profile_generation?: number purposes: Record personalization_enabled: boolean federation_enrolled: boolean personalization_without_shared_learning: boolean affects_payment: boolean authoritative: boolean storage_consistency: { backend: string; durable_multi_replica: boolean; note: string } label: string } export interface DifferentialPrivacyDetail { differential_privacy: string budget_exhausted: boolean candidate_privacy_eligibility: string cumulative_epsilon?: number | null budget_epsilon?: number | null production_privacy_guarantee: boolean note: string } export interface FederatedStatus { client_type: string real_device_training: string native_local_only_training: string real_user_data_used: boolean federation: string secure_aggregation: string differential_privacy: string differential_privacy_detail: DifferentialPrivacyDetail automatic_model_promotion: boolean affects_payment_authorization: boolean candidate_status: string candidate_promotable: boolean disclaimer: string label: string aggregators?: string[] } export interface DeleteProfileResult { subject: string profile: string deleted_now: boolean deleted_categories: string[] retained_categories: string[] retention_reason: string retired_generation: number audit_reference: string erasure_type: string affects_payment: boolean note: string } function requestId(): string { const c = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto return c?.randomUUID ? c.randomUUID() : `req-${Date.now()}-${Math.random().toString(16).slice(2)}` } export function modelStatus(signal?: AbortSignal): Promise { return apiRequest('/ai/v1/models/status', { signal }) } export function profileStatus(signal?: AbortSignal): Promise { return apiRequest('/ai/v1/profile/status', { signal }) } export function setConsent(purpose: ConsentPurpose, grant: boolean, expectedVersion?: number, signal?: AbortSignal): Promise { return apiRequest('/ai/v1/consent', { body: { purpose, grant, request_id: requestId(), expected_version: expectedVersion }, signal, }) } export function deleteProfile(signal?: AbortSignal): Promise { return apiRequest('/ai/v1/profile', { method: 'DELETE', body: { request_id: requestId() }, signal }) } export function federatedStatus(signal?: AbortSignal): Promise { return apiRequest('/ai/v1/federated/status', { signal }) } export function demoPayee(iban: string, signal?: AbortSignal): Promise<{ payee_ref: string; display: string }> { return apiRequest('/ai/v1/demo/payee', { body: { iban }, signal }) } export function orchestrate(input: ScenarioInput, signal?: AbortSignal): Promise { return apiRequest('/ai/v1/demo/orchestrate', { body: input, signal }) }