9/10

31 of 31 controls verified · 7 audit rounds · AES-256-GCM encryption

31 Security Controls

Every control has been verified across 7 audit rounds. Filter by category or sort any column.

# Category Control Status

Category Scores

Radar breakdown across all 11 security dimensions.

Recently Resolved

Security fixes across 7 audit rounds, most recent first.

Round 7
Cache key collision fix (JSON.stringify), lazy migration race condition, gallery companyId guards
Round 6
PerCompanyStore base class, per-company data isolation for contacts, memories, and agents
Round 5
Tool credential company overrides, dual-tier resolution (company → user → null)
Round 4
Prompt injection detection via regex patterns, tool rate limits, output filtering
Round 3
CSRF double-submit cookie pattern, timing-safe comparison (crypto.timingSafeEqual)
Round 2
AES-256-GCM encryption for stored credentials, scrypt key derivation (N=16384, r=8, p=1)
Round 1
Helmet.js security headers, HSTS (max-age 31536000), Content Security Policy with nonce, CORS origin whitelist, rate limiting

Encryption at Rest

All sensitive credentials are encrypted before touching disk.

What's Encrypted

OAuth tokensGoogle, Microsoft
API keysLinkedIn, WhatsApp, OpenAI
Wallet credentialsAlpaca, crypto exchanges
Tool configsPer-company overrides

Algorithm Details

CipherAES-256-GCM
Key derivationscrypt(secret, salt, 32)
scrypt paramsN=16384, r=8, p=1
Salt16 bytes random per credential
IV12 bytes random per credential
Plaintext Credential
scrypt Key Derivation
AES-256-GCM Encrypt
salt:iv:tag:ciphertext

Storage Format

<salt_hex_32> : <iv_hex_24> : <auth_tag_hex_32> : <ciphertext_hex>

Per-credential random salt and IV ensure no key reuse, even for identical plaintext values.

Data Isolation Model

Strict directory-based isolation ensures no data leaks between users or companies.

Directory Structure

data/users/{userId}/Per-user root
workflows.jsonUser workflows
memories.jsonUser memories
credentials/Encrypted credentials
companies/{companyId}/Per-company root
contacts.jsonCompany contacts
memories.jsonCompany memories
agents.jsonCompany agents
tool-credentials.jsonCompany tool overrides

Resolution Order

1. Company overridecompanies/{cId}/tool-credentials.json
2. User defaultusers/{uId}/credentials/
3. nullTool auto-disabled
Lazy migration: First company gets legacy data, others start fresh. No cross-company data sharing.

On-Demand Vision, No Background Processing

Sentinel can identify visitors at your door against your household's reference photos. The pipeline runs only when a person, voice call, workflow, or rule explicitly asks — never as a background daemon. No surprise face matching while you're away.

Reference photos

SourceAddress-book Contact records (single source of truth — no separate face DB)
StoragePer-user / per-company on disk; never leave your account
Sent to OpenAIdetail: low (85 tokens flat)
Embeddings stored?No. Vision API is stateless on our side
Delete contactRemoves them from vision matching too

Probe images (camera frames)

WyzeCloudFront-signed thumbnail (~1h TTL); downloaded on-demand
Home Assistant/api/camera_proxy/<entity> via your local HA
Stored on server?No — bytes are sent to vision and discarded
TriggerChat / voice / workflow / Sentinel automation rule
Default poll intervalnone (off-by-default)
OpenAI biometric policy: our prompt is engineered to comply — we ask the model to compare a snapshot to the homeowner's own labeled household photos and return a name from the supplied list, never to "identify" a stranger from a public database. If a future model rejects the prompt, the dispatcher falls back to "Snapshot captured." with no false match.
Wyze account password: Wyze publishes no public OAuth, so the cloud-direct path holds your account password encrypted at rest with AES-256-GCM (per-user key) and uses it only against Wyze's auth host. Never logged. For users who want stricter isolation, the older Mac-mini-routed path keeps the password in macOS Keychain.

No-PII Web Pages — Opaque Token Auth

Households can share the week's menu, manage email opt-ins, and vote on Friday eat-out plans without an account. Engineered so the URL and rendered HTML never reveal who the family is.

Token Model

Format12 chars · base32 minus 0/O/1/l/I
Sourcecrypto.randomBytes(8)
Keyspace≈ 1.4×10¹⁵
Kindsmenu_week · preferences · eatout
LifecycleIssued on plan generation; revocable
Per-member tokensLazy on first newsletter

Privacy Guarantees

URLOpaque token only — no family or member id derivable
HTMLNo family / member name, role, age, dietary, contact
HeaderHardcoded "Your week's menu" / "Kitchen Mini"
PhotosToken-scoped — must be a dish in the bound plan
Searchnoindex meta + X-Robots-Tag + robots.txt Disallow
TestedSnapshot test fails on any forbidden substring
One-click invalidation: The Kitchen home page exposes a "Regenerate" action that revokes the current week's token. Anyone with the prior URL gets a 404 on the next request. Old non-current-week tokens stay live as read-only snapshots; they're pruned 90 days after revocation.
Distinction from website builder: The Dev Mini website builder is for marketing pages where the user wants identity in the URL (subdomain, indexable). Kitchen menu pages are for sharing live application state where privacy matters (apex, opaque token, noindex). Different problems, different security postures.

Path to 10/10

Three items remain before achieving a perfect security score.

Gallery & Reports stores still use PerUserStore with filter-only isolation. Should migrate to PerCompanyStore for full directory-level isolation.
callRelay admin tools lack companyId scoping on workflow and contact operations. Could allow cross-company data access.
gmailPoller missing null companyId guard on trigger polling. Edge case where triggers without a company context may fire incorrectly.