30 lines
2.0 KiB
TypeScript
Executable File
30 lines
2.0 KiB
TypeScript
Executable File
import { randomUUID, createHash } from 'node:crypto';
|
|
import type { Pool } from 'pg';
|
|
import { z } from 'zod';
|
|
import { CUSTODY_WRITE_LOCK } from './postgres-schema.ts';
|
|
import { auditPostgres } from './postgres-custody.ts';
|
|
|
|
/** Operator-only acknowledgement AFTER externally reviewed repair/restore; never repairs or resets a journal. */
|
|
export async function resolveCustodyIncident(pool: Pool, evidenceId: string): Promise<{ resolutionId: string }> {
|
|
z.string().uuid().parse(evidenceId);
|
|
const c = await pool.connect();
|
|
try {
|
|
await c.query('BEGIN');
|
|
await c.query("SET LOCAL statement_timeout='10s'; SET LOCAL lock_timeout='3s'; SET LOCAL idle_in_transaction_session_timeout='10s'; SET LOCAL synchronous_commit=on");
|
|
await c.query('SELECT pg_advisory_xact_lock($1::bigint)', [CUSTODY_WRITE_LOCK]);
|
|
const allowed = await c.query("SELECT pg_has_role(current_user,nspowner,'MEMBER') AS owns FROM pg_namespace WHERE nspname='wk_custody'");
|
|
if (allowed.rows[0]?.owns !== true) throw new Error('Schema-owner approval required for integrity resolution');
|
|
const incident = await c.query("SELECT value FROM wk_custody.custody_meta WHERE key='integrity_incident_v1' FOR UPDATE");
|
|
if (!incident.rowCount) throw new Error('No unresolved custody integrity incident');
|
|
// Delete is UNCOMMITTED and exclusively locked. Any audit failure rolls it back, leaving the hold intact.
|
|
await c.query("DELETE FROM wk_custody.custody_meta WHERE key='integrity_incident_v1'");
|
|
await auditPostgres(c);
|
|
const resolutionId = randomUUID();
|
|
await c.query('INSERT INTO wk_custody.custody_meta VALUES($1,$2)', [`integrity_resolution:${resolutionId}`,
|
|
JSON.stringify({ version: 1, evidenceId, resolvedAt: Date.now(),
|
|
incidentDigest: createHash('sha256').update(incident.rows[0].value).digest('hex') })]);
|
|
await c.query('COMMIT'); return { resolutionId };
|
|
} catch (error) { await c.query('ROLLBACK').catch(() => {}); throw error; }
|
|
finally { c.release(); }
|
|
}
|