diff --git a/DECISIONS.md b/DECISIONS.md index 920e6b70..18805258 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1132,3 +1132,5 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-08-19] undo_bank_file_import (#1672) skips unbooked rows carrying payment_match_log history instead of weakening the audit_log_immutable delete guard: the log is append-only räkenskapsinformation (BFL 7 kap) per 20260323120000 ("Do NOT add cleanup/DELETE jobs") and the single-row DELETE route already refuses those rows (TRANSACTION_DELETE_HAS_AUDIT_TRAIL); the undo reports skipped_match_history so the user can ignore the stragglers. Rejected: a scoped trigger bypass like the GDPR account-delete RPC uses (erasure is a legal right overriding retention; an import undo is not). [2026-08-19] transactions.bank_file_import_id has NO retroactive backfill: attribution by (format, date window) can mislink rows to the wrong batch when a company has several same-format imports, and "undo this import" must never delete rows from a different one. Imports executed before 20260820071500 are simply not undoable through this action. [2026-08-19] Sidebar company switch (#1664): reinstated the existing CompanySwitcher at the top of the expanded desktop sidebar instead of turning the logo slot into the switcher (the issue offered both); the logo keeps its home link plus a native title tooltip, the user-menu flyout stays as the secondary path, and the collapsed rail relies on the UserMenu avatar since 64px has no room for a labeled switcher. +[2026-08-19] #1674 audit fixed the in-repo disclosures only (privacy sub-processor Bedrock row now names Anthropic as model vendor, replay paragraph states the deny-by-default guarantee, both locked by app/(public)/privacy/__tests__): Anthropic was deliberately NOT added as its own sub-processor row because no code path sends data to Anthropic (hosted uses Bedrock in eu-north-1, lib/ai/provider.ts); the artifact the prospect actually read (published DPA PDF or marketing security page listing Anthropic/OpenAI in the US) lives outside this repo and needs founder/legal action, as does any underbitraede-list wording. +[2026-08-19] #1674 audit fixed the in-repo disclosures only (privacy sub-processor Bedrock row now says AI requests go to Amazon Bedrock and the models are Anthropics Claude running inside Bedrock, replay paragraph states the deny-by-default guarantee, both locked by app/(public)/privacy/__tests__): Anthropic was not added as its own sub-processor row because the HOSTED posture is Bedrock by credential precedence (lib/ai/provider.ts: static AWS keys select Bedrock, region is AWS_REGION with eu-north-1 as the default, not a guarantee); a direct Anthropic API path DOES exist in code for self-hosted deployments (AI_PROVIDER=anthropic, or ANTHROPIC_API_KEY without static AWS keys), so "no code path sends data to Anthropic" would be false and the page asserts nothing about Anthropics underbitraede status either way; whether Anthropic is an underbitraede, plus aligning the published DPA PDF / marketing security page (which listed Anthropic/OpenAI in the US), is founder/legal action outside this repo. diff --git a/app/(public)/privacy/__tests__/ai-and-replay-disclosures.test.ts b/app/(public)/privacy/__tests__/ai-and-replay-disclosures.test.ts new file mode 100644 index 00000000..9d9bbd5f --- /dev/null +++ b/app/(public)/privacy/__tests__/ai-and-replay-disclosures.test.ts @@ -0,0 +1,146 @@ +import { describe, it, expect } from 'vitest' +import fs from 'node:fs' +import path from 'node:path' + +/** + * Guards the public privacy and DPA pages against drifting away from what the + * code actually does (#1674). A prospect compared our security claims with a + * stale published DPA that listed Anthropic and OpenAI as US processors; the + * in-repo pages were right, but nothing pinned them to the code. These tests + * anchor every AI and session-replay disclosure to the source of truth: + * + * - lib/ai/provider.ts: hosted inference is Claude on Amazon Bedrock by + * credential precedence, default region eu-north-1 (AWS_REGION overrides). + * A direct Anthropic API path exists for self-hosted deployments, and an + * OpenAI-compatible protocol client for operator-configured BYO endpoints + * (AI_BASE_URL); nothing defaults to or calls OpenAI's hosted API. + * - instrumentation-client.ts + lib/analytics/replay-masking.ts: session + * replay masking is deny-by-default with no input-mask exceptions. + * + * The pages are server components and this repo deliberately has no component + * test harness (CLAUDE.md: scope is lib/ + app/api/), so these assert on the + * page source, the same pattern as + * app/(auth)/register/__tests__/invite-email-prefill.test.ts. + */ +const ROOT = path.resolve(__dirname, '../../../..') + +function read(rel: string): string { + return fs.readFileSync(path.resolve(ROOT, rel), 'utf8') +} + +/** Source with comment lines dropped, so prose about a pattern is never mistaken for the pattern. */ +function code(src: string): string { + return src + .split('\n') + .filter((line) => !/^\s*(\*|\/\/|\/\*)/.test(line)) + .join('\n') +} + +const PRIVACY = read('app/(public)/privacy/page.tsx') +const DPA = read('app/(public)/dpa/page.tsx') +const PROVIDER = read('lib/ai/provider.ts') +const CLIENT = read('instrumentation-client.ts') +const PKG = JSON.parse(read('package.json')) as { + dependencies?: Record + devDependencies?: Record +} + +/** The AWS/Bedrock row of the sub-processor table. */ +function bedrockRow(): string { + const anchor = PRIVACY.indexOf('Amazon Web Services (AWS)') + expect(anchor, 'no AWS row in the sub-processor table').toBeGreaterThan(-1) + const start = PRIVACY.lastIndexOf('', anchor)) +} + +/** The PostHog row of the sub-processor table. */ +function posthogRow(): string { + const anchor = PRIVACY.indexOf('PostHog') + expect(anchor, 'no PostHog row in the sub-processor table').toBeGreaterThan(-1) + const start = PRIVACY.lastIndexOf('', anchor)) +} + +describe('AI provider disclosures match the code', () => { + it('has no code path that sends data to OpenAI the company', () => { + // The only openai-named dependency is the protocol client for + // operator-configured self-host endpoints (AI_BASE_URL); it must never + // default to OpenAI's hosted API. + const deps = { ...PKG.dependencies, ...PKG.devDependencies } + expect(Object.keys(deps).filter((name) => name.toLowerCase().includes('openai'))).toEqual([ + '@ai-sdk/openai-compatible', + ]) + expect(code(PROVIDER)).not.toContain('api.openai.com') + expect(code(read('lib/ai/services/openai-compatible.ts'))).not.toContain('api.openai.com') + // The openai-compatible provider is only reachable when the operator + // points AI_BASE_URL somewhere; without it the provider is unconfigured. + expect(code(PROVIDER)).toMatch(/openai-compatible'\) return !!process\.env\.AI_BASE_URL/) + }) + + it('never mentions OpenAI on the privacy or DPA page', () => { + // A published artifact once listed OpenAI as a processor; no code path + // calls OpenAI, so any mention on these pages is factually wrong. + expect(PRIVACY.toLowerCase()).not.toContain('openai') + expect(DPA.toLowerCase()).not.toContain('openai') + }) + + it('ships the Bedrock SDK the disclosure describes', () => { + expect(PKG.dependencies?.['@anthropic-ai/bedrock-sdk']).toBeTruthy() + }) + + it('discloses the exact region the provider defaults to', () => { + // lib/ai/provider.ts pins hosted inference to eu-north-1 unless AWS_REGION + // overrides it; the pages must claim that region, not a generic "EU". + expect(code(PROVIDER)).toContain("process.env.AWS_REGION || 'eu-north-1'") + expect(bedrockRow()).toContain('eu-north-1') + expect(bedrockRow()).toContain('Stockholm') + expect(DPA).toContain('eu-north-1') + }) + + it('describes the Bedrock row with claims provable from code, no sub-processor verdict', () => { + // The prospect read a bare AWS row next to a DPA listing Anthropic as a US + // processor. The row must say what the code shows: AI requests go to + // Amazon Bedrock, and the models are Anthropic's Claude running inside + // Bedrock. Whether Anthropic is an underbiträde of AWS is a contractual + // question between AWS and Anthropic that this repo cannot verify, so the + // page must not assert it either way; that wording is founder/legal's. + const row = bedrockRow() + const normalized = row.replace(/\s+/g, ' ') + expect(normalized).toContain('AI-anropen skickas till Amazon Bedrock') + expect(normalized).toContain( + 'modellerna som används är Anthropics Claude-modeller, körda inom Bedrock', + ) + expect(row).not.toContain('underbiträde') + }) + + it('keeps the DPA pointing at the privacy policy as the single sub-processor list', () => { + expect(DPA).toContain('href="/privacy"') + // The DPA must not grow its own (divergent) vendor list: /privacy owns the + // sub-processor table, and its Bedrock row is where the Claude models are + // described. + expect(DPA).not.toContain('Anthropic') + }) +}) + +describe('session replay disclosure matches the masking config', () => { + it('is actually deny-by-default in the PostHog init', () => { + const client = code(CLIENT) + expect(client).toContain('maskAllInputs: true') + expect(client).toContain("maskTextSelector: '*'") + expect(client).toContain('maskTextFn: replayMaskText') + // No maskInputFn: rrweb masks every input value with no exceptions. If one + // is ever added, the "utan undantag" wording below becomes a lie. + expect(client).not.toContain('maskInputFn') + }) + + it('states the deny-by-default guarantee, not just a masking feature', () => { + const row = posthogRow() + const normalized = row.replace(/\s+/g, ' ') + expect(normalized).toContain('maskering standardläget') + expect(normalized).toContain('kan inte stängas av') + expect(normalized).toContain('maskeras utan undantag') + // The failure mode for untagged new UI is over-masking, never leakage + // (lib/analytics/replay-masking.ts). + expect(normalized).toContain('övermaskering') + }) +}) diff --git a/app/(public)/privacy/page.tsx b/app/(public)/privacy/page.tsx index add45ad9..b1986acd 100644 --- a/app/(public)/privacy/page.tsx +++ b/app/(public)/privacy/page.tsx @@ -19,7 +19,7 @@ export default function PrivacyPolicyPage() { Integritetspolicy

- Senast uppdaterad: 2026-08-17 + Senast uppdaterad: 2026-08-19

@@ -133,8 +133,10 @@ export default function PrivacyPolicyPage() { Amazon Web Services (AWS) AI-inferens (kategorisering samt dokument- och - kvittotolkning) via Amazon Bedrock. Bearbetar bokföringsdata - och uppladdade underlag: endast när AI-funktioner är + kvittotolkning): AI-anropen skickas till Amazon Bedrock, + och modellerna som används är Anthropics Claude-modeller, + körda inom Bedrock. Bearbetar bokföringsdata och + uppladdade underlag: endast när AI-funktioner är aktiverade. EU (eu-north-1, Stockholm) @@ -158,12 +160,17 @@ export default function PrivacyPolicyPage() { användar-ID, e-postadress, namn och företagsnamn. Om du själv skriver till supporten i appen skickas även ditt meddelande dit som ett ärende, så att vi kan svara. I - sessionsinspelningar maskeras allt du skriver och allt - innehåll i din bokföring (namn, beskrivningar, belopp, - person- och organisationsnummer); läsbart är endast - appens eget gränssnitt, som rubriker, knappar, menyer - och ledtexter, så att vi kan se var i appen du stöter - på problem utan att se dina uppgifter. + sessionsinspelningar är maskering standardläget och kan + inte stängas av: allt du skriver maskeras utan undantag, + och all annan text maskeras om den inte är appens eget + statiska gränssnitt, som rubriker, knappar, menyer och + ledtexter. Ditt innehåll (namn, beskrivningar, belopp, + person- och organisationsnummer) är därför aldrig + läsbart, och även nytt eller omärkt gränssnitt maskeras + tills det uttryckligen märkts som gränssnittstext: + felläget är övermaskering, aldrig att dina uppgifter + syns. Inspelningarna finns så att vi kan se var i appen + du stöter på problem utan att se dina uppgifter. Organisationsnummer skickas aldrig som analysdata. Identifiering sker endast för inloggade användare (ej sandbox/demo). Inga kakor används, och själva analysdatan