| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| import { captureSilentError } from '../../api/_sentry-edge.js'; |
|
|
| const CONVEX_INTERNAL_SEARCH_PATH = '/api/internal-intel-search'; |
| const CONVEX_INTERNAL_TIMELINE_PATH = '/api/internal-intel-timeline'; |
|
|
| |
| const CONVEX_TIMEOUT_MS = 5_000; |
|
|
| let _didWarnMissingConvexSiteUrl = false; |
| let _didWarnMissingConvexSharedSecret = false; |
|
|
| |
| |
| |
| |
| |
| function getConvexSiteUrl(): string { |
| const siteUrl = process.env.CONVEX_SITE_URL ?? ''; |
| if (!siteUrl && !_didWarnMissingConvexSiteUrl) { |
| _didWarnMissingConvexSiteUrl = true; |
| console.warn('[intel-history] CONVEX_SITE_URL not set; history reads disabled'); |
| } |
| return siteUrl; |
| } |
|
|
| function getConvexSharedSecret(): string { |
| const secret = process.env.CONVEX_SERVER_SHARED_SECRET ?? ''; |
| if (!secret && !_didWarnMissingConvexSharedSecret) { |
| _didWarnMissingConvexSharedSecret = true; |
| console.warn('[intel-history] CONVEX_SERVER_SHARED_SECRET not set; history reads disabled'); |
| } |
| return secret; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export function normalizeCountry(value: unknown): string { |
| return typeof value === 'string' ? value.trim().toUpperCase() : ''; |
| } |
|
|
| export function normalizeDomain(value: unknown): string { |
| return typeof value === 'string' ? value.trim().toLowerCase() : ''; |
| } |
|
|
| |
| |
| |
| |
| |
| export interface IntelHistoryScope { |
| domain?: string; |
| country?: string; |
| from?: number; |
| to?: number; |
| limit: number; |
| } |
|
|
| |
| interface WireRecord { |
| id?: unknown; |
| domain?: unknown; |
| resource?: unknown; |
| country?: unknown; |
| category?: unknown; |
| title?: unknown; |
| summary?: unknown; |
| sourceUrl?: unknown; |
| occurredAt?: unknown; |
| ingestedAt?: unknown; |
| _score?: unknown; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export interface IntelHistoryRecordView { |
| id: string; |
| domain: string; |
| resource: string; |
| country: string; |
| category: string; |
| title: string; |
| summary: string; |
| sourceUrl: string; |
| occurredAt: number; |
| ingestedAt: number; |
| score: number; |
| } |
|
|
| function str(value: unknown): string { |
| return typeof value === 'string' ? value : ''; |
| } |
|
|
| function num(value: unknown): number { |
| return typeof value === 'number' && Number.isFinite(value) ? value : 0; |
| } |
|
|
| |
| |
| |
| |
| |
| function toIntelHistoryRecord(raw: WireRecord): IntelHistoryRecordView { |
| return { |
| id: str(raw.id), |
| domain: str(raw.domain), |
| resource: str(raw.resource), |
| country: str(raw.country), |
| category: str(raw.category), |
| title: str(raw.title), |
| summary: str(raw.summary), |
| sourceUrl: str(raw.sourceUrl), |
| occurredAt: num(raw.occurredAt), |
| ingestedAt: num(raw.ingestedAt), |
| score: num(raw._score), |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| export function resolveLimit(requested: unknown, fallback: number, max: number): number { |
| const value = Number(requested); |
| return Number.isFinite(value) && value > 0 ? Math.min(Math.floor(value), max) : fallback; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| function scopeBody(scope: IntelHistoryScope): Record<string, unknown> { |
| const body: Record<string, unknown> = {}; |
| if (scope.domain) body.domain = scope.domain; |
| if (scope.country) body.country = scope.country; |
| if (typeof scope.from === 'number' && Number.isFinite(scope.from) && scope.from !== 0) { |
| body.from = scope.from; |
| } |
| if (typeof scope.to === 'number' && Number.isFinite(scope.to) && scope.to !== 0) { |
| body.to = scope.to; |
| } |
| body.limit = scope.limit; |
| return body; |
| } |
|
|
| |
| |
| |
| |
| |
| async function readIntelHistory( |
| path: string, |
| payload: Record<string, unknown>, |
| ): Promise<{ records: IntelHistoryRecordView[]; partial: boolean } | null> { |
| const siteUrl = getConvexSiteUrl(); |
| const sharedSecret = getConvexSharedSecret(); |
| if (!siteUrl || !sharedSecret) return null; |
|
|
| try { |
| const resp = await fetch(`${siteUrl}${path}`, { |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json', |
| 'User-Agent': 'worldmonitor-gateway/1.0', |
| 'x-convex-shared-secret': sharedSecret, |
| }, |
| body: JSON.stringify(payload), |
| signal: AbortSignal.timeout(CONVEX_TIMEOUT_MS), |
| }); |
| if (!resp.ok) { |
| console.warn(`[intel-history] ${path} returned HTTP ${resp.status}`); |
| return null; |
| } |
| const body = (await resp.json()) as { records?: unknown; partial?: unknown }; |
| if (!Array.isArray(body?.records)) { |
| console.warn(`[intel-history] ${path} returned no records array`); |
| return null; |
| } |
| return { |
| records: (body.records as WireRecord[]) |
| .filter((rec): rec is WireRecord => rec !== null && typeof rec === 'object') |
| .map(toIntelHistoryRecord), |
| partial: body.partial === true, |
| }; |
| } catch (err) { |
| |
| |
| const msg = err instanceof Error ? err.message : String(err); |
| console.warn(`[intel-history] ${path} failed: ${msg}`); |
| captureSilentError(err, { |
| tags: { surface: 'server', component: 'intel-history', stage: 'convex-read' }, |
| fingerprint: ['intel-history', 'convex-read-error', path], |
| }); |
| return null; |
| } |
| } |
|
|
| |
| export function intelHistorySearch( |
| params: IntelHistoryScope & { embedding: number[]; minScore?: number }, |
| ): Promise<{ records: IntelHistoryRecordView[]; partial: boolean } | null> { |
| const { embedding, minScore, ...scope } = params; |
| return readIntelHistory(CONVEX_INTERNAL_SEARCH_PATH, { |
| embedding, |
| ...scopeBody(scope), |
| ...(typeof minScore === 'number' ? { minScore } : {}), |
| }); |
| } |
|
|
| |
| |
| |
| |
| |
| export function intelHistoryTimeline( |
| scope: IntelHistoryScope, |
| ): Promise<{ records: IntelHistoryRecordView[]; partial: boolean } | null> { |
| return readIntelHistory(CONVEX_INTERNAL_TIMELINE_PATH, scopeBody(scope)); |
| } |
|
|