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:
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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, 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
|
||||
|
||||
Reference in New Issue
Block a user