diff --git a/frontend/src/app/trust/page.tsx b/frontend/src/app/trust/page.tsx index ad7269059..2637f9a13 100644 --- a/frontend/src/app/trust/page.tsx +++ b/frontend/src/app/trust/page.tsx @@ -50,7 +50,7 @@ const STATUS_SUMMARY = [ }, { label: 'Preview', - detail: 'Capability is usable for evaluation in supported workspaces, but is not GA. GS1 EPCIS 2.0 export and ingest stay Preview until separately promoted.', + detail: 'Capability is usable for evaluation in supported workspaces, but has not reached general release. GS1 EPCIS 2.0 export and ingest stay Preview until separately promoted.', }, { label: 'Pilot', diff --git a/frontend/src/lib/__tests__/trust-subprocessors.test.ts b/frontend/src/lib/__tests__/trust-subprocessors.test.ts new file mode 100644 index 000000000..578194ac0 --- /dev/null +++ b/frontend/src/lib/__tests__/trust-subprocessors.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from 'vitest'; +import { + TRUST_SUBPROCESSORS, + type TrustSubprocessor, +} from '../trust-subprocessors'; + +const IN_PRODUCTION_NAMES = [ + 'Supabase', + 'Railway', + 'Vercel', + 'Sentry', + 'Stripe', + 'PostHog', + 'Resend', + 'TypeSafe AI', + 'Cloudflare R2', +] as const; + +const NOT_IN_PRODUCTION_NAMES = [ + 'OpenAI', + 'Google Cloud Vertex AI', + 'LlamaCloud', +] as const; + +describe('TRUST_SUBPROCESSORS', () => { + it('includes every required vendor exactly once', () => { + const names = TRUST_SUBPROCESSORS.map((sp) => sp.name); + for (const name of [...IN_PRODUCTION_NAMES, ...NOT_IN_PRODUCTION_NAMES]) { + expect(names.filter((n) => n === name)).toHaveLength(1); + } + }); + + it('marks OpenAI, Vertex, and LlamaCloud as not_in_production', () => { + for (const name of NOT_IN_PRODUCTION_NAMES) { + const row = TRUST_SUBPROCESSORS.find((sp) => sp.name === name); + expect(row?.availability).toBe('not_in_production'); + } + }); + + it('does not use where_configured for vendors that are not in production', () => { + for (const name of NOT_IN_PRODUCTION_NAMES) { + const row = TRUST_SUBPROCESSORS.find((sp) => sp.name === name); + expect(row?.availability).not.toBe('where_configured'); + } + }); + + it('uses the exact LlamaCloud availabilityLabel from diligence steering', () => { + const llama = TRUST_SUBPROCESSORS.find((sp) => sp.name === 'LlamaCloud'); + expect(llama?.availabilityLabel).toBe( + 'Planned: document extraction for admin bulk PDF uploads; not enabled in production as of Oct 28, 2026; enablement pending legal review and customer notice', + ); + }); + + it('uses the Legal not-in-production label for OpenAI and Vertex', () => { + for (const name of ['OpenAI', 'Google Cloud Vertex AI'] as const) { + const row = TRUST_SUBPROCESSORS.find((sp) => sp.name === name); + expect(row?.availabilityLabel).toBe( + 'Not used in production; may be enabled only after notice', + ); + } + }); + + it('exposes region and transferMechanism fields on every row', () => { + for (const sp of TRUST_SUBPROCESSORS) { + expect(sp).toHaveProperty('region'); + expect(sp).toHaveProperty('transferMechanism'); + expect(typeof sp.region).toBe('string'); + expect(typeof sp.transferMechanism).toBe('string'); + } + }); + + it('keeps availabilityLabel free of en/em dashes', () => { + for (const sp of TRUST_SUBPROCESSORS) { + expect(sp.availabilityLabel).not.toMatch(/[\u2013\u2014]/); + expect(sp.purpose).not.toMatch(/[\u2013\u2014]/); + expect(sp.dataProcessed).not.toMatch(/[\u2013\u2014]/); + expect(sp.location).not.toMatch(/[\u2013\u2014]/); + expect(sp.region).not.toMatch(/[\u2013\u2014]/); + expect(sp.transferMechanism).not.toMatch(/[\u2013\u2014]/); + } + }); + + it('uses Vercel UK Addendum wording from the Vercel DPA', () => { + const vercel = TRUST_SUBPROCESSORS.find((sp) => sp.name === 'Vercel'); + expect(vercel?.transferMechanism).toBe( + 'EU Standard Contractual Clauses with UK International Data Transfer Addendum', + ); + expect(vercel?.transferMechanism).not.toMatch(/Agreement/i); + }); + + it('requires non-empty required display fields', () => { + for (const sp of TRUST_SUBPROCESSORS as TrustSubprocessor[]) { + expect(sp.name.length).toBeGreaterThan(0); + expect(sp.purpose.length).toBeGreaterThan(0); + expect(sp.dataProcessed.length).toBeGreaterThan(0); + expect(sp.location.length).toBeGreaterThan(0); + expect(sp.link.startsWith('https://')).toBe(true); + expect(sp.availabilityLabel.length).toBeGreaterThan(0); + } + }); +}); diff --git a/frontend/src/lib/trust-subprocessors.ts b/frontend/src/lib/trust-subprocessors.ts index e00fcf90d..6c8db0a8a 100644 --- a/frontend/src/lib/trust-subprocessors.ts +++ b/frontend/src/lib/trust-subprocessors.ts @@ -1,30 +1,53 @@ -export type SubprocessorAvailability = 'default' | 'where_configured'; +export type SubprocessorAvailability = + | 'default' + | 'where_configured' + | 'not_in_production'; export interface TrustSubprocessor { name: string; purpose: string; dataProcessed: string; location: string; + /** Processing region stated on the vendor's official DPA or regions page. Empty when unstated. */ + region: string; + /** Cross-border transfer mechanism stated on the vendor's official DPA. Empty when unstated. */ + transferMechanism: string; link: string; availability: SubprocessorAvailability; availabilityLabel: string; } +/** + * Public Trust / DPA subprocessor inventory. + * In production use: Supabase, Railway, Vercel, Sentry, Stripe, PostHog, Resend, TypeSafe AI, Cloudflare R2. + * Not enabled in production as of Oct 28, 2026: OpenAI, Google Cloud Vertex AI, LlamaCloud. + * region and transferMechanism are filled only from each vendor's official DPA / regions pages. + */ export const TRUST_SUBPROCESSORS: TrustSubprocessor[] = [ { name: 'Supabase', - purpose: 'Authentication for developer-portal accounts and a legacy session-validation path; stores developer-portal API-key records and pre-sales assessment leads. Core tenant workspace data is stored in PostgreSQL managed by Railway.', - dataProcessed: 'Developer-portal account records, API-key records, pre-sales assessment leads, and authentication metadata.', + purpose: + 'Authentication for developer-portal accounts and a legacy session-validation path; stores developer-portal API-key records and pre-sales assessment leads. Core tenant workspace data is stored in PostgreSQL managed by Railway.', + dataProcessed: + 'Developer-portal account records, API-key records, pre-sales assessment leads, and authentication metadata.', location: 'United States; project region per executed agreement/current DPA', + region: 'Customer directed project region', + transferMechanism: + 'EU Standard Contractual Clauses (Module 2 and Module 3); UK International Data Transfer Addendum; Swiss Addendum', link: 'https://supabase.com/privacy', availability: 'default', availabilityLabel: 'Default platform processor', }, { name: 'Railway', - purpose: 'Platform hosting the RegEngine application and the core PostgreSQL database for tenant workspace data.', - dataProcessed: 'Workspace records, evidence ledger rows, API requests, application logs, and workflow metadata.', + purpose: + 'Platform hosting the RegEngine application and the core PostgreSQL database for tenant workspace data.', + dataProcessed: + 'Workspace records, evidence ledger rows, API requests, application logs, and workflow metadata.', location: 'United States', + region: 'United States', + transferMechanism: + 'EU U.S. Data Privacy Framework and/or EU Standard Contractual Clauses with UK International Data Transfer Addendum', link: 'https://railway.app/legal/privacy', availability: 'default', availabilityLabel: 'Default platform processor', @@ -34,15 +57,23 @@ export const TRUST_SUBPROCESSORS: TrustSubprocessor[] = [ purpose: 'Frontend hosting, edge delivery, and authenticated request routing.', dataProcessed: 'Authenticated web requests, CDN metadata, and limited edge logs.', location: 'United States / global CDN', + region: 'United States', + transferMechanism: + 'EU Standard Contractual Clauses with UK International Data Transfer Addendum', link: 'https://vercel.com/legal/privacy-policy', availability: 'default', availabilityLabel: 'Default platform processor', }, { name: 'Sentry', - purpose: 'Server, edge, and backend error monitoring with PII scrubbing configured where available.', - dataProcessed: 'Error events, stack traces, request metadata, and environment diagnostics. Browser replay capture is not enabled by default.', + purpose: + 'Server, edge, and backend error monitoring with PII scrubbing configured where available.', + dataProcessed: + 'Error events, stack traces, request metadata, and environment diagnostics. Browser replay capture is not enabled by default.', location: 'United States', + region: 'United States and other countries where Sentry or its subprocessors maintain operations', + transferMechanism: + 'EU U.S. Data Privacy Framework; EU Standard Contractual Clauses with UK International Data Transfer Addendum and Swiss adaptations', link: 'https://sentry.io/privacy/', availability: 'where_configured', availabilityLabel: 'Where configured', @@ -50,8 +81,12 @@ export const TRUST_SUBPROCESSORS: TrustSubprocessor[] = [ { name: 'Stripe', purpose: 'Billing, invoices, payment processing, and tax support.', - dataProcessed: 'Billing contact information, subscription metadata, payment processor tokens, and invoice history.', + dataProcessed: + 'Billing contact information, subscription metadata, payment processor tokens, and invoice history.', location: 'United States', + region: 'United States', + transferMechanism: + 'EU U.S. Data Privacy Framework; EEA Standard Contractual Clauses; UK International Data Transfer Addendum', link: 'https://stripe.com/privacy', availability: 'where_configured', availabilityLabel: 'Where billing is enabled', @@ -59,44 +94,39 @@ export const TRUST_SUBPROCESSORS: TrustSubprocessor[] = [ { name: 'PostHog', purpose: 'Privacy-conscious product analytics after consent or workspace enablement.', - dataProcessed: 'Workspace-scoped product events, pseudonymous identifiers, and consent metadata.', + dataProcessed: + 'Workspace-scoped product events, pseudonymous identifiers, and consent metadata.', location: 'United States', + region: 'Customer selected data center location', + transferMechanism: + 'EU U.S. Data Privacy Framework (including UK Extension and Swiss U.S.); EU Standard Contractual Clauses with UK International Data Transfer Addendum and Swiss adaptations', link: 'https://posthog.com/privacy', availability: 'where_configured', availabilityLabel: 'Where analytics is enabled', }, { name: 'Resend', - purpose: 'Transactional email, including invitations, alerts, passwordless sign-in messages, and Supabase Auth delivery (password reset, magic-link, and one-time-code emails) via SMTP where configured.', + purpose: + 'Transactional email, including invitations, alerts, passwordless sign-in messages, and Supabase Auth delivery (password reset, magic-link, and one-time-code emails) via SMTP where configured.', dataProcessed: 'Recipient email addresses, message metadata, and delivery events.', location: 'United States', + region: 'United States', + transferMechanism: + 'EU Standard Contractual Clauses (Module 2 and Module 3) with UK Addendum; EU U.S. Data Privacy Framework and UK Extension', link: 'https://resend.com/legal/privacy-policy', availability: 'where_configured', availabilityLabel: 'Where email is configured', }, - { - name: 'OpenAI', - purpose: 'AI-assisted document extraction, OCR enrichment, and classification workflows.', - dataProcessed: 'Uploaded document text, label images, and extracted field candidates submitted for enabled AI workflows.', - location: 'United States / multi-region', - link: 'https://openai.com/policies/privacy-policy', - availability: 'where_configured', - availabilityLabel: 'Where AI extraction is enabled', - }, - { - name: 'Google Cloud Vertex AI', - purpose: 'Gemini-assisted extraction and classification for configured AI workflows.', - dataProcessed: 'Uploaded document text, label images, and extracted field candidates submitted for enabled AI workflows.', - location: 'United States (us-central1)', - link: 'https://cloud.google.com/terms/cloud-privacy-notice', - availability: 'where_configured', - availabilityLabel: 'Where AI extraction is enabled', - }, { name: 'TypeSafe AI', - purpose: 'Calibrated classification of CSV column headers during CSV ingestion (Jev System One model): which canonical FSMA 204 field an unrecognised column holds, and, for a file that declares no critical tracking event, which one its rows record.', - dataProcessed: 'CSV column headers; when the uploader declares a single CTE for the file, that canonical CTE name (for example "shipping"); and, only where sample values are explicitly enabled, PII-redacted sample cell values for unrecognised columns. Never whole rows or files.', + purpose: + 'Calibrated classification of CSV column headers during CSV ingestion (Jev System One model): which canonical FSMA 204 field an unrecognised column holds, and, for a file that declares no critical tracking event, which one its rows record.', + dataProcessed: + 'CSV column headers; when the uploader declares a single CTE for the file, that canonical CTE name (for example "shipping"); and, only where sample values are explicitly enabled, PII-redacted sample cell values for unrecognised columns. Never whole rows or files.', location: 'United States', + region: '', + transferMechanism: + 'EU Standard Contractual Clauses (Module 2 and Module 3); UK International Data Transfer Addendum', link: 'https://typesafe.ai/legal/privacy', availability: 'where_configured', availabilityLabel: 'Where AI CSV suggestions are enabled', @@ -104,10 +134,58 @@ export const TRUST_SUBPROCESSORS: TrustSubprocessor[] = [ { name: 'Cloudflare R2', purpose: 'Object storage for uploaded evidence files and generated artifacts.', - dataProcessed: 'Raw uploads, normalized JSON, parsed text extracts, and generated artifacts.', + dataProcessed: + 'Raw uploads, normalized JSON, parsed text extracts, and generated artifacts.', location: 'United States / global edge', + region: '', + transferMechanism: + 'EU U.S. Data Privacy Framework (including UK Extension and Swiss U.S.); EU Standard Contractual Clauses (Module 2 and Module 3) with UK International Data Transfer Addendum', link: 'https://www.cloudflare.com/privacypolicy/', availability: 'where_configured', availabilityLabel: 'Where object storage is configured', }, + { + name: 'OpenAI', + purpose: + 'AI-assisted document extraction, OCR enrichment, and classification workflows.', + dataProcessed: + 'Uploaded document text, label images, and extracted field candidates submitted for enabled AI workflows.', + location: 'United States / multi-region', + region: '', + transferMechanism: + 'EU Standard Contractual Clauses for EEA and Swiss transfers; UK International Data Transfer Addendum for UK transfers', + link: 'https://openai.com/policies/privacy-policy', + availability: 'not_in_production', + availabilityLabel: 'Not used in production; may be enabled only after notice', + }, + { + name: 'Google Cloud Vertex AI', + purpose: 'Gemini-assisted extraction and classification for configured AI workflows.', + dataProcessed: + 'Uploaded document text, label images, and extracted field candidates submitted for enabled AI workflows.', + location: 'United States (us-central1)', + region: 'Customer selected Google Cloud region', + transferMechanism: + 'EU Standard Contractual Clauses or an Alternative Transfer Solution adopted by Google', + link: 'https://cloud.google.com/terms/cloud-privacy-notice', + availability: 'not_in_production', + availabilityLabel: 'Not used in production; may be enabled only after notice', + }, + { + name: 'LlamaCloud', + purpose: + 'Planned document extraction for admin bulk PDF uploads. Not enabled in production; enablement pending legal review and customer notice.', + dataProcessed: + 'Complete uploaded document file content sent for an extraction job, plus extraction outputs returned to RegEngine. RegEngine attempts to delete the vendor side file when the job ends and disables vendor result caching by default.', + location: + 'United States by default (LlamaCloud NA). EU Frankfurt only if an EU endpoint is configured.', + region: + 'United States (AWS us-east-1) by default; European Union Frankfurt (AWS eu-central-1) when using the EU endpoint', + transferMechanism: + 'EU Standard Contractual Clauses (Module 2 and Module 3) and UK International Data Transfer Addendum (self serve DPA incorporated via Terms of Service)', + link: 'https://www.llamaindex.ai/legal/terms-of-service', + availability: 'not_in_production', + availabilityLabel: + 'Planned: document extraction for admin bulk PDF uploads; not enabled in production as of Oct 28, 2026; enablement pending legal review and customer notice', + }, ]; diff --git a/frontend/tests/e2e/authenticated-core-journeys.spec.ts b/frontend/tests/e2e/authenticated-core-journeys.spec.ts index 898afec55..2cb914634 100644 --- a/frontend/tests/e2e/authenticated-core-journeys.spec.ts +++ b/frontend/tests/e2e/authenticated-core-journeys.spec.ts @@ -97,8 +97,19 @@ function removeInviteEmailOutboxRecords(email: string): void { fs.writeFileSync(outbox, remaining.length > 0 ? `${remaining.join('\n')}\n` : '', 'utf-8'); } -function expectAdminAuditLog(tenantId: string, eventType: string, resourceId: string): void { +// The admin service writes to this same SQLite file while the browser is on +// /settings/users. Without a busy timeout SQLite answers SQLITE_BUSY at once +// ("database is locked") instead of waiting for that write to commit. +const ADMIN_SQLITE_BUSY_TIMEOUT_MS = 5000; + +function openAdminDatabase(): DatabaseSync { const database = new DatabaseSync(adminSqlitePath()); + database.exec(`PRAGMA busy_timeout = ${ADMIN_SQLITE_BUSY_TIMEOUT_MS}`); + return database; +} + +function expectAdminAuditLog(tenantId: string, eventType: string, resourceId: string): void { + const database = openAdminDatabase(); try { const row = database .prepare('SELECT event_type, resource_type, resource_id FROM audit_logs WHERE tenant_id = ? AND event_type = ? AND resource_id = ? LIMIT 1') @@ -113,7 +124,7 @@ function cleanupInviteFixture(tenantId: string | null, email: string, userId?: s removeInviteEmailOutboxRecords(email); if (!tenantId || !isLocalE2E()) return; - const database = new DatabaseSync(adminSqlitePath()); + const database = openAdminDatabase(); try { if (userId) { database.prepare('DELETE FROM memberships WHERE tenant_id = ? AND user_id = ?').run(tenantId, userId); diff --git a/scripts/copy-lint/baseline.json b/scripts/copy-lint/baseline.json index a9272faea..6d091f71f 100644 --- a/scripts/copy-lint/baseline.json +++ b/scripts/copy-lint/baseline.json @@ -2,22 +2,12 @@ "frontend/src/app/auth/verify/page.tsx": { "empty-render": 2 }, - "frontend/src/app/blog/fda-food-traceability-requirements/page.tsx": { - "epcis-unlabeled": 1 - }, "frontend/src/app/blog/food-traceability-software/page.tsx": { - "epcis-unlabeled": 1, "sample-demo": 2 }, "frontend/src/app/blog/fsma-204-compliance-checklist-2026/page.tsx": { "invented-entity": 2 }, - "frontend/src/app/blog/fsma-204-compliance-guide/page.tsx": { - "epcis-unlabeled": 2 - }, - "frontend/src/app/blog/fsma-204-cte-kde-guide/page.tsx": { - "epcis-unlabeled": 5 - }, "frontend/src/app/contact/layout.tsx": { "sample-demo": 2 }, @@ -38,7 +28,6 @@ "empty-render": 1 }, "frontend/src/app/design-system/page.tsx": { - "invented-entity": 1, "sample-demo": 1, "sandbox": 1 }, @@ -58,9 +47,6 @@ "dash-placeholder": 3, "empty-render": 2 }, - "frontend/src/app/fsma-204/[cte]/page.tsx": { - "invented-entity": 1 - }, "frontend/src/app/identity/page.tsx": { "dash-placeholder": 2, "empty-render": 1 @@ -78,9 +64,6 @@ "frontend/src/app/legal/vulnerability-disclosure/page.tsx": { "sample-demo": 1 }, - "frontend/src/app/product/page.tsx": { - "sandbox": 2 - }, "frontend/src/app/records/page.tsx": { "dash-placeholder": 3, "empty-render": 1 @@ -100,9 +83,6 @@ "frontend/src/app/tools/tlc-validator/components/TLCValidatorClient.tsx": { "sample-demo": 1 }, - "frontend/src/app/why-regengine/page.tsx": { - "epcis-unlabeled": 1 - }, "frontend/src/components/dashboard/compliance-status-widget.tsx": { "empty-render": 1 }, @@ -133,9 +113,6 @@ "frontend/src/lib/dev-compliance-snapshots.ts": { "dash-placeholder": 2 }, - "frontend/src/lib/epcis-upload.ts": { - "epcis-unlabeled": 4 - }, "frontend/src/lib/mock-tenants.ts": { "sample-demo": 1 }, @@ -144,8 +121,5 @@ }, "frontend/src/lib/trust-subprocessors.ts": { "sample-demo": 1 - }, - "frontend/src/lib/utils.ts": { - "dash-placeholder": 1 } }