PR5b, the final code piece of the WhatsApp intake track. A daily cron (04:15) enforces the channel's retention table; the receipt itself stays 7-year WORM under BFL and is never touched. Retention actions (lib/retention.ts, each isolated and idempotent): - whatsapp_messages transcripts past 90 days: body_text + raw_payload cleared in id batches under a wall-clock budget; the row skeleton (wamid, direction, timestamps, status, inbox_item_id) survives for audit. Only rows still carrying content match. - Rows with phone_link_id IS NULL (unknown senders, orphans) past 30 days: deleted. - Link codes expired more than 24h ago: deleted, used or not. - Sender rate counters idle 2+ days: deleted (minute/day window keys are dead weight after that). - Links revoked 90+ days ago: phone_enc crypto-shredded to '' (column is NOT NULL), one-shot via neq guard; phone_hash and phone_masked kept for uniqueness history and audit display. Route mirrors the sweep cron exactly: withCronContext + registry gate (503 EXTENSION_DISABLED when the extension is off). vercel.json gets the schedule and both Docker crontabs are regenerated. Compliance: new whatsapp.receipt_intake activity in .compliance/ropa.yaml covering purpose, Art 6(1)(b)/(c)/(f) bases with the Art 14(5)(b) note for third-party attendee names, Meta Platforms Ireland as processor (Cloud API, EU SCC addendum, Local Storage region DE), the differentiated retention table, and security measures. Co-authored-by: Jakob Wennberg <jakob.wennberg@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
49 lines
2.1 KiB
TypeScript
49 lines
2.1 KiB
TypeScript
import { NextResponse } from 'next/server'
|
|
import { loadExtensions } from '@/lib/extensions/loader'
|
|
import { extensionRegistry } from '@/lib/extensions/registry'
|
|
import { withCronContext } from '@/lib/api/with-cron-context'
|
|
import { createServiceClientNoCookies } from '@/lib/auth/api-keys'
|
|
import { runRetention } from '@/extensions/general/whatsapp-inbox/lib/retention'
|
|
|
|
/**
|
|
* GET /api/extensions/whatsapp-inbox/retention/cron: daily GDPR purge for
|
|
* the WhatsApp channel. Enforces the retention table in .compliance/ropa.yaml
|
|
* (whatsapp.receipt_intake): 90-day chat transcripts, 30-day unknown-sender
|
|
* rows, expired link codes, dead rate counters, and the 90-day crypto-shred
|
|
* of revoked phone bindings. Receipts themselves are 7-year WORM under BFL
|
|
* and are never touched here. Scheduled in vercel.json (and the generated
|
|
* Docker crontabs).
|
|
*
|
|
* Overlap with a slow previous run is safe: every predicate only matches
|
|
* rows still carrying the data, so a second pass is a no-op.
|
|
*/
|
|
|
|
// The transcript purge loops over a possibly large first-run backlog.
|
|
export const maxDuration = 300
|
|
|
|
export const GET = withCronContext('cron.whatsapp_retention', async (_request, ctx) => {
|
|
// Load the registry so it reflects extensions.config.json.
|
|
loadExtensions()
|
|
|
|
// Physical routes under app/api/extensions/<id>/ compile into EVERY build,
|
|
// including the core-with-zero-extensions one: the registry (generated from
|
|
// extensions.config.json) is what actually switches an extension on. Mirror
|
|
// the ext/[...path] dispatcher: a disabled extension must not expose a live
|
|
// surface, and a scheduled-but-disabled cron must fail visibly (503)
|
|
// instead of quietly doing the work anyway.
|
|
if (!extensionRegistry.get('whatsapp-inbox')) {
|
|
ctx.log.warn('whatsapp-inbox extension is not enabled; cron refused')
|
|
return NextResponse.json(
|
|
{ error: 'WhatsApp inbox extension is not enabled', code: 'EXTENSION_DISABLED' },
|
|
{ status: 503 },
|
|
)
|
|
}
|
|
|
|
const supabase = createServiceClientNoCookies()
|
|
const summary = await runRetention(supabase)
|
|
|
|
ctx.log.info('whatsapp retention complete', { ...summary })
|
|
|
|
return NextResponse.json({ data: summary })
|
|
})
|