docs(privacy): name Anthropic in the Bedrock row, state replay deny-by-default, pin disclosures to code (#1766)

* docs(privacy): name Anthropic in the Bedrock row, state replay deny-by-default

A prospect compared our security claims with a stale published DPA that
listed Anthropic and OpenAI as US processors and read the privacy page's
bare AWS row next to 'delas inte med Anthropic' as a contradiction
(#1674). The in-repo pages were factually right but nothing pinned them
to the code, and the Bedrock row never said whose models run inside it.

- Sub-processor table: the AWS row now states the models are Anthropics
  Claude, run entirely inside Amazon Bedrock (eu-north-1, Stockholm),
  and that Anthropic is the model vendor, not a sub-processor, and
  receives no data. Matches lib/ai/provider.ts: hosted inference is
  AnthropicBedrock, default region eu-north-1; no OpenAI code path
  exists anywhere.
- PostHog row: the session-replay paragraph now states the
  deny-by-default guarantee: masking is the default and cannot be
  turned off, every input is masked with no exceptions, untagged new
  UI over-masks rather than leaks. Matches instrumentation-client.ts
  (maskAllInputs: true, maskTextSelector '*', no maskInputFn) and
  lib/analytics/replay-masking.ts.
- New source-content test locks the disclosures to the code so they
  cannot drift apart silently: no OpenAI dependency or mention, region
  claim equals the provider default, Anthropic named inside the Bedrock
  row, DPA keeps /privacy as the single sub-processor list, replay
  config still deny-by-default.

The artifact the prospect actually read (published DPA PDF or marketing
security page) lives outside this repo and needs founder/legal action.

Refs #1674

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Emil <emilmattsson14@gmail.com>

* fix(privacy): drop unverifiable underbitraede claim, state only code-provable facts

Adversarial review of the #1674 branch found two overstatements:

1. The Bedrock row asserted 'Anthropic, som ar modelleverantor men inte
   underbitraede'. Whether Anthropic is a sub-processor of AWS is a
   contractual matter between AWS and Anthropic and cannot be verified
   from this repo, and the issue explicitly forbade invented legal
   wording. The row now states only what the code shows: AI requests go
   to Amazon Bedrock and the models used are Anthropics Claude models
   running inside Bedrock. No claim about Anthropics sub-processor
   status in either direction. The pre-existing footnote below the
   table is untouched (identical to main).

2. The DECISIONS.md entry claimed 'no code path sends data to
   Anthropic'. False as a global claim: lib/ai/provider.ts
   createAiClient() builds a direct Anthropic API client when
   AI_PROVIDER=anthropic or when ANTHROPIC_API_KEY is set without
   static AWS keys (the documented self-hosted path), and the region is
   process.env.AWS_REGION || 'eu-north-1', a default rather than a
   guarantee. The entry now says the hosted posture is Bedrock by
   credential precedence, acknowledges the direct API path, and routes
   the underbitraede question plus published DPA PDF / marketing page
   alignment to founder/legal.

The source-content test now pins the corrected row wording, asserts
the row contains no underbitraede verdict, and no longer cements the
removed sentence.

Refs #1674

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Emil <emilmattsson14@gmail.com>

* test(privacy): track the openai-compatible BYO provider added on main

Signed-off-by: Emil <emilmattsson14@gmail.com>

---------

Signed-off-by: Emil <emilmattsson14@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-08-20 21:36:47 +02:00
committed by GitHub
parent 72181e49bd
commit 577552ca94
3 changed files with 164 additions and 9 deletions
+2
View File
@@ -1132,3 +1132,5 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. 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.
@@ -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<string, string>
devDependencies?: Record<string, string>
}
/** 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('<tr', anchor)
return PRIVACY.slice(start, PRIVACY.indexOf('</tr>', 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('<tr', anchor)
return PRIVACY.slice(start, PRIVACY.indexOf('</tr>', 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')
})
})
+16 -9
View File
@@ -19,7 +19,7 @@ export default function PrivacyPolicyPage() {
Integritetspolicy
</h1>
<p className="text-muted-foreground">
Senast uppdaterad: 2026-08-17
Senast uppdaterad: 2026-08-19
</p>
</div>
@@ -133,8 +133,10 @@ export default function PrivacyPolicyPage() {
<td className="py-2 pr-4 font-medium">Amazon Web Services (AWS)</td>
<td className="py-2 pr-4">
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.
</td>
<td className="py-2 pr-4">EU (eu-north-1, Stockholm)</td>
@@ -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, 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, att vi kan se var i appen du stöter
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 att vi kan se var i appen
du stöter 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