diff --git a/src/lib/server/osoh-state.json b/src/lib/server/osoh-state.json new file mode 100644 index 0000000..6395c2b --- /dev/null +++ b/src/lib/server/osoh-state.json @@ -0,0 +1,26 @@ +{ + "monitoredSites": [ + { + "siteId": "sentinel-demo", + "siteSlug": "sentinel-demo", + "displayName": "Sentinel Demo", + "classification": "external_monitored_surface", + "status": "active_monitored_surface", + "createdAt": "2026-01-01T00:00:00.000Z", + "updatedAt": "2026-01-01T00:00:00.000Z" + } + ], + "monitoredSurfaceTokens": [ + { + "tokenId": "token-sentinel-demo", + "siteId": "sentinel-demo", + "tokenHash": "f17bed16f9e3d3f1d9f4f81b19bf5f7f62e8c31c6f6b2c1e6d9d2a1a5f6b4c3d", + "scope": "ingest", + "revoked": false, + "createdAt": "2026-01-01T00:00:00.000Z", + "updatedAt": "2026-01-01T00:00:00.000Z" + } + ], + "monitoringEvents": [], + "auditTrail": [] +} diff --git a/src/lib/server/osoh-store.ts b/src/lib/server/osoh-store.ts index 58b87e0..a3f68e3 100644 --- a/src/lib/server/osoh-store.ts +++ b/src/lib/server/osoh-store.ts @@ -1,3 +1,6 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; + export type IsoUtcTimestamp = string; export type OsohMonitoredSurfaceClass = 'external_monitored_surface'; @@ -18,7 +21,9 @@ export type OsohAuditAction = | 'site_updated' | 'site_disabled' | 'token_created' - | 'token_revoked'; + | 'token_revoked' + | 'ingest_auth_failed' + | 'ingest_event_accepted'; export interface OsohMonitoredSiteRecord { siteId: string; @@ -69,9 +74,21 @@ export interface OsohStoreState { auditTrail: OsohAuditTrailRecord[]; } +const statePath = resolve(process.cwd(), 'src/lib/server/osoh-state.json'); + export const createEmptyOsohStoreState = (): OsohStoreState => ({ monitoredSites: [], monitoredSurfaceTokens: [], monitoringEvents: [], auditTrail: [], }); + +export const loadOsohStoreState = async (): Promise => { + const raw = await readFile(statePath, 'utf8'); + const parsed = JSON.parse(raw) as OsohStoreState; + return parsed; +}; + +export const saveOsohStoreState = async (state: OsohStoreState): Promise => { + await writeFile(statePath, JSON.stringify(state, null, 2) + "`n", 'utf8'); +}; diff --git a/src/pages/api/v1/monitoring/ingest.ts b/src/pages/api/v1/monitoring/ingest.ts index 2d707db..b2711bc 100644 --- a/src/pages/api/v1/monitoring/ingest.ts +++ b/src/pages/api/v1/monitoring/ingest.ts @@ -8,7 +8,8 @@ import type { OsohStoreState, } from '../../../../../lib/server/osoh-store'; import { - createEmptyOsohStoreState, + loadOsohStoreState, + saveOsohStoreState, } from '../../../../../lib/server/osoh-store'; import { resolveIngestAuth, @@ -17,7 +18,7 @@ import { export const prerender = false; -type RouteStage = 'auth' | 'validation' | 'accepted'; +type RouteStage = 'auth' | 'validation' | 'storage' | 'accepted'; type RouteErrorReason = | 'missing_token' @@ -32,7 +33,8 @@ type RouteErrorReason = | 'missing_occurred_at' | 'invalid_occurred_at' | 'missing_payload_body' - | 'invalid_payload_body'; + | 'invalid_payload_body' + | 'state_write_failed'; const json = (body: unknown, status: number): Response => new Response(JSON.stringify(body), { @@ -45,43 +47,20 @@ const json = (body: unknown, status: number): Response => const nowIso = (): IsoUtcTimestamp => new Date().toISOString(); -const loadOsohStoreState = async (): Promise => { - return createEmptyOsohStoreState(); -}; - -const appendMonitoringEvent = async ( +const appendMonitoringEvent = ( state: OsohStoreState, event: OsohMonitoringEventRecord, -): Promise => { +): void => { state.monitoringEvents.push(event); }; -const appendAuditTrailRecord = async ( +const appendAuditTrailRecord = ( state: OsohStoreState, record: OsohAuditTrailRecord, -): Promise => { +): void => { state.auditTrail.push(record); }; -const auditAuthFailure = async ( - state: OsohStoreState, - reason: Extract< - RouteErrorReason, - 'missing_token' | 'unknown_token' | 'revoked_token' | 'inactive_site' | 'invalid_scope' - >, -): Promise => { - await appendAuditTrailRecord(state, { - auditId: randomUUID(), - siteId: 'unknown', - action: 'site_updated', - occurredAt: nowIso(), - details: { - category: 'ingest_auth_failure', - reason, - }, - }); -}; - export const POST: APIRoute = async ({ request }) => { const state = await loadOsohStoreState(); @@ -91,13 +70,35 @@ export const POST: APIRoute = async ({ request }) => { ); if (!authResult.ok) { - await auditAuthFailure(state, authResult.reason); + appendAuditTrailRecord(state, { + auditId: randomUUID(), + siteId: 'unknown', + action: 'ingest_auth_failed', + occurredAt: nowIso(), + details: { + category: 'ingest_auth_failure', + reason: authResult.reason, + }, + }); + + try { + await saveOsohStoreState(state); + } catch { + return json( + { + ok: false, + stage: 'storage', + reason: 'state_write_failed', + }, + 500, + ); + } return json( { ok: false, - stage: 'auth' satisfies RouteStage, - reason: authResult.reason satisfies RouteErrorReason, + stage: 'auth', + reason: authResult.reason, }, 401, ); @@ -111,8 +112,8 @@ export const POST: APIRoute = async ({ request }) => { return json( { ok: false, - stage: 'validation' satisfies RouteStage, - reason: 'invalid_json' satisfies RouteErrorReason, + stage: 'validation', + reason: 'invalid_json', }, 400, ); @@ -124,8 +125,8 @@ export const POST: APIRoute = async ({ request }) => { return json( { ok: false, - stage: 'validation' satisfies RouteStage, - reason: validationResult.reason satisfies RouteErrorReason, + stage: 'validation', + reason: validationResult.reason, }, 400, ); @@ -134,7 +135,7 @@ export const POST: APIRoute = async ({ request }) => { const receivedAt = nowIso(); const eventId = randomUUID(); - await appendMonitoringEvent(state, { + appendMonitoringEvent(state, { eventId, siteId: authResult.siteId, eventType: validationResult.normalized.eventType, @@ -143,10 +144,10 @@ export const POST: APIRoute = async ({ request }) => { payload: validationResult.normalized.payload, }); - await appendAuditTrailRecord(state, { + appendAuditTrailRecord(state, { auditId: randomUUID(), siteId: authResult.siteId, - action: 'site_updated', + action: 'ingest_event_accepted', occurredAt: receivedAt, details: { category: 'ingest_event_accepted', @@ -155,10 +156,23 @@ export const POST: APIRoute = async ({ request }) => { }, }); + try { + await saveOsohStoreState(state); + } catch { + return json( + { + ok: false, + stage: 'storage', + reason: 'state_write_failed', + }, + 500, + ); + } + return json( { ok: true, - stage: 'accepted' satisfies RouteStage, + stage: 'accepted', eventId, siteId: authResult.siteId, receivedAt,