# Security and data

## Secrets and tokens
- **Secret API keys** (`sk_snakkbar_…`) identify a tenant's backend. Keep them server-side; rotate by
  creating a new key, deploying it, then revoking the old one. Snakkbar stores only a hash.
- **Operator tokens** are signed JWTs bound to one tenant and one operator, ≤ 24 h. Mint them only for
  your authenticated staff, and only with the role they should have.
- **Visitor tokens**: anonymous ones are random and stored hashed; identified ones are signed JWTs
  bound to your `externalId`. A visitor can only read and write its own conversations.
- Tokens are sent as `Authorization: Bearer`. Snakkbar never uses cookies across sites, so CSRF does
  not apply; the origin allow-list stops other sites from using a visitor's token in the browser.

## Origins
Only origins in the tenant's allowed list can run the widget and the embedded admin. Add every
production origin (with and without `www`), and staging origins as needed. `http://localhost` is
allowed outside production.

## What visitors can see
Visitors see their own conversation: messages, operator names and photos, typing and read state.
They never see staff-only notes (`handoff` summaries), other conversations, operator emails or
tenant settings beyond the public config.

## Content
- Assistant output is Markdown rendered without raw HTML; links open in a new tab with
  `rel="noopener"`.
- Uploads are checked by their bytes (JPEG, PNG, WebP, GIF) and served through short-lived signed
  URLs.
- Operator "move visitor" only allows URLs on the tenant's own site.

## Visitor profiles
Details sent from the browser (`PATCH /visitor/me`, the widget's `setUser`, `data-user-*`) are
**unverified**: anyone can send anything from a browser. For an anonymous visitor they are stored
as sent. For an identified visitor the browser can only fill `name`, `email`, `phone` and
`company` while they are empty, and only add `attributes` keys that don't exist yet (never change
or remove one); values from your backend (`POST /server/visitor-tokens`) always win. A key only
the browser has set is still browser data, so set anything that must be trusted from your backend.
Staff see `verified` on identified visitors, and the assistant is told when details are
unconfirmed. Never put secrets in a profile; staff see all of it.

## Live visitors
With the tenant's `liveVisitors` switch on (default), the widget (or the SDK's `startTracking()`)
reports each open page so staff can see who is on the site. See
[widget.md → Live visitors](widget.md#live-visitors) for the switch and consent.

**Collected:** the page URL and title; the referrer as origin and path only; tab visibility;
viewport size, scroll position and page height; the user agent (shown as device type, browser and
OS); country and city from the hosting's geo headers at session start (the IP address is not
stored on the visitor); and timestamps. Pointer positions and clicks (as coordinates) are sent
**only while an operator is watching** that visitor. They are relayed to the operators' streams
and not saved with the visitor or its history (the realtime transport keeps its own short-lived
message log, like for every doorbell event).

**Never collected:** page content, the DOM, form values, keystrokes, text selections or
screenshots. The live view is not a recording: the operator sees the public page re-loaded in
their own browser, with the visitor's cursor drawn on top.

**URL cleaning.** Before a page URL (live visitors' current page, navigation history and
`pointer` events, and the `pageUrl` of conversations and messages) is stored, shown to staff or
loaded in a live view, Snakkbar keeps
only `http(s)` URLs, removes user name and password, drops query parameters whose names look like
credentials (`token`, `secret`, `password`, `code`, `state`, `auth…`, `key`, `api_key`, `sig`,
`signature`, `session`, `sid`, `otp`, `reset…`, `ticket`, `jwt`, `nonce`, `hash` and similar) and
drops fragments that carry data (`#access_token=…`; hash routes such as `#/products/12` stay).
This way a one-time login, reset or payment link is never replayed from an operator's browser.
Use `startTracking({ url })` to report even less (e.g. without ids in the path). Referrers are cut
to origin and path.

**The live view runs in the operator's browser.** It loads the cleaned public URL with the
operator's own cookies and session, never the visitor's, so it does not show the visitor's
logged-in content, basket or form input; pages behind a login usually show their logged-out
version or a login form. The page's own scripts (including any analytics) do run in the
operator's browser. The page is framed in a sandboxed iframe without forms, popups or top-level
navigation ([admin-embed.md](admin-embed.md#framing-the-customers-site)), and the customer's site
must opt in to being framed (`frame-ancestors`).

**Who sees it.** Any operator of the tenant (both roles) can list visitors, open their history and
watch them.

## Retention
Conversations, messages and images are deleted automatically after the tenant's retention period
(7, 30, 90 or 365 days since the last message; default 90). Admins can delete a conversation at
any time.

The same period applies to live-visitor data: navigation history (pageviews) older than the
retention period is deleted, and visitors who never chatted are deleted once they have not been
seen for that long. Visitors with conversations are kept as long as their conversations.

## Rate limits
See [api.md → Rate limits](api.md#rate-limits). Limits are shared across all servers.
