Security
What we do, what we don't, and the threat models the system is built for.
Auth model
Three Bearer token types, each with a different scope:
| Token | Format | Scope | Used by |
|---|---|---|---|
| Project key | pk_* |
One project, project-scoped MCP tools + /v1/* writes |
Widget snippet, scripts, CI |
| OAuth JWT | RFC 7519 (HS256) | One user + one team, project-scoped + user-scoped MCP tools, filtered by selected_tools |
Coding agents, integrations |
| Supabase session | Supabase Auth cookies | One user, full dashboard access via team membership | Browser, dashboard pages |
All Bearer comparisons use timingSafeEqual / constantTimeMatch. No string ==.
OAuth 2.1 + DCR
Dynamic Client Registration per RFC 7591. PKCE S256 required. No PATs; the OAuth flow IS the install path for agents.
- Authorization codes: 10-minute TTL, single-use, deleted on success or failure.
- Access tokens: 1h TTL, HS256 JWT signed with
OAUTH_JWT_SIGNING_KEY(32 bytes hex). Each token carries a randomjtiso duplicate claims still produce unique tokens. - Refresh tokens: opaque 48-byte random, SHA-256 hashed at rest, rotated on every use.
- Tool selection captured at consent (the Sentry pattern). The user picks which MCP tools the client may call.
tools/listfilters;tools/callrefuses.
Encryption at rest
| Data | Mechanism |
|---|---|
Integration credentials (integrations.credentials_encrypted) |
AES-256-GCM, INTEGRATION_ENCRYPTION_KEY (32 bytes hex). Versioned {v, alg, iv, tag, ct} JSON envelope. |
| OAuth client secrets | SHA-256 hashed at rest. Never retrievable. |
| OAuth refresh tokens | SHA-256 hashed. |
| Supabase Auth passwords / session tokens | Managed by Supabase (bcrypt + JWT). |
The integration cipher envelope is portable: when we move to Supabase Vault or AWS KMS in production, on-disk shape stays the same.
RLS
Postgres row-level security is enabled on teams, team_members, projects, feedback, integrations, oauth_*. Policies check auth.uid() against team_members.user_id.
The dashboard reads via Drizzle + service role for performance, with explicit per-call team scope checks (requireUserScope + assertProjectAccess). RLS stays enabled as defense-in-depth so a leaked anon key can't read another team's data.
Constant-time comparisons
- Project key check in
/v1/*and MCP:timingSafeEqualon equal-length buffers; if lengths differ, runs a dummy compare against itself to keep the timing oracle flat. - OAuth client secret check: same pattern via
constantTimeMatch.
Data captured by the widget
| Item | When | Where stored |
|---|---|---|
| Voice transcript | Always (text or speech) | feedback.voice_transcript |
| Audio recording | Voice mode only | Supabase Storage audio/<projectId>/sessions/<sessionId>.<ext> |
| Screenshot | Voice or text mode | Supabase Storage audio/<projectId>/screenshots/<sessionId>.png |
| DOM snapshot (rrweb) | Always | feedback.context.dom |
| Console entries | Last 50 | feedback.context.console |
| Network entries | Last 10 | feedback.context.network |
| Device + page metadata | Always | feedback.context.{device,page} |
Identity (if identify() called) |
Always when called | feedback.identity |
Privacy controls
- Masked inputs: rrweb's
maskAllInputs: trueis set by default. Form values aren't captured. - Element hiding: add
class="earshot-block"to any element to keep it out of the DOM snapshot entirely. - Text masking: add
class="earshot-mask"to keep an element's structure but replace its text with*. - No cross-origin reads: html2canvas + rrweb only see the host page.
- Storage paths are private: the
audiobucket is non-public; the dashboard signs URLs at view time with a 1-hour TTL.
What ships before public launch
| Status | |
|---|---|
| OAuth 2.1 DCR + PKCE | ✓ |
| Encryption at rest for integration creds | ✓ |
| Constant-time comparisons | ✓ |
| RLS on every table | ✓ |
| Per-project CORS origin allowlist | Pending — Sprint 5 |
Rate limiting on /v1/* |
Pending — Sprint 5 |
| Privacy Policy + Terms + DPA | Pending — Sprint 6 |
| Configurable IP / geo capture opt-out | Pending — Sprint 6 |
| Audio retention + delete endpoint | Pending — Sprint 6 |
| Sentry on dashboard + MCP + widget | Pending — Sprint 6 |
| SOC 2 work | Post-launch with first paying customer |
Reporting an issue
Please email security@earshot.dev. We respond within one business day. PGP key on the security page (TBD).