import { clientFetch } from '@/lib/api/client'; import { ApiError } from '@/lib/api/errors'; import { unwrap, type ApiEnvelope } from '@/lib/api/types'; import type { CredentialDetailsInput, DocumentConfirmedResult, IdentityKycInput, RunStepResult, TrustBadge, UploadUrlResult, VerificationApi, VerificationDocument, VerificationStatus, } from '../types'; const BASE = '/api/v1/nurse_verification'; const NURSES_BASE = '/api/v1/nurses'; /** * Computes the browser-side integrity hash the confirm endpoint records against the uploaded bytes * (SHA-256 hex). Runs in the browser only (Web Crypto); the mock skips it. */ async function sha256Hex(file: File): Promise { const buffer = await file.arrayBuffer(); const digest = await crypto.subtle.digest('SHA-256', buffer); return Array.from(new Uint8Array(digest)) .map((byte) => byte.toString(16).padStart(2, '0')) .join(''); } /** * PUTs the file bytes to the signed object-storage URL with upload **progress**. This is a direct PUT * to `IObjectStorage` (not our API), so it uses XHR — `fetch` can't report upload progress and the * signed URL needs no bearer. The bearer-carrying JSON calls still go through `clientFetch`. */ function putSignedUrl(uploadUrl: string, file: File, onProgress?: (percent: number) => void): Promise { return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.open('PUT', uploadUrl); xhr.setRequestHeader('Content-Type', file.type); xhr.upload.onprogress = (event) => { if (event.lengthComputable) onProgress?.(Math.round((event.loaded / event.total) * 100)); }; xhr.onload = () => xhr.status >= 200 && xhr.status < 300 ? resolve() : reject(new ApiError(xhr.status, 'Object storage upload failed', 'upload_failed')); xhr.onerror = () => reject(new ApiError(0, 'Network error during upload', 'network_error')); xhr.send(file); }); } /** * Real HTTP implementation of the VerificationApi seam (b6 contract). Routes are action-style + * snake_case; JSON bodies/fields are camelCase; step ids come from the route. Automated-step failures * come back as `200` with `stepStatus:"failed"` — surfaced, not thrown. Selected once * USE_VERIFICATION_MOCK is false. * * Gap: `submitCredentialDetails` has no nurse-facing b6 endpoint (admin enters the structured fields on * review) — it is filed in `for-backend.md` and no-ops here; the document uploads it accompanies ARE * contract-backed (upload_url → PUT → documents). The mock persists the details for the standalone demo. */ export const verificationClientApi: VerificationApi = { getStatus: async () => unwrap(await clientFetch>(BASE)), start: async () => unwrap(await clientFetch>(`${BASE}/submit`, { method: 'POST' })), runIdentityKyc: async ({ nationalId, livenessCaptured }: IdentityKycInput) => unwrap( await clientFetch>(`${BASE}/steps/identity_kyc/run`, { method: 'POST', body: JSON.stringify({ nationalId, livenessPayload: livenessCaptured ? 'captured' : null }), }), ), runShahkarMatch: async () => unwrap(await clientFetch>(`${BASE}/steps/shahkar_match/run`, { method: 'POST' })), runBankVerification: async () => unwrap( await clientFetch>(`${BASE}/steps/bank_account_verification/run`, { method: 'POST', }), ), uploadStepDocument: async (stepId, file, onProgress): Promise => { const { objectStorageKey, uploadUrl } = unwrap( await clientFetch>(`${BASE}/steps/${stepId}/upload_url`, { method: 'POST', body: JSON.stringify({ contentType: file.type, fileName: file.name }), }), ); await putSignedUrl(uploadUrl, file, onProgress); const integrityHash = await sha256Hex(file); const confirmed = unwrap( await clientFetch>(`${BASE}/steps/${stepId}/documents`, { method: 'POST', body: JSON.stringify({ objectStorageKey, integrityHash, contentType: file.type, fileSizeBytes: file.size, originalFileName: file.name, }), }), ); return { id: confirmed.documentId, contentType: file.type, fileSizeBytes: file.size, originalFileName: file.name, url: '', }; }, // No nurse-facing endpoint yet (see gap note above / for-backend.md); the accompanying document // uploads carry the real signal. Kept as a seam method so the mock can persist details unchanged. submitCredentialDetails: async (_input: CredentialDetailsInput) => {}, getTrustBadge: async (nurseId) => unwrap(await clientFetch>(`${NURSES_BASE}/${nurseId}/trust_badge`)), };