amanpay / web /src /api /ai.ts
MHamdan's picture
CI deploy d3d1cf1
0f4d9ea verified
Raw
History Blame Contribute Delete
5.87 kB
// 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 })
}