{
  "slug": "audit-compliance",
  "spec_file": "docs/specs/2026-05-30-audit-compliance.md",
  "findings": [
    {
      "id": "audit-compliance-001",
      "severity": "P0",
      "type": "security",
      "summary": "POST /api/cron/data-retention-purge has no CRON_SECRET guard",
      "spec_ref": "Foundation Deltas — data-retention-purge cron",
      "code_ref": "apps/zync-api/src/routes/cron/data-retention-purge.ts:12",
      "evidence": "Handler calls deleteExpiredAuditLogs with no x-cron-secret check. Peer crons (e.g. audit-log-retention.ts:24-31) use timingSafeEqual on CRON_SECRET. Route is mounted at apps/zync-api/src/routes/index.ts:435 and dispatched from cron/runner.ts:22.",
      "repro": "curl -X POST https://<api>/api/cron/data-retention-purge — returns 200 without secret header.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "audit-compliance-002",
      "severity": "P0",
      "type": "security",
      "summary": "POST /api/cron/audit-partition-create has no CRON_SECRET guard",
      "spec_ref": "Foundation Deltas — audit-partition-create cron",
      "code_ref": "apps/zync-api/src/routes/cron/audit-partition-create.ts:13",
      "evidence": "Handler executes CREATE TABLE … PARTITION OF audit_log with no auth. Mounted at routes/index.ts:436; dispatched from cron/runner.ts:23.",
      "repro": "curl -X POST https://<api>/api/cron/audit-partition-create — unauthenticated DDL-capable endpoint.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "audit-compliance-003",
      "severity": "P1",
      "type": "broken",
      "summary": "data-retention-purge DELETE on audit_log conflicts with immutability triggers",
      "spec_ref": "Audit Log — Immutability (RAISE on UPDATE/DELETE)",
      "code_ref": "packages/db/src/queries/audit-compliance.ts:159",
      "evidence": "deleteOldAuditLogs runs db.delete(auditLog). Migration 0014_wave11_consolidated.sql:181-182 creates BEFORE DELETE trigger audit_log_no_delete raising 'audit_log is immutable: DELETE not permitted'. data-retention-purge.ts:15 calls this helper.",
      "repro": "Run data-retention-purge against DB with rows older than cutoff — Postgres raises audit_log is immutable: DELETE not permitted.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "audit-compliance-004",
      "severity": "P1",
      "type": "missing",
      "summary": "POST /api/tenant/export route absent",
      "spec_ref": "API Endpoints — POST /api/tenant/export",
      "code_ref": "apps/zync-api/src/routes/index.ts:138",
      "evidence": "grep apps/zync-api for /api/tenant/export or tenant-export returns no route file. createTenantExportJob exists at packages/db/src/queries/tenant-export-jobs.ts:32 but zero app imports.",
      "repro": "POST /api/tenant/export as tenant owner → 404.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "audit-compliance-005",
      "severity": "P1",
      "type": "missing",
      "summary": "GET /api/tenant/export/:exportId route absent",
      "spec_ref": "API Endpoints — GET /api/tenant/export/:exportId",
      "code_ref": "packages/db/src/queries/tenant-export-jobs.ts:49",
      "evidence": "getTenantExportJobById helper exists (tenant-scoped at :61) but no HTTP route calls it. grep getTenantExportJobById in apps/ returns no matches.",
      "repro": "GET /api/tenant/export/<uuid> → 404.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "audit-compliance-006",
      "severity": "P1",
      "type": "missing",
      "summary": "POST /api/admin/export route absent",
      "spec_ref": "GDPR — POST /api/admin/export (system admin)",
      "code_ref": "apps/zync-api/src/routes/data/export.ts:35",
      "evidence": "Only POST /api/data/export exists (data-export-gdpr spec) requiring settings:write and synchronous inline export. No requireAdminSession route accepting { tenantId } per audit-compliance plan Task 7.",
      "repro": "POST /api/admin/export { tenantId } as SYSTEM_ADMIN → 404.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "audit-compliance-007",
      "severity": "P1",
      "type": "missing",
      "summary": "POST /api/admin/users/:userId/erase GDPR erasure route absent",
      "spec_ref": "User Data Erasure — POST /api/admin/users/:userId/erase",
      "code_ref": "apps/zync-api/src/routes/admin/index.ts:1",
      "evidence": "grep user_erased|users/:userId/erase|admin-erase across repo (excluding docs) returns no handler. Only customer anonymize at apps/zync-api/src/routes/data/export.ts:120.",
      "repro": "POST /api/admin/users/<uuid>/erase → 404.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "audit-compliance-008",
      "severity": "P1",
      "type": "missing",
      "summary": "export.generate queue consumer for encrypted tenant ZIP pipeline not implemented",
      "spec_ref": "GDPR Tenant Export — export.generate consumer",
      "code_ref": "apps/zync-api/src/index.ts:137",
      "evidence": "export.generate handler only processes type uniform-format and report.schedule jobs. No branch reads { exportId, tenantId, requestedBy } or writes exports/{tenantId}/{exportId}.zip. wrangler.toml:166 lists export.generate as comment only — no [[queues.consumers]] for it.",
      "repro": "Enqueue tenant_export_jobs message — no consumer marks PROCESSING/DONE or writes encrypted ZIP to R2.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "audit-compliance-009",
      "severity": "P1",
      "type": "missing",
      "summary": "tickets and ticket_messages writes emit no audit_log rows",
      "spec_ref": "Audit Log Scope — tickets, ticket_messages",
      "code_ref": "packages/db/src/queries/support.ts:284",
      "evidence": "createTicket/updateTicket/softDeleteTicket/createTicketMessage use bare db.insert/update with zero auditLog or logAuditEvent. grep auditLog|logAuditEvent support.ts — no matches.",
      "repro": "Create or reply to ticket — no row in audit_log for entity_type ticket/ticket_message.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "audit-compliance-010",
      "severity": "P1",
      "type": "missing",
      "summary": "kb_articles create/publish/delete emit no audit_log rows",
      "spec_ref": "Audit Log Scope — kb_articles",
      "code_ref": "packages/db/src/queries/kb.ts:1",
      "evidence": "grep auditLog|tenantAuditLog|logAuditEvent packages/db/src/queries/kb.ts — no matches. kb-versions.ts:315 writes tenant_audit_log only on restore, not audit_log.",
      "repro": "Create or publish KB article — audit_log has no kb_article entity.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "audit-compliance-011",
      "severity": "P1",
      "type": "missing",
      "summary": "listAuditLogSystem helper never exposed via SYSTEM_ADMIN API",
      "spec_ref": "Permissions — View audit log (all tenants) SYSTEM_ADMIN",
      "code_ref": "packages/db/src/queries/audit-compliance.ts:90",
      "evidence": "listAuditLogSystem exported from packages/db/src/queries/index.ts:1200 but grep apps/ for listAuditLogSystem returns no route usage. Admin tenant audit at admin/tenant-audit.ts:28 serves billing/tier history via getTenantAuditLog, not compliance audit_log.",
      "repro": "SYSTEM_ADMIN has no API to list partitioned audit_log across tenants.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "audit-compliance-012",
      "severity": "P1",
      "type": "missing",
      "summary": "ADR documenting no-RLS decision absent",
      "spec_ref": "Row-Level Security Consideration; Plan Task 14",
      "code_ref": "docs/adr/",
      "evidence": "glob docs/adr/*audit* returns 0 files. Plan requires docs/adr/audit-compliance-no-rls.md.",
      "repro": "",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "audit-compliance-013",
      "severity": "P2",
      "type": "divergent",
      "summary": "Many write paths use async logAuditEvent queue instead of same-transaction audit_log insert",
      "spec_ref": "Write Pattern — audit in same DB transaction as business op",
      "code_ref": "packages/db/src/queries/audit.ts:57",
      "evidence": "logAuditEvent enqueues AUDIT_QUEUE fire-and-forget; consumer inserts tenant_audit_log (audit-log-consumer.ts:26). Example: apps/zync-api/src/routes/marketing/leads.ts:276 stage change calls logAuditEvent, not tx.insert(auditLog). Spec requires synchronous audit_log in business transaction.",
      "repro": "Mutate lead stage — row lands in tenant_audit_log asynchronously; audit_log may have no matching row.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "audit-compliance-014",
      "severity": "P2",
      "type": "divergent",
      "summary": "data-retention-purge only attempts audit_log deletion; omits sessions, export ZIPs, PII",
      "spec_ref": "Data Retention Policy; Cron data-retention-purge",
      "code_ref": "apps/zync-api/src/routes/cron/data-retention-purge.ts:12",
      "evidence": "Handler solely calls deleteExpiredAuditLogs(db, 2555). No session/token purge, no R2 export ZIP 7-day cleanup, no non-financial PII 3-year purge per spec table and plan Task 11.",
      "repro": "Run cron — expired sessions and export ZIPs in R2 remain.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "audit-compliance-015",
      "severity": "P2",
      "type": "divergent",
      "summary": "GET /api/audit missing actor filter",
      "spec_ref": "Audit Log View — Filters: tenant, actor, entity type, entity ID, date range",
      "code_ref": "apps/zync-api/src/routes/audit.ts:23",
      "evidence": "auditQuerySchema has entity_type, entity_id, from, to, limit, offset only. AuditListOptions (audit-compliance.ts:11) has no actorId. listAuditLogV2 buildWhereConditions filters tenant_id/entity/date only.",
      "repro": "GET /api/audit?actor_id=<uuid> — parameter ignored; no server-side actor filter.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "audit-compliance-016",
      "severity": "P2",
      "type": "divergent",
      "summary": "Tenant audit UI reads tenant_audit_log via /api/audit-log, not compliance /api/audit",
      "spec_ref": "Audit Log View — admin surfaces and CSV export",
      "code_ref": "apps/zync-app/src/modules/audit/useAuditLog.ts:48",
      "evidence": "useAuditLog fetches /api/audit-log and exports /api/audit-log/export. No app code calls /api/audit. Compliance partitioned audit_log API has no UI consumer.",
      "repro": "Open /settings/audit-log — network tab shows /api/audit-log, not /api/audit.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "audit-compliance-017",
      "severity": "P2",
      "type": "divergent",
      "summary": "createExportJob writes audit_log outside the export_jobs insert transaction",
      "spec_ref": "Write Pattern — same DB transaction",
      "code_ref": "packages/db/src/queries/data-export.ts:180",
      "evidence": "insert exportJobs then separate await db.insert(auditLog) at :191 — not wrapped in db.transaction with paired inserts.",
      "repro": "",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "audit-compliance-018",
      "severity": "P2",
      "type": "divergent",
      "summary": "Observability: unhandled errors log to console only, no Sentry/Logpush structured capture",
      "spec_ref": "Observability — Error Tracking",
      "code_ref": "apps/zync-api/src/index.ts:48",
      "evidence": "app.onError logs console.error only. No capture of { requestId, tenantId, path, error.message, error.stack } to Sentry DSN or Logpush. wrangler.toml has no Sentry binding.",
      "repro": "Throw in route — no external error-tracking event.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "audit-compliance-019",
      "severity": "P2",
      "type": "divergent",
      "summary": "Rate-limit credential-stuffing alert threshold not documented in worker config",
      "spec_ref": "Rate Limit Monitoring — >100×/hour per tenant alert",
      "code_ref": "apps/zync-api/wrangler.toml:88",
      "evidence": "RATE_LIMITER_AUTH binding exists but no runbook comment or config documenting >100 trips/hour/tenant alert or optional tenant auto-freeze.",
      "repro": "",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "audit-compliance-020",
      "severity": "P2",
      "type": "divergent",
      "summary": "Cloudflare cron triggers disabled in wrangler.toml",
      "spec_ref": "Foundation Deltas — data-retention-purge monthly; audit-partition-create monthly",
      "code_ref": "apps/zync-api/wrangler.toml:244",
      "evidence": "[triggers] crons block commented with note 'PENDING: CF account at 5-cron free-plan limit'. Partition-create and retention routes exist but scheduled() never fires in deployed worker.",
      "repro": "Deploy worker — no automatic partition creation or retention dispatch.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "audit-compliance-021",
      "severity": "P2",
      "type": "divergent",
      "summary": "GET /api/audit/export caps at 10k rows despite spec full filtered set",
      "spec_ref": "GET /api/audit/export — CSV export of audit log",
      "code_ref": "apps/zync-api/src/routes/audit.ts:123",
      "evidence": "listAuditLogV2 called with limit: 10000 hardcoded. Spec/plan Task 6: export full filtered set without pagination cap.",
      "repro": "Tenant with >10k matching audit rows — CSV truncated.",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "audit-compliance-022",
      "severity": "P2",
      "type": "divergent",
      "summary": "tenantQuery/systemQuery audit helpers not registered on query factories",
      "spec_ref": "Plan Task 2 — tenantQuery().audit.list / write",
      "code_ref": "packages/db/src/queries/tenant-query.ts:1",
      "evidence": "grep .audit. tenant-query.ts — no matches. Standalone listAuditLogV2/writeAuditLog exported from index.ts instead of factory pattern in plan.",
      "repro": "",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "audit-compliance-023",
      "severity": "P3",
      "type": "divergent",
      "summary": "writeAuditLog helper exported but never called from application code",
      "spec_ref": "Plan Task 2 — write helper for erasure/export routes",
      "code_ref": "packages/db/src/queries/audit-compliance.ts:126",
      "evidence": "grep writeAuditLog across repo (excluding index re-export) — only definition in audit-compliance.ts. Erasure/export routes that should use it are missing.",
      "repro": "",
      "fix_direction": "code_to_spec"
    },
    {
      "id": "audit-compliance-024",
      "severity": "P3",
      "type": "divergent",
      "summary": "data-retention-purge dispatched daily not monthly",
      "spec_ref": "Cron data-retention-purge — monthly",
      "code_ref": "apps/zync-api/src/cron/runner.ts:19",
      "evidence": "data-retention-purge included in daily '30 1 * * *' group, not a monthly cron expression. File comment says 'monthly on the 1st' but runner fires every day at 01:30 UTC.",
      "repro": "",
      "fix_direction": "code_to_spec"
    }
  ],
  "summary": {
    "P0": 2,
    "P1": 10,
    "P2": 10,
    "P3": 2,
    "total": 24
  }
}
