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 })
}