import { config } from './config.js'; export interface PlatformClientOptions { /** Bearer token for the upstream request */ token?: string; /** x-request-id to propagate */ requestId?: string; /** x-product-id to forward */ productId?: string; } export type RunKind = 'job' | 'agent'; export type RunStatus = 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled'; export type RunStepStatus = | 'pending' | 'running' | 'succeeded' | 'failed' | 'skipped' | 'cancelled'; export async function platformFetch( path: string, init: RequestInit, opts: PlatformClientOptions ): Promise { const url = `${config.PLATFORM_SERVICE_URL}${path}`; const headers: Record = { 'Content-Type': 'application/json', ...(opts.token ? { Authorization: `Bearer ${opts.token}` } : {}), ...(opts.requestId ? { 'x-request-id': opts.requestId } : {}), ...(opts.productId ? { 'x-product-id': opts.productId } : {}), }; const res = await fetch(url, { ...init, headers: { ...headers, ...(init.headers ?? {}) }, signal: AbortSignal.timeout(10_000), }); if (!res.ok) { const body = await res.text().catch(() => ''); throw new Error(`platform-service ${init.method ?? 'GET'} ${path} → ${res.status}: ${body}`); } return res.json() as Promise; } // ── Telemetry ───────────────────────────────────────────────────────────────── export interface TelemetryQueryResult { events: unknown[]; total: number; continuationToken?: string; } export async function telemetryQuery( params: { productId: string; eventType?: string; from?: string; to?: string; limit?: number; continuationToken?: string; }, opts: PlatformClientOptions ): Promise { const qs = new URLSearchParams(); qs.set('productId', params.productId); if (params.eventType) qs.set('eventType', params.eventType); if (params.from) qs.set('from', params.from); if (params.to) qs.set('to', params.to); qs.set( 'limit', String(Math.min(params.limit ?? config.QUERY_DEFAULT_LIMIT, config.QUERY_MAX_LIMIT)) ); if (params.continuationToken) qs.set('continuationToken', params.continuationToken); return platformFetch(`/api/telemetry/query?${qs}`, { method: 'GET' }, opts); } export interface TelemetryCluster { /** Cluster doc ID: ${fingerprint}:${yyyyMM} */ id: string; /** Partition key: ${productId}:${platform}:${module} */ pk: string; fingerprint: string; severity: 'warn' | 'error' | 'fatal'; totalCount: number; lastSeenAt: string; firstSeenAt: string; platform: string; module: string; eventName: string; sampleMessage?: string; status: 'open' | 'resolved' | 'ignored'; resolvedBy?: string; resolvedAt?: string; } export async function telemetryClusters( params: { from?: string; to?: string; platform?: string; module?: string }, opts: PlatformClientOptions ): Promise<{ clusters: TelemetryCluster[]; total: number }> { const qs = new URLSearchParams(); if (params.from) qs.set('from', params.from); if (params.to) qs.set('to', params.to); if (params.platform) qs.set('platform', params.platform); if (params.module) qs.set('module', params.module); return platformFetch<{ clusters: TelemetryCluster[]; total: number }>( `/api/telemetry/clusters?${qs}`, { method: 'GET' }, opts ); } export async function telemetryMetrics(opts: PlatformClientOptions): Promise { return platformFetch('/api/telemetry/metrics', { method: 'GET' }, opts); } export async function telemetryListPolicies(opts: PlatformClientOptions): Promise { return platformFetch('/api/telemetry/policies', { method: 'GET' }, opts); } export async function telemetryPreviewPolicy( body: { targeting?: Record }, opts: PlatformClientOptions ): Promise { return platformFetch( '/api/telemetry/policies/preview', { method: 'POST', body: JSON.stringify(body) }, opts ); } export async function telemetryCreatePolicy( body: Record, opts: PlatformClientOptions ): Promise { return platformFetch( '/api/telemetry/policies', { method: 'POST', body: JSON.stringify(body) }, opts ); } export async function telemetryUpdatePolicy( policyId: string, body: Record, opts: PlatformClientOptions ): Promise { return platformFetch( `/api/telemetry/policies/${encodeURIComponent(policyId)}`, { method: 'PUT', body: JSON.stringify(body) }, opts ); } export async function telemetryDeletePolicy( policyId: string, opts: PlatformClientOptions ): Promise<{ success: boolean }> { return platformFetch<{ success: boolean }>( `/api/telemetry/policies/${encodeURIComponent(policyId)}`, { method: 'DELETE' }, opts ); } export async function telemetryUpdateCluster( clusterId: string, pk: string, status: 'open' | 'resolved' | 'ignored', opts: PlatformClientOptions ): Promise { const qs = new URLSearchParams({ pk }); return platformFetch( `/api/telemetry/clusters/${encodeURIComponent(clusterId)}?${qs}`, { method: 'PATCH', body: JSON.stringify({ status }) }, opts ); } // ── Runs ────────────────────────────────────────────────────────────────────── export async function runsCreate( body: { id: string; kind: RunKind; name: string; source: string; triggeredBy?: string; parentRunId?: string; queueName?: string; queueJobId?: string; input?: Record; metadata?: Record; }, opts: PlatformClientOptions ): Promise { return platformFetch('/api/runs', { method: 'POST', body: JSON.stringify(body) }, opts); } export async function runsUpdate( runId: string, body: { status: RunStatus; output?: Record; error?: string; }, opts: PlatformClientOptions ): Promise { return platformFetch( `/api/runs/${encodeURIComponent(runId)}`, { method: 'PATCH', body: JSON.stringify(body) }, opts ); } export async function runStepsCreate( runId: string, body: { stepName: string; order: number; input?: Record; metadata?: Record; }, opts: PlatformClientOptions ): Promise { return platformFetch( `/api/runs/${encodeURIComponent(runId)}/steps`, { method: 'POST', body: JSON.stringify(body) }, opts ); } export async function runStepsUpdate( runId: string, stepName: string, body: { status: RunStepStatus; output?: Record; error?: string; }, opts: PlatformClientOptions ): Promise { return platformFetch( `/api/runs/${encodeURIComponent(runId)}/steps/${encodeURIComponent(stepName)}`, { method: 'PATCH', body: JSON.stringify(body) }, opts ); } // ── AI Budgets ─────────────────────────────────────────────────────────────── export async function aiBudgetsRecordSpend( body: { scopeType: 'product' | 'agent'; scopeId: string; policyId?: string; agentId?: string; agentVersionId?: string; runId?: string; evaluationRunId?: string; model?: string; tokensUsed?: number; costUsd: number; source?: string; }, opts: PlatformClientOptions ): Promise { return platformFetch( '/api/ai-budgets/spend', { method: 'POST', body: JSON.stringify(body) }, opts ); } // ── Support Cases ──────────────────────────────────────────────────────────── export async function supportCasesCreate( body: { orgId?: string; workspaceId?: string; requesterUserId?: string; assignedTo?: string; title: string; description?: string; priority?: 'critical' | 'high' | 'medium' | 'low'; source?: 'manual' | 'agent' | 'telemetry' | 'customer'; runId?: string; reviewId?: string; knowledgeBaseId?: string; tags?: string[]; }, opts: PlatformClientOptions ): Promise { return platformFetch('/api/support/cases', { method: 'POST', body: JSON.stringify(body) }, opts); } // ── Diagnostics ─────────────────────────────────────────────────────────────── export interface DebugSession { id: string; productId: string; status: string; collectionLevel: string; captureLogs: boolean; captureNetwork: boolean; captureScreenshots: boolean; maxDurationMinutes: number; logCount: number; traceCount: number; createdAt: string; expiresAt: string; targetUserId?: string; targetAnonymousId?: string; } export async function diagnosticsListSessions( params: { productId?: string; status?: string; limit?: number; offset?: number; }, opts: PlatformClientOptions ): Promise<{ sessions: DebugSession[]; total: number }> { const qs = new URLSearchParams(); if (params.productId) qs.set('productId', params.productId); if (params.status) qs.set('status', params.status); qs.set( 'limit', String(Math.min(params.limit ?? config.QUERY_DEFAULT_LIMIT, config.QUERY_MAX_LIMIT)) ); if (params.offset) qs.set('offset', String(params.offset)); return platformFetch<{ sessions: DebugSession[]; total: number }>( `/api/diagnostics/sessions?${qs}`, { method: 'GET' }, opts ); } // ── Reviews ────────────────────────────────────────────────────────────────── export async function reviewsCreate( body: { title: string; description: string; category: string; priority?: 'low' | 'normal' | 'high' | 'urgent'; scope?: 'org' | 'workspace'; orgId: string; workspaceId?: string; assignedTo?: string; runId?: string; source: string; actionType: string; metadata?: Record; dueAt?: string; }, opts: PlatformClientOptions ): Promise { return platformFetch('/api/reviews', { method: 'POST', body: JSON.stringify(body) }, opts); } export async function diagnosticsCreateSession( body: { productId: string; targetUserId?: string; targetAnonymousId?: string; collectionLevel?: 'standard' | 'debug' | 'trace'; captureLogs?: boolean; captureNetwork?: boolean; maxDurationMinutes?: number; }, opts: PlatformClientOptions ): Promise { return platformFetch( '/api/diagnostics/sessions', { method: 'POST', body: JSON.stringify(body) }, opts ); } export async function diagnosticsGetSession( sessionId: string, opts: PlatformClientOptions ): Promise { return platformFetch( `/api/diagnostics/sessions/${encodeURIComponent(sessionId)}`, { method: 'GET' }, opts ); } export async function diagnosticsUpdateSession( sessionId: string, body: { status?: string; collectionLevel?: string; maxDurationMinutes?: number }, opts: PlatformClientOptions ): Promise { return platformFetch( `/api/diagnostics/sessions/${encodeURIComponent(sessionId)}`, { method: 'PATCH', body: JSON.stringify(body) }, opts ); } export async function diagnosticsGetLogs( sessionId: string, params: { level?: string; from?: string; to?: string; limit?: number }, opts: PlatformClientOptions ): Promise<{ logs: unknown[]; continuationToken?: string }> { const qs = new URLSearchParams(); if (params.level) qs.set('level', params.level); if (params.from) qs.set('from', params.from); if (params.to) qs.set('to', params.to); qs.set( 'limit', String(Math.min(params.limit ?? config.QUERY_DEFAULT_LIMIT, config.QUERY_MAX_LIMIT)) ); return platformFetch<{ logs: unknown[]; continuationToken?: string }>( `/api/diagnostics/sessions/${encodeURIComponent(sessionId)}/logs?${qs}`, { method: 'GET' }, opts ); } export async function diagnosticsGetTraces( sessionId: string, params: { limit?: number; continuationToken?: string }, opts: PlatformClientOptions ): Promise<{ traces: unknown[]; continuationToken?: string }> { const qs = new URLSearchParams(); qs.set( 'limit', String(Math.min(params.limit ?? config.QUERY_DEFAULT_LIMIT, config.QUERY_MAX_LIMIT)) ); if (params.continuationToken) qs.set('continuationToken', params.continuationToken); return platformFetch<{ traces: unknown[]; continuationToken?: string }>( `/api/diagnostics/sessions/${encodeURIComponent(sessionId)}/traces?${qs}`, { method: 'GET' }, opts ); } // ── Tracker — items ─────────────────────────────────────────────────────────── export interface TrackerItemDoc { id: string; productId: string; type: string; status: string; priority: string; title: string; description?: string; labels?: string[]; assignee?: string | null; reportedBy?: string; source?: string; visibility?: string; voteCount: number; commentCount: number; targetRelease?: string | null; createdAt: string; updatedAt: string; } export function trackerItemsList( params: { productId?: string; type?: string; status?: string; priority?: string; q?: string; labels?: string; visibility?: string; sortBy?: string; sortOrder?: string; limit?: number; offset?: number; }, opts: PlatformClientOptions ): Promise<{ items: TrackerItemDoc[]; total: number; limit: number; offset: number }> { const qs = new URLSearchParams(); if (params.productId) qs.set('productId', params.productId); if (params.type) qs.set('type', params.type); if (params.status) qs.set('status', params.status); if (params.priority) qs.set('priority', params.priority); if (params.q) qs.set('q', params.q); if (params.labels) qs.set('labels', params.labels); if (params.visibility) qs.set('visibility', params.visibility); if (params.sortBy) qs.set('sortBy', params.sortBy); if (params.sortOrder) qs.set('sortOrder', params.sortOrder); qs.set( 'limit', String(Math.min(params.limit ?? config.QUERY_DEFAULT_LIMIT, config.QUERY_MAX_LIMIT)) ); if (params.offset !== undefined) qs.set('offset', String(params.offset)); return platformFetch<{ items: TrackerItemDoc[]; total: number; limit: number; offset: number }>( `/api/items?${qs}`, { method: 'GET' }, opts ); } export function trackerItemsStats(opts: PlatformClientOptions): Promise<{ total: number; byType: Record; byStatus: Record; byPriority: Record; }> { return platformFetch<{ total: number; byType: Record; byStatus: Record; byPriority: Record; }>('/api/items/stats', { method: 'GET' }, opts); } export function trackerItemsGet( itemId: string, opts: PlatformClientOptions ): Promise { return platformFetch( `/api/items/${encodeURIComponent(itemId)}`, { method: 'GET' }, opts ); } export function trackerItemsCreate( input: { productId?: string; type: string; priority: string; title: string; description?: string; labels?: string[]; assignee?: string; source?: string; visibility?: string; targetRelease?: string; }, opts: PlatformClientOptions ): Promise { return platformFetch( '/api/items', { method: 'POST', body: JSON.stringify(input) }, opts ); } export function trackerItemsUpdateStatus( itemId: string, status: string, opts: PlatformClientOptions ): Promise { return platformFetch( `/api/items/${encodeURIComponent(itemId)}/status`, { method: 'PATCH', body: JSON.stringify({ status }) }, opts ); } export function trackerItemsDelete( itemId: string, opts: PlatformClientOptions ): Promise<{ success: boolean }> { return platformFetch<{ success: boolean }>( `/api/items/${encodeURIComponent(itemId)}`, { method: 'DELETE' }, opts ); } // ── Tracker — votes ─────────────────────────────────────────────────────────── export function trackerVotesToggle( itemId: string, opts: PlatformClientOptions ): Promise<{ voted: boolean; voteCount: number }> { return platformFetch<{ voted: boolean; voteCount: number }>( `/api/items/${encodeURIComponent(itemId)}/vote`, { method: 'POST', body: '{}' }, opts ); } export function trackerVotesList( itemId: string, opts: PlatformClientOptions ): Promise<{ votes: unknown[]; count: number }> { return platformFetch<{ votes: unknown[]; count: number }>( `/api/items/${encodeURIComponent(itemId)}/votes`, { method: 'GET' }, opts ); } // ── Tracker — comments ──────────────────────────────────────────────────────── export function trackerCommentsList( itemId: string, opts: PlatformClientOptions ): Promise<{ comments: unknown[]; count: number }> { return platformFetch<{ comments: unknown[]; count: number }>( `/api/items/${encodeURIComponent(itemId)}/comments`, { method: 'GET' }, opts ); } export function trackerCommentsAdd( itemId: string, body: string, opts: PlatformClientOptions ): Promise { return platformFetch( `/api/items/${encodeURIComponent(itemId)}/comments`, { method: 'POST', body: JSON.stringify({ body }) }, opts ); } export function trackerCommentsDelete( itemId: string, commentId: string, opts: PlatformClientOptions ): Promise<{ success: boolean }> { return platformFetch<{ success: boolean }>( `/api/items/${encodeURIComponent(itemId)}/comments/${encodeURIComponent(commentId)}`, { method: 'DELETE' }, opts ); } // ── Tracker — public roadmap ────────────────────────────────────────────────── export function trackerPublicRoadmap( params: { productId?: string; type?: string; status?: string; q?: string; sortBy?: string; sortOrder?: string; limit?: number; offset?: number; }, opts: PlatformClientOptions ): Promise<{ items: TrackerItemDoc[]; total: number }> { const qs = new URLSearchParams(); if (params.productId) qs.set('productId', params.productId); if (params.type) qs.set('type', params.type); if (params.status) qs.set('status', params.status); if (params.q) qs.set('q', params.q); if (params.sortBy) qs.set('sortBy', params.sortBy); if (params.sortOrder) qs.set('sortOrder', params.sortOrder); qs.set( 'limit', String(Math.min(params.limit ?? config.QUERY_DEFAULT_LIMIT, config.QUERY_MAX_LIMIT)) ); if (params.offset !== undefined) qs.set('offset', String(params.offset)); return platformFetch<{ items: TrackerItemDoc[]; total: number }>( `/api/public/roadmap?${qs}`, { method: 'GET' }, opts ); } export function trackerPublicStats(opts: PlatformClientOptions): Promise<{ total: number; byStatus: Record; byType: Record; totalVotes: number; }> { return platformFetch<{ total: number; byStatus: Record; byType: Record; totalVotes: number; }>('/api/public/roadmap/stats', { method: 'GET' }, opts); } // ── Flags ───────────────────────────────────────────────────────────────────── export interface FeatureFlagDoc { id: string; productId: string; key: string; enabled: boolean; percentage: number; description?: string; platforms: string[]; regions?: string[]; createdAt: string; updatedAt: string; } export function flagsList(opts: PlatformClientOptions): Promise<{ flags: FeatureFlagDoc[] }> { return platformFetch<{ flags: FeatureFlagDoc[] }>('/api/flags', { method: 'GET' }, opts); } export function flagsGet(key: string, opts: PlatformClientOptions): Promise { return platformFetch( `/api/flags/${encodeURIComponent(key)}`, { method: 'GET' }, opts ); } export async function flagsUpsert( key: string, input: { enabled: boolean; percentage?: number; description?: string; platforms?: string[]; regions?: string[]; }, opts: PlatformClientOptions ): Promise { // Try update first; fall back to create if not found try { return await platformFetch( `/api/flags/${encodeURIComponent(key)}`, { method: 'PUT', body: JSON.stringify(input) }, opts ); } catch (err) { if (err instanceof Error && err.message.includes('404')) { return platformFetch( '/api/flags', { method: 'POST', body: JSON.stringify({ key, ...input }) }, opts ); } throw err; } } export function flagsDelete( key: string, opts: PlatformClientOptions ): Promise<{ success: boolean }> { return platformFetch<{ success: boolean }>( `/api/flags/${encodeURIComponent(key)}`, { method: 'DELETE' }, opts ); } export function flagsKillSwitch( input: { platform?: string; keys?: string[] }, opts: PlatformClientOptions ): Promise<{ disabled: string[]; count: number }> { return platformFetch<{ disabled: string[]; count: number }>( '/api/flags/kill', { method: 'POST', body: JSON.stringify(input) }, opts ); } // ── Jobs ────────────────────────────────────────────────────────────────────── export function jobsList(opts: PlatformClientOptions): Promise { return platformFetch('/api/jobs', { method: 'GET' }, opts); } export function jobsGet(id: string, opts: PlatformClientOptions): Promise { return platformFetch(`/api/jobs/${encodeURIComponent(id)}`, { method: 'GET' }, opts); } export function jobsTrigger(jobName: string, opts: PlatformClientOptions): Promise { return platformFetch( '/api/jobs/trigger', { method: 'POST', body: JSON.stringify({ jobName }) }, opts ); } export function jobsListRuns( name: string, limit: number, opts: PlatformClientOptions ): Promise { return platformFetch( `/api/jobs/${encodeURIComponent(name)}/runs?limit=${limit}`, { method: 'GET' }, opts ); } // ── Maintenance ─────────────────────────────────────────────────────────────── export function maintenanceGetCurrent(opts: PlatformClientOptions): Promise { return platformFetch('/api/settings/maintenance/full', { method: 'GET' }, opts); } export function maintenanceSet( input: { mode: 'none' | 'scheduled' | 'active' | 'emergency'; message?: string; affectedServices?: string[]; scheduledStart?: string; scheduledEnd?: string; }, opts: PlatformClientOptions ): Promise { return platformFetch( '/api/settings/maintenance', { method: 'PUT', body: JSON.stringify(input) }, opts ); } export function maintenanceScheduleWindow( input: { title: string; message: string; mode: string; scheduledStart: string; scheduledEnd: string; affectedServices?: string[]; }, opts: PlatformClientOptions ): Promise { return platformFetch( '/api/settings/maintenance/schedule', { method: 'POST', body: JSON.stringify(input) }, opts ); } export function maintenanceGetSchedule(opts: PlatformClientOptions): Promise { return platformFetch('/api/settings/maintenance/schedule', { method: 'GET' }, opts); } export function maintenanceDeleteWindow( windowId: string, opts: PlatformClientOptions ): Promise<{ success: boolean }> { return platformFetch<{ success: boolean }>( `/api/settings/maintenance/schedule/${encodeURIComponent(windowId)}`, { method: 'DELETE' }, opts ); } // ── Settings ────────────────────────────────────────────────────────────────────────────────── export function settingsGet(opts: PlatformClientOptions): Promise { return platformFetch('/api/settings', { method: 'GET' }, opts); } export function settingsUpdate( settings: Record, opts: PlatformClientOptions ): Promise { return platformFetch( '/api/settings', { method: 'PUT', body: JSON.stringify({ settings }) }, opts ); } export function settingsCheckKillSwitch( productId: string, opts: Pick ): Promise<{ enabled: boolean; disabled: boolean; message: string }> { return platformFetch<{ enabled: boolean; disabled: boolean; message: string }>( `/api/settings/kill-switch?productId=${encodeURIComponent(productId)}`, { method: 'GET' }, opts ); } // ── Webhooks ────────────────────────────────────────────────────────────────── export function webhooksListSubscriptions( productId: string, opts: PlatformClientOptions ): Promise { return platformFetch( `/api/webhooks/subscriptions?productId=${encodeURIComponent(productId)}`, { method: 'GET' }, opts ); } export function webhooksCreate( input: { productId: string; url: string; events: string[]; description?: string; enabled?: boolean; }, opts: PlatformClientOptions ): Promise { return platformFetch( '/api/webhooks/subscriptions', { method: 'POST', body: JSON.stringify(input) }, opts ); } export function webhooksGet( id: string, productId: string, opts: PlatformClientOptions ): Promise { return platformFetch( `/api/webhooks/subscriptions/${encodeURIComponent(id)}?productId=${encodeURIComponent(productId)}`, { method: 'GET' }, opts ); } export function webhooksUpdate( id: string, productId: string, input: { url?: string; events?: string[]; enabled?: boolean; description?: string }, opts: PlatformClientOptions ): Promise { return platformFetch( `/api/webhooks/subscriptions/${encodeURIComponent(id)}?productId=${encodeURIComponent(productId)}`, { method: 'PATCH', body: JSON.stringify(input) }, opts ); } export function webhooksDelete( id: string, productId: string, opts: PlatformClientOptions ): Promise<{ success: boolean }> { return platformFetch<{ success: boolean }>( `/api/webhooks/subscriptions/${encodeURIComponent(id)}?productId=${encodeURIComponent(productId)}`, { method: 'DELETE' }, opts ); } export function webhooksListDeliveries( id: string, limit: number, opts: PlatformClientOptions ): Promise { return platformFetch( `/api/webhooks/subscriptions/${encodeURIComponent(id)}/deliveries?limit=${limit}`, { method: 'GET' }, opts ); } export function webhooksTest( id: string, productId: string, opts: PlatformClientOptions ): Promise<{ success: boolean; message: string }> { return platformFetch<{ success: boolean; message: string }>( `/api/webhooks/subscriptions/${encodeURIComponent(id)}/test?productId=${encodeURIComponent(productId)}`, { method: 'POST', body: '{}' }, opts ); } export function webhooksRotateSecret( id: string, productId: string, opts: PlatformClientOptions ): Promise<{ secret: string; message: string }> { return platformFetch<{ secret: string; message: string }>( `/api/webhooks/subscriptions/${encodeURIComponent(id)}/rotate-secret?productId=${encodeURIComponent(productId)}`, { method: 'POST', body: '{}' }, opts ); }