File size: 5,866 Bytes
adb91eb 0f4d9ea adb91eb 0f4d9ea adb91eb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 | // 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<string, string>
}
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<ConsentPurpose, PurposeMeta>
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<ModelStatus> {
return apiRequest<ModelStatus>('/ai/v1/models/status', { signal })
}
export function profileStatus(signal?: AbortSignal): Promise<ProfileStatus> {
return apiRequest<ProfileStatus>('/ai/v1/profile/status', { signal })
}
export function setConsent(purpose: ConsentPurpose, grant: boolean, expectedVersion?: number, signal?: AbortSignal): Promise<ProfileStatus> {
return apiRequest<ProfileStatus>('/ai/v1/consent', {
body: { purpose, grant, request_id: requestId(), expected_version: expectedVersion }, signal,
})
}
export function deleteProfile(signal?: AbortSignal): Promise<DeleteProfileResult> {
return apiRequest<DeleteProfileResult>('/ai/v1/profile', { method: 'DELETE', body: { request_id: requestId() }, signal })
}
export function federatedStatus(signal?: AbortSignal): Promise<FederatedStatus> {
return apiRequest<FederatedStatus>('/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<OrchestrateResult> {
return apiRequest<OrchestrateResult>('/ai/v1/demo/orchestrate', { body: input, signal })
}
|