diff --git a/.compliance/Data_Classification_Handling.md b/.compliance/Data_Classification_Handling.md index 3846b590..704ce6cb 100644 --- a/.compliance/Data_Classification_Handling.md +++ b/.compliance/Data_Classification_Handling.md @@ -70,3 +70,47 @@ logs. The authenticated RLS authorization is repeated through a stateless service-role client with explicit `user_id` and `company_id` predicates. Export queries filter by that company directly or use parent IDs fetched under the same filter. Recipients must store and transfer the ZIP as Confidential data. + +## Client-side storage inventory + +Everything this application persists on a user's device. It is inventoried here +because the analytics posture is "no analytics data and no cookies on the +device" (`persistence: 'memory'`), and the exceptions are only defensible if +they are known, enumerated and reviewed rather than discovered in production. + +Application-owned keys: + +- `Accounted:chat-sidebar-collapsed` — assistant sidebar UI state. +- `gnubok.inbox.onboarding.dismissed` — one-time onboarding hint dismissal. + +PostHog-owned keys. Both are written by PostHog's own modules straight to +`localStorage`, bypassing the `persistence: 'memory'` setting, so neither is +prevented by the SDK configuration: + +- `seenSurvey_` — `"true"`. Suppresses a survey the user already + answered or dismissed. Without it every survey re-prompts on each page load + under memory persistence. +- `ph_conv_` — `{"widgetSessionId": ""}`. Lets a user + return to their own support ticket in the same browser. Created at page + load, before the user contacts support. + +Classification: neither PostHog key is personal data. Both are functional UI +state: a random identifier and a boolean marker, with no name, address, email +or accounting content, and neither is readable across origins. They are the +basis for treating this storage as strictly necessary rather than requiring +consent, so a change in their content changes that assessment. + +Controls: + +- `lib/analytics/purge-legacy-storage.ts` removes storage from the retired + Recapt processor (`recapt` / `glimt` substrings) on every boot. It + deliberately does NOT touch `seenSurvey_*` or `ph_conv_*`: clearing those + would re-prompt surveys and orphan the user's own ticket. +- No cookies are set by the application or by PostHog under this configuration. + +Review trigger: **enabling any new PostHog product may silently add device +storage**, because the products write directly to `localStorage` rather than +through the SDK's persistence setting. After enabling one, inspect +`localStorage` on production and update this section, `.compliance/ropa.yaml` +and `app/(public)/privacy/page.tsx` together. The Support product was caught +this way; the assumption that `persistence: 'memory'` was sufficient was wrong. diff --git a/.compliance/ropa.yaml b/.compliance/ropa.yaml index 4ebf8d6f..a7fa2a83 100644 --- a/.compliance/ropa.yaml +++ b/.compliance/ropa.yaml @@ -588,7 +588,15 @@ processing_activities: - session_replay_masks_all_inputs - org_number_never_transmitted # låst av test i lib/analytics/__tests__ - no_pii_in_event_properties # PII endast som person properties via identify() - - no_device_storage # persistence: 'memory', inga kakor, ingen consent-banner krävs + # persistence: 'memory' -> ingen analysdata och inga kakor på enheten. + # TVÅ undantag, båda funktionell UI-state utan personuppgifter, båda + # skrivna direkt till localStorage av PostHogs egna moduler förbi + # persistence-inställningen: seenSurvey_ ("visa inte igen") och + # ph_conv_ = {"widgetSessionId": uuid} (hitta tillbaka till sitt + # eget ärende i samma webbläsare). ph_conv skapas vid sidladdning, alltså + # innan användaren faktiskt kontaktar supporten. Verifierat i prod. + - no_analytics_data_or_cookies_on_device + - only_functional_ui_state_persisted # seenSurvey_*, ph_conv_* (uuid, ingen PII) - same_origin_reverse_proxy # /rl-rewrite; ingen tredjepartsvärd i CSP - sandbox_companies_never_identified - disabled_entirely_when_self_hosted diff --git a/DECISIONS.md b/DECISIONS.md index 8fa8bf61..6488261b 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -618,3 +618,4 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-07-27] isConfigured() gates /connect only, not /disconnect, /schedule or /sync: a deployment losing its OAuth credentials must not trap users with a connection they cannot remove or a schedule they cannot switch off. [2026-07-27] Dropbox web links point at /home/Apps unless DROPBOX_APP_FOLDER_NAME is set: app-folder scoped calls cannot discover where the app folder sits in the user's account, and a link into the wrong folder reads as a lost backup, so the app-name deep link is opt-in rather than guessed. [2026-07-27] performSync keeps `provider` optional, defaulting to Google Drive: it preserves the pre-Dropbox call shape (and its test suite) as the documented legacy path while all three production call sites pass a provider explicitly. +[2026-07-27] Privacy page corrected after verifying prod: PostHog's conversations module writes ph_conv_ = {"widgetSessionId": uuid} to localStorage at PAGE LOAD, not when support is contacted, bypassing persistence: 'memory' exactly like surveys' seenSurvey_ flags. The page previously claimed enkäter were the only local storage, which was wrong the moment Support was enabled. Both values are functional UI state without personuppgifter (do-not-ask-again, and find-my-own-ticket), so the no-consent-banner position holds, but "inga kakor eller annan lagring" did not and is now stated accurately. Worth remembering that enabling any new PostHog product can silently add device storage: check localStorage in prod after each one rather than trusting the persistence setting. diff --git a/app/(public)/privacy/page.tsx b/app/(public)/privacy/page.tsx index c38ac9f3..31e89282 100644 --- a/app/(public)/privacy/page.tsx +++ b/app/(public)/privacy/page.tsx @@ -162,9 +162,13 @@ export default function PrivacyPolicyPage() { gränssnittet du klickar, aldrig vad som står i din bokföring. Organisationsnummer överförs aldrig. Identifiering sker endast för inloggade användare (ej - sandbox/demo). Inga kakor eller annan lagring på din - enhet används för analysen; enkäter sparar enbart en - lokal markering om att du redan sett dem. + sandbox/demo). Inga kakor används, och själva analysdatan + lagras inte på din enhet. Två små tekniska värden sparas + dock lokalt: en markering om vilka enkäter du redan sett, + så att du inte får samma fråga igen, och ett + slumpmässigt ärende-ID som gör att du hittar tillbaka + till ditt supportärende i samma webbläsare. Inget av dem + innehåller personuppgifter. EU (Frankfurt)