From dd920355b359213469fe3359884ce7a85175adbe Mon Sep 17 00:00:00 2001 From: Jakob Wennberg <149234542+jakobwennberg@users.noreply.github.com> Date: Tue, 21 Apr 2026 09:20:12 +0200 Subject: [PATCH] fix: use service-role client for processing_history writes (#289) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit appendProcessingHistory was accepting a caller-provided SupabaseClient, but processing_history has no INSERT RLS policy (by design — audit trail writes should not be user-controllable). Extension handlers pass the authenticated user client (ctx.supabase), which caused silent RLS violations caught by try-catch — the audit trail was wired up but empty. Switch to creating a service-role client internally, matching the existing event_log pattern (event-log-handler.ts uses createServiceClientNoCookies). Remove the supabase parameter from both appendProcessingHistory and appendProcessingHistoryBatch. Update all 5 call sites in invoice-inbox and inbox-smart-match. Co-authored-by: Claude Opus 4.6 (1M context) --- .../inbox-smart-match/lib/process-match.ts | 4 ++-- extensions/general/invoice-inbox/index.ts | 6 ++--- lib/processing-history/append.ts | 24 ++++++++++--------- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/extensions/general/inbox-smart-match/lib/process-match.ts b/extensions/general/inbox-smart-match/lib/process-match.ts index 99f2d71a..eee1975d 100644 --- a/extensions/general/inbox-smart-match/lib/process-match.ts +++ b/extensions/general/inbox-smart-match/lib/process-match.ts @@ -68,7 +68,7 @@ export async function processInboxItemMatch( // Append DeterministicMatch event — records that the narrowing ran let deterministicEventId: string try { - deterministicEventId = await appendProcessingHistory(ctx.supabase, { + deterministicEventId = await appendProcessingHistory({ companyId: ctx.companyId, correlationId, aggregateType: 'MatchProposal', @@ -121,7 +121,7 @@ export async function processInboxItemMatch( // Record the LLM attempt in processing_history try { - await appendProcessingHistory(ctx.supabase, { + await appendProcessingHistory({ companyId: ctx.companyId, correlationId, causationId: deterministicEventId, diff --git a/extensions/general/invoice-inbox/index.ts b/extensions/general/invoice-inbox/index.ts index 738af26b..6f6aa188 100644 --- a/extensions/general/invoice-inbox/index.ts +++ b/extensions/general/invoice-inbox/index.ts @@ -68,7 +68,7 @@ async function uploadAndClassify( // Audit: DocumentIngested try { - await appendProcessingHistory(supabase, { + await appendProcessingHistory({ companyId, correlationId, aggregateType: 'Document', @@ -102,7 +102,7 @@ async function uploadAndClassify( // Audit: DocumentExtractionAttempted (fires whether classification succeeded or failed) try { - await appendProcessingHistory(supabase, { + await appendProcessingHistory({ companyId, correlationId, aggregateType: 'Document', @@ -190,7 +190,7 @@ async function uploadAndClassify( // Audit: DocumentClassified (only when classification succeeded) if (!classificationError && classificationResult) { try { - await appendProcessingHistory(supabase, { + await appendProcessingHistory({ companyId, correlationId, aggregateType: 'Document', diff --git a/lib/processing-history/append.ts b/lib/processing-history/append.ts index 9d69c0c5..0700c134 100644 --- a/lib/processing-history/append.ts +++ b/lib/processing-history/append.ts @@ -1,9 +1,10 @@ /** * Processing history (behandlingshistorik) — append helper. * - * Appends events to the processing_history table within the caller's - * database transaction. Throws on failure so that the table writes - * and the audit trail are atomically consistent. + * Uses a service-role client internally (no INSERT RLS policy on + * processing_history — matching the event_log pattern). Company scoping + * is enforced by companyId in the event payload, not by RLS. + * Throws on failure. * * PII BOUNDARY: payload MUST contain pseudonymous IDs only (user UUIDs, * company UUIDs, counterparty IDs). Never names, emails, personnummer, @@ -13,11 +14,11 @@ * reference, which is the required behavior per v0.2 §10. */ -import type { SupabaseClient } from '@supabase/supabase-js' import type { ProcessingHistoryAggregateType, ProcessingHistoryActor, } from '@/types' +import { createServiceClient } from '@/lib/supabase/server' import { z } from 'zod' // ── PII validator ─────────────────────────────────────────────── @@ -90,16 +91,15 @@ export interface AppendEventInput { // ── Append functions ──────────────────────────────────────────── /** - * Append a single event to processing_history within the caller's transaction. + * Append a single event to processing_history. * - * Uses the provided SupabaseClient (which should be the same client used for - * table writes in the command handler). Throws on failure so that both the - * table writes and the audit trail roll back together. + * Uses a service-role client internally (bypasses RLS) since processing_history + * has no INSERT policy — matching the event_log pattern. Company scoping is + * enforced by the companyId in the event payload, not by RLS. * * Returns the generated event_id (pre-generated client-side for causation chaining). */ export async function appendProcessingHistory( - supabase: SupabaseClient, input: AppendEventInput ): Promise { // Validate payload + actor.label contain no PII @@ -107,6 +107,7 @@ export async function appendProcessingHistory( assertActorPiiSafe(input.actor) const eventId = crypto.randomUUID() + const supabase = createServiceClient() const { error } = await supabase .from('processing_history') @@ -135,13 +136,12 @@ export async function appendProcessingHistory( } /** - * Append multiple events atomically within the caller's transaction. + * Append multiple events atomically (single INSERT). * Used for batch operations (e.g., migration commits, multi-event command handlers). * * Returns array of generated event_ids in input order. */ export async function appendProcessingHistoryBatch( - supabase: SupabaseClient, inputs: AppendEventInput[] ): Promise { if (inputs.length === 0) return [] @@ -169,6 +169,8 @@ export async function appendProcessingHistoryBatch( occurred_at: input.occurredAt.toISOString(), })) + const supabase = createServiceClient() + const { error } = await supabase .from('processing_history') .insert(rows)