export type EvidenceLevel = 'provider_documentation' | 'schema_read' | 'practical_test'; export interface PlanRequest { task_id?: string; query?: string; apps?: string[]; read_only?: boolean; runtime?: 'any' | 'local' | 'remote'; granted_scopes?: string[]; evidence?: boolean; } export interface Evidence { url: string; last_confirmed: string; method: EvidenceLevel; } export interface Implementation { app: string; operation: string; scope_alternatives: string[] | null; evidence: Evidence; write: boolean; } export interface Plan { guide_urls?: string[]; dataset_version: string; catalog_version: string; rule_version: string; status: string; coverage: { documented: number; required: number; practically_tested: number; }; steps: { capability: string; status: string; implementations: Implementation[]; }[]; } export class TraceVeroError extends Error { code: string; status: number; requestId: string | null; constructor(code: string, status: number, requestId: string | null = null) { super(code); this.code = code; this.status = status; this.name = 'TraceVeroError'; this.requestId = requestId; } } export class TraceVero { private base: string; constructor(base = 'https://api.tracevero.com/v2') { this.base = base.replace(/\/$/, ''); } async request>( path: string, body?: unknown, signal?: AbortSignal, options: { method?: string; accessKey?: string } = {}, ): Promise { let response: Response; try { response = await fetch(this.base + path, { method: options.method ?? (body === undefined ? 'GET' : 'POST'), redirect: 'error', headers: { ...(body === undefined ? {} : { 'Content-Type': 'application/json' }), ...(options.accessKey ? { Authorization: 'Bearer ' + options.accessKey } : {}), }, body: body === undefined ? undefined : JSON.stringify(body), signal: signal ?? AbortSignal.timeout(20000), }); } catch { throw new TraceVeroError(signal?.aborted ? 'request_aborted' : 'network_error', 0); } const header = response.headers.get('x-request-id'); const requestId = header && /^[a-f0-9-]{36}$/i.test(header) ? header : null; let result; try { result = response.status === 204 ? null : await response.json(); } catch { throw new TraceVeroError(response.ok ? 'invalid_response' : 'http_error', response.status, requestId); } if (!response.ok) throw new TraceVeroError( typeof result?.error?.code === 'string' && /^[a-z_]{1,64}$/.test(result.error.code) ? result.error.code : 'http_error', response.status, requestId, ); return result as T; } planWorkflow(input: PlanRequest, signal?: AbortSignal) { return this.request('/workflows/plan', input, signal); } findCapabilities(query: string, offset = 0) { return this.request( '/capabilities?' + new URLSearchParams({ query, offset: String(offset) }), ); } getConnectionOptions(id: string) { return this.request( '/connections/' + id.split('/').map(encodeURIComponent).join('/'), ); } getVerification(id: string) { return this.request( '/verifications/' + id.split('/').map(encodeURIComponent).join('/'), ); } checkCompatibility(id: string, client: string, connection = 'auto') { return this.request('/compatibility/check', { id, client, connection }); } explainPermissions(input: PlanRequest) { return this.request('/permissions/explain', input); } validateConfig(config: Record | string, client: string) { return this.request('/config/validate', { config, client }); } getCapabilityGaps(app: string) { return this.request('/capability-gaps?' + new URLSearchParams({ app })); } getChangeImpact(snapshot: Record) { return this.request('/changes/impact', { snapshot }); } findReplacement(app: string, capabilities: string[] = []) { return this.request( '/replacements?' + new URLSearchParams({ app, capabilities: capabilities.join(',') }), ); } snapshot(input: PlanRequest) { return this.request('/workflows/snapshot', input); } createProject(plan: PlanRequest, intervalSeconds = 86400) { return this.request('/monitoring/projects', { plan, interval_seconds: intervalSeconds, }); } getProject(id: string, accessKey: string) { return this.request( '/monitoring/projects/' + encodeURIComponent(id), undefined, undefined, { accessKey }, ); } updateProject( id: string, accessKey: string, changes: { paused?: boolean; interval_seconds?: number; acknowledge_catalog?: boolean; }, ) { return this.request( '/monitoring/projects/' + encodeURIComponent(id), changes, undefined, { method: 'PATCH', accessKey }, ); } deleteProject(id: string, accessKey: string) { return this.request( '/monitoring/projects/' + encodeURIComponent(id), undefined, undefined, { method: 'DELETE', accessKey }, ); } submitMeasurement( id: string, accessKey: string, report: Record, ) { return this.request( '/monitoring/projects/' + encodeURIComponent(id) + '/measurements', report, undefined, { accessKey }, ); } exportProject(id: string, accessKey: string) { return this.request( '/monitoring/projects/' + encodeURIComponent(id) + '/export', undefined, undefined, { accessKey }, ); } configure(id: string, client: string, connection = 'auto') { return this.request('/configure', {id,client,connection}); } createRecovery(id: string, accessKey: string) { return this.request('/monitoring/projects/'+encodeURIComponent(id)+'/recovery', {}, undefined, {accessKey}); } recoverProject(id: string, recoveryKey: string) { return this.request('/monitoring/recover', {id,recovery_key:recoveryKey}); } setNotifications(id: string, accessKey: string, enabled: boolean) { return this.request('/monitoring/projects/'+encodeURIComponent(id)+'/notifications', {enabled}, undefined, {method:'PUT',accessKey}); } getNotifications(id: string, accessKey: string) { return this.request('/monitoring/projects/'+encodeURIComponent(id)+'/notifications', undefined, undefined, {accessKey}); } }