fix: RLS guard in match_incidents + try/catch around AI/embed calls
- Add new migration 20260711000013_match_incidents_auth_guard.sql that replaces match_incidents with an inline auth guard: callers without hse/admin role receive PGRST301 Forbidden, closing the SECURITY DEFINER RLS bypass. - Wrap anthropic.messages.create() in try/catch returning 503 in all four AI routes: quality-check, triage-suggest, rca-draft, similar. - Wrap JSON.parse(inc.embedding) and embedText() in similar/route.ts in a shared try/catch returning 503 Embedding service unavailable. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FFDuBhMKvoWjrWT3ZnGmmr
This commit is contained in:
@@ -41,7 +41,9 @@ export async function POST(
|
||||
const siteName = (incident.sites as unknown as { name: string } | null)?.name ?? 'Unknown'
|
||||
const zoneName = (incident.zones as unknown as { name: string } | null)?.name ?? 'Unknown'
|
||||
|
||||
const message = await anthropic.messages.create({
|
||||
let message: Awaited<ReturnType<typeof anthropic.messages.create>>
|
||||
try {
|
||||
message = await anthropic.messages.create({
|
||||
model: 'claude-opus-4-8',
|
||||
thinking: { type: 'adaptive' },
|
||||
max_tokens: 2048,
|
||||
@@ -94,6 +96,9 @@ Triage notes: ${inc.triage_notes ?? 'none'}
|
||||
Provide 3–5 Why steps drilling from immediate cause to root cause. Give a one-sentence root cause statement. Suggest 3 corrective/preventive actions appropriate for a Malaysian warehouse context.`,
|
||||
}],
|
||||
})
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'AI service unavailable' }, { status: 503 })
|
||||
}
|
||||
|
||||
const toolBlock = message.content.find(b => b.type === 'tool_use')
|
||||
if (!toolBlock || toolBlock.type !== 'tool_use')
|
||||
|
||||
@@ -32,7 +32,9 @@ export async function POST(
|
||||
medical_status: string | null
|
||||
}
|
||||
|
||||
const message = await anthropic.messages.create({
|
||||
let message: Awaited<ReturnType<typeof anthropic.messages.create>>
|
||||
try {
|
||||
message = await anthropic.messages.create({
|
||||
model: 'claude-opus-4-8',
|
||||
thinking: { type: 'adaptive' },
|
||||
max_tokens: 1024,
|
||||
@@ -69,6 +71,9 @@ Asset/equipment involved: ${inc.asset_involved ? 'yes' : 'no'}
|
||||
Suggest severity (1–5) and tick the appropriate NADOPOD 2004 flags. Give a one-sentence rationale.`,
|
||||
}],
|
||||
})
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'AI service unavailable' }, { status: 503 })
|
||||
}
|
||||
|
||||
const toolBlock = message.content.find(b => b.type === 'tool_use')
|
||||
if (!toolBlock || toolBlock.type !== 'tool_use')
|
||||
|
||||
@@ -27,6 +27,7 @@ export async function GET(
|
||||
const inc = incident as { id: string; description: string; embedding: string | null }
|
||||
|
||||
let embeddingVec: number[]
|
||||
try {
|
||||
if (inc.embedding) {
|
||||
embeddingVec = JSON.parse(inc.embedding) as number[]
|
||||
} else {
|
||||
@@ -35,6 +36,9 @@ export async function GET(
|
||||
embedding: `[${embeddingVec.join(',')}]` as unknown as string,
|
||||
}).eq('id', id)
|
||||
}
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'Embedding service unavailable' }, { status: 503 })
|
||||
}
|
||||
|
||||
const { data: similar } = await supabase.rpc('match_incidents', {
|
||||
query_embedding: `[${embeddingVec.join(',')}]`,
|
||||
|
||||
@@ -19,7 +19,9 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'description and incident_type required' }, { status: 422 })
|
||||
}
|
||||
|
||||
const message = await anthropic.messages.create({
|
||||
let message: Awaited<ReturnType<typeof anthropic.messages.create>>
|
||||
try {
|
||||
message = await anthropic.messages.create({
|
||||
model: 'claude-opus-4-8',
|
||||
thinking: { type: 'adaptive' },
|
||||
max_tokens: 1024,
|
||||
@@ -52,6 +54,9 @@ Description: ${body.description}
|
||||
Score 1–10 based on: specificity (location, time, persons involved), completeness (what happened + immediate actions), and clarity. Score 6 or above passes. If score is below 6, give up to 3 actionable suggestions.`,
|
||||
}],
|
||||
})
|
||||
} catch {
|
||||
return NextResponse.json({ error: 'AI service unavailable' }, { status: 503 })
|
||||
}
|
||||
|
||||
const toolBlock = message.content.find(b => b.type === 'tool_use')
|
||||
if (!toolBlock || toolBlock.type !== 'tool_use') {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
-- Add authorization guard to match_incidents to enforce DB-level access control.
|
||||
-- Without this, SECURITY DEFINER bypasses RLS for any direct caller.
|
||||
create or replace function match_incidents(
|
||||
query_embedding vector(1024),
|
||||
exclude_id uuid,
|
||||
match_count int default 5
|
||||
)
|
||||
returns table (
|
||||
id uuid,
|
||||
reference_no text,
|
||||
incident_type text,
|
||||
description text,
|
||||
severity int,
|
||||
similarity float
|
||||
)
|
||||
language plpgsql
|
||||
security definer
|
||||
as $$
|
||||
begin
|
||||
-- Enforce that only hse/admin roles can call this function directly
|
||||
if not exists (
|
||||
select 1 from public.users
|
||||
where id = auth.uid()
|
||||
and role in ('hse', 'admin')
|
||||
) then
|
||||
raise exception 'Forbidden' using errcode = 'PGRST301';
|
||||
end if;
|
||||
|
||||
return query
|
||||
select
|
||||
i.id,
|
||||
i.reference_no,
|
||||
i.incident_type,
|
||||
i.description,
|
||||
i.severity,
|
||||
1 - (i.embedding <=> query_embedding) as similarity
|
||||
from incidents i
|
||||
where i.id != exclude_id
|
||||
and i.embedding is not null
|
||||
order by i.embedding <=> query_embedding
|
||||
limit match_count;
|
||||
end;
|
||||
$$;
|
||||
Reference in New Issue
Block a user