mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 19:01:08 +02:00
docs: Phase 9 documentation roundup — close encryption-shaped doc gaps
Five documentation surfaces gained encryption awareness in this
sweep. Before this commit, the only place anyone could learn about
the at-rest encryption layer or the zero-knowledge opt-in was the
internal DATA_LAYER_AUDIT.md. New contributors and self-hosters
would never discover one of the most important features of the
product just by reading the standard onboarding docs.
apps/docs/src/content/docs/architecture/security.mdx (NEW)
----------------------------------------------------------
First-class user-facing security page in the Starlight site,
slotted into the Architecture sidebar between Authentication and
Backend.
Sections:
- What's encrypted (overview table of 27 modules + the
intentional plaintext carve-outs)
- Standard mode flow with ASCII diagram
- "What Mana CAN see" trust statements per mode
- Zero-knowledge mode setup walkthrough (Steps component)
- Unlock flow on a new device
- Recovery code rotation
- Deployment requirements (the loud MANA_AUTH_KEK warning)
- Audit trail action vocabulary
- Threat model summary table
- Implementation file references with paths
services/mana-auth/CLAUDE.md
----------------------------
New "Encryption Vault" section under Key Endpoints, listing all 7
routes (status, init, key, rotate, recovery-wrap GET+DELETE,
zero-knowledge) with their HTTP method, path, error codes, and a
description. Mentions the three CHECK constraints + RLS + audit
table. Points readers at DATA_LAYER_AUDIT.md and the new
security.mdx for the deep dive.
Environment Variables block gains MANA_AUTH_KEK with a multi-line
comment explaining the openssl rand command + dev fallback warning.
apps/mana/CLAUDE.md
-------------------
Full rewrite. The existing file was from the Supabase era and
described things like @supabase/ssr, safeGetSession(), and a
five-table schema with users + organizations + teams that doesn't
exist any more. Replaced with the unified-app architecture:
- Module system layout (collections.ts / queries.ts / stores/)
- Mana Auth (Better Auth + EdDSA JWT) instead of Supabase
- Local-first data layer with the full pipeline diagram
- At-rest encryption section with the "when writing module code
that touches sensitive fields" 4-step guide
- Updated routing structure (no more separate /organizations,
/teams routes)
- Module store pattern code example
- Reference document table at the bottom pointing at the audit,
the new security.mdx, and the auth doc
Root CLAUDE.md
--------------
New "At-Rest Encryption (Phase 1–9)" subsection under the
Local-First Architecture section. Two-mode trust summary table,
production requirement for MANA_AUTH_KEK with the openssl command,
the "when writing module code" 4-step guide, and a reference
table. New contributors reading the root CLAUDE.md from top to
bottom now hit encryption naturally as part of the data layer
discussion.
.env.macmini.example
--------------------
MANA_AUTH_KEK was missing from the production env example
entirely — the macmini deployment would silently boot on the
32-zero-byte dev fallback if you copied this file. Added with a
multi-paragraph comment covering: how to generate, why it's
required, how to store securely (Docker secrets / KMS / Vault),
and the rotation caveat.
apps/docs/src/content/docs/deployment/self-hosting.mdx
------------------------------------------------------
Two changes:
1. Added MANA_AUTH_KEK to the mana-auth service block in the
Compose example with an inline comment pointing at the new
section below.
2. New "Encryption Vault Setup" H2 section with subsections:
- Generating a KEK (with a fake example value labelled DO NOT
USE — generate your own)
- Securing the KEK (Docker secrets, KMS, systemd
LoadCredential, anti-patterns)
- "What if I lose the KEK?" — explains the data is
unrecoverable by design and mitigation via zero-knowledge
mode opt-in
- KEK rotation — calls out the missing background re-wrap
job as a known limitation
apps/docs/astro.config.mjs
--------------------------
Added "Security & Encryption" entry to the Architecture sidebar
between Authentication and Backend so the new page is reachable
from the docs nav.
Astro check: 0 errors, 0 warnings, 0 hints across 4 .astro files.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b961453244
commit
142a65a22f
7 changed files with 483 additions and 132 deletions
|
|
@ -32,6 +32,22 @@ JWT_SECRET=your-jwt-secret-here
|
||||||
JWT_PUBLIC_KEY=
|
JWT_PUBLIC_KEY=
|
||||||
JWT_PRIVATE_KEY=
|
JWT_PRIVATE_KEY=
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Encryption Vault Key Encryption Key (KEK) — REQUIRED
|
||||||
|
# ============================================
|
||||||
|
# Wraps every user's master key in auth.encryption_vaults.
|
||||||
|
# Generate with: openssl rand -base64 32
|
||||||
|
#
|
||||||
|
# Without a real value, mana-auth boots with a 32-zero-byte fallback
|
||||||
|
# and prints a loud warning every startup. Production must set this.
|
||||||
|
# Treat it like a database root password — store as a Docker secret,
|
||||||
|
# KMS-injected env var, or Vault-served value.
|
||||||
|
#
|
||||||
|
# Rotation requires planned downtime today (no background re-wrap job
|
||||||
|
# yet). The kek_id column on encryption_vaults is reserved for the
|
||||||
|
# future migration path.
|
||||||
|
MANA_AUTH_KEK=
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# Supabase (optional, for legacy features)
|
# Supabase (optional, for legacy features)
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
|
||||||
25
CLAUDE.md
25
CLAUDE.md
|
|
@ -655,6 +655,31 @@ The Mana unified app uses a **single IndexedDB** (`mana`) containing all 120+ co
|
||||||
|
|
||||||
Individual apps in `apps/*/apps/web/` still use `@mana/local-store` with per-app IndexedDB databases (`mana-{appId}`). When users first open the unified Mana app, `legacy-migration.ts` migrates data from these old DBs into the unified DB.
|
Individual apps in `apps/*/apps/web/` still use `@mana/local-store` with per-app IndexedDB databases (`mana-{appId}`). When users first open the unified Mana app, `legacy-migration.ts` migrates data from these old DBs into the unified DB.
|
||||||
|
|
||||||
|
### At-Rest Encryption (Phase 1–9)
|
||||||
|
|
||||||
|
User-typed content in **27 tables** is encrypted with **AES-GCM-256** before it touches IndexedDB. The master key lives in mana-auth (KEK-wrapped) and is fetched on login. Two trust modes:
|
||||||
|
|
||||||
|
- **Standard mode** (default): server holds the user's master key wrapped with `MANA_AUTH_KEK` (env-provided). Mana operators with KEK access could theoretically decrypt; protected against all realistic threats except court-ordered disclosure.
|
||||||
|
- **Zero-Knowledge mode** (opt-in via Settings → Sicherheit): user generates a 32-byte recovery code locally, the server NULLs out the KEK wrap. Mana is then **computationally incapable** of decrypting user data — losing the recovery code means losing the data, by design.
|
||||||
|
|
||||||
|
**Production requirement:** `MANA_AUTH_KEK` must be set on the mana-auth service to a base64-encoded 32-byte value (`openssl rand -base64 32`). The dev fallback is 32 zero bytes and prints a loud warning at startup.
|
||||||
|
|
||||||
|
**When writing module code that touches sensitive fields:**
|
||||||
|
|
||||||
|
1. Add the table to `apps/mana/apps/web/src/lib/data/crypto/registry.ts` with the field allowlist
|
||||||
|
2. Wrap writes with `await encryptRecord(tableName, record)` before `table.add()` / `table.update()`
|
||||||
|
3. Wrap reads with `decryptRecords(tableName, visible)` after the Dexie query, before the type converter
|
||||||
|
4. Default to **encrypt** for user-typed text; default to **plaintext** for IDs / timestamps / sort keys / enum discriminators
|
||||||
|
|
||||||
|
**Reference docs:**
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `apps/mana/apps/web/src/lib/data/DATA_LAYER_AUDIT.md` | Complete sprint history, rollout phases, threat model, backlog |
|
||||||
|
| `apps/docs/src/content/docs/architecture/security.mdx` | User-facing walkthrough + threat model table |
|
||||||
|
| `services/mana-auth/sql/002_encryption_vaults.sql` | Vault schema + RLS policies |
|
||||||
|
| `services/mana-auth/sql/003_recovery_wrap.sql` | Phase 9 recovery columns + CHECK constraints |
|
||||||
|
|
||||||
### Dev Commands (Local-First Stack)
|
### Dev Commands (Local-First Stack)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ export default defineConfig({
|
||||||
items: [
|
items: [
|
||||||
{ label: 'Overview', slug: 'architecture/overview' },
|
{ label: 'Overview', slug: 'architecture/overview' },
|
||||||
{ label: 'Authentication', slug: 'architecture/authentication' },
|
{ label: 'Authentication', slug: 'architecture/authentication' },
|
||||||
|
{ label: 'Security & Encryption', slug: 'architecture/security' },
|
||||||
{ label: 'Backend (NestJS)', slug: 'architecture/backend' },
|
{ label: 'Backend (NestJS)', slug: 'architecture/backend' },
|
||||||
{ label: 'Web (SvelteKit)', slug: 'architecture/web' },
|
{ label: 'Web (SvelteKit)', slug: 'architecture/web' },
|
||||||
{ label: 'Mobile (Expo)', slug: 'architecture/mobile' },
|
{ label: 'Mobile (Expo)', slug: 'architecture/mobile' },
|
||||||
|
|
|
||||||
204
apps/docs/src/content/docs/architecture/security.mdx
Normal file
204
apps/docs/src/content/docs/architecture/security.mdx
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
---
|
||||||
|
title: Security & Encryption
|
||||||
|
description: Trust model, at-rest encryption, and the optional zero-knowledge mode for Mana user data.
|
||||||
|
---
|
||||||
|
|
||||||
|
import { Aside, Tabs, TabItem, Steps } from '@astrojs/starlight/components';
|
||||||
|
|
||||||
|
# Security & Encryption
|
||||||
|
|
||||||
|
Mana encrypts user-typed content with **AES-GCM-256** before it touches IndexedDB or the sync server. The user has two trust modes to choose from:
|
||||||
|
|
||||||
|
| Mode | Default | What the server can decrypt |
|
||||||
|
|------|---------|----------------------------|
|
||||||
|
| **Standard** | ✅ Yes | The user's master key, via the server-side KEK |
|
||||||
|
| **Zero-Knowledge** | Opt-in | Nothing — the recovery code lives only with the user |
|
||||||
|
|
||||||
|
## What's encrypted
|
||||||
|
|
||||||
|
**27 tables** ship with at-rest encryption enabled. The full list is in [`DATA_LAYER_AUDIT.md`](https://github.com/mana-how/mana-monorepo/blob/main/apps/mana/apps/web/src/lib/data/DATA_LAYER_AUDIT.md), but the highlights:
|
||||||
|
|
||||||
|
| Module | Fields |
|
||||||
|
|--------|--------|
|
||||||
|
| Chat | `messages.messageText`, `conversations.title`, chat templates |
|
||||||
|
| Notes | `title`, `content` |
|
||||||
|
| Dreams | `title`, `content`, `transcript`, `interpretation`, `location` |
|
||||||
|
| Memoro | `title`, `intro`, `transcript` (the largest plaintext blobs in the app) |
|
||||||
|
| Contacts | 16 PII fields (firstName, lastName, email, phone, mobile, birthday, address, social) |
|
||||||
|
| Cycles | `notes`, `mood` (GDPR Art. 9 sensitive personal data) |
|
||||||
|
| Finance | `transactions.description`, `transactions.note` |
|
||||||
|
| Cards | `front`, `back`, deck name + description |
|
||||||
|
| Todo | `tasks.title`, `description`, `subtasks`, `metadata` |
|
||||||
|
| Calendar | `events.title`, `description`, `location` + the cross-module `timeBlocks` hub |
|
||||||
|
| Picture | `images.prompt`, `negativePrompt`, board name + description |
|
||||||
|
| Storage | `files.name`, `originalName` |
|
||||||
|
| Music | `songs.title`, playlist name + description |
|
||||||
|
| Events | `socialEvents.title/description/location`, guest contact details |
|
||||||
|
|
||||||
|
What's intentionally **plaintext** for structural reasons:
|
||||||
|
|
||||||
|
- IDs, foreign keys, timestamps
|
||||||
|
- Sort/filter keys (e.g. `tasks.dueDate`, `songs.artist`)
|
||||||
|
- Public-redirect lookup keys (`uload.links.shortCode` and `originalUrl`)
|
||||||
|
- Published social-event content (decrypted by design when the user shares an RSVP link)
|
||||||
|
|
||||||
|
## Standard Mode (default)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────┐ Login ┌──────────────┐
|
||||||
|
│ Browser │────────────>│ mana-auth │
|
||||||
|
│ │ │ :3001 │
|
||||||
|
└─────────────┘ └──────────────┘
|
||||||
|
│ │
|
||||||
|
│ GET /encryption-vault/key│
|
||||||
|
│ ──────────────────────────>│
|
||||||
|
│ │ 1. Load wrapped MK from
|
||||||
|
│ │ auth.encryption_vaults
|
||||||
|
│ │ 2. Unwrap with KEK from
|
||||||
|
│ │ MANA_AUTH_KEK env var
|
||||||
|
│ │ 3. Return raw 32-byte MK
|
||||||
|
│ <──────────────────────────│
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
importMasterKey() → CryptoKey (non-extractable)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
MemoryKeyProvider holds the key for the session
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
encryptRecord() / decryptRecord() per Dexie write/read
|
||||||
|
```
|
||||||
|
|
||||||
|
The master key never crosses the browser process boundary except as base64 over HTTPS during the initial fetch. Once imported, it's a **non-extractable CryptoKey** — even malicious JavaScript with a reference to it cannot read its raw bytes.
|
||||||
|
|
||||||
|
### What Mana CAN see in Standard Mode
|
||||||
|
|
||||||
|
- ❌ Never the contents of encrypted fields without actively unwrapping the KEK
|
||||||
|
- ⚠️ Theoretically: a Mana operator with KEK access could unwrap the master key and decrypt user data. Protected against all realistic threats except a court-ordered disclosure.
|
||||||
|
|
||||||
|
This is the same trust model as 1Password's "I trust the company" tier or Signal's local-only key.
|
||||||
|
|
||||||
|
## Zero-Knowledge Mode (opt-in)
|
||||||
|
|
||||||
|
Users who want **provable** confidentiality (not just "we promise") can opt into zero-knowledge mode in **Settings → Sicherheit**.
|
||||||
|
|
||||||
|
<Steps>
|
||||||
|
|
||||||
|
1. **Generate a recovery code.** The browser generates 32 random bytes via Web Crypto, derives an AES wrap key via HKDF-SHA256, and seals the user's master key locally. The wrapped blob is sent to the server. The 32-byte secret itself **never leaves the browser**.
|
||||||
|
|
||||||
|
2. **Back up the recovery code.** The UI displays the formatted code (`1A2B-3C4D-...`, 79 characters). The user copies it into their password manager. They have to type it back to confirm — we don't move forward until the confirmation matches.
|
||||||
|
|
||||||
|
3. **Activate.** The server NULLs out the KEK-wrapped master key and sets `zero_knowledge=true` on the vault row. From this moment on, **the server is computationally incapable of decrypting the user's data**.
|
||||||
|
|
||||||
|
</Steps>
|
||||||
|
|
||||||
|
### Unlock flow on a new device
|
||||||
|
|
||||||
|
When a zero-knowledge user signs in on a new device:
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Login → JWT
|
||||||
|
↓
|
||||||
|
2. Browser fetches /encryption-vault/key
|
||||||
|
↓
|
||||||
|
3. Server returns { requiresRecoveryCode: true, recoveryWrappedMk, recoveryIv }
|
||||||
|
↓
|
||||||
|
4. Browser shows the RecoveryCodeUnlockModal
|
||||||
|
↓
|
||||||
|
5. User pastes the code from their password manager
|
||||||
|
↓
|
||||||
|
6. Browser unwraps the master key locally with HKDF + AES-GCM
|
||||||
|
↓
|
||||||
|
7. App boots with decrypted data
|
||||||
|
```
|
||||||
|
|
||||||
|
If the user loses the recovery code, **the data is unrecoverable**. Mana cannot help — that's the design. The trade-off is the entire point of zero-knowledge.
|
||||||
|
|
||||||
|
### What Mana CAN see in Zero-Knowledge Mode
|
||||||
|
|
||||||
|
- ❌ Never the contents of encrypted fields
|
||||||
|
- ❌ Never the master key (the server has no usable copy)
|
||||||
|
- ❌ Never the recovery code (it lives only with the user)
|
||||||
|
- ✅ Structural metadata: number of records, timestamps, foreign keys, which modules are active
|
||||||
|
|
||||||
|
A database CHECK constraint (`encryption_vaults_zk_consistency`) enforces the "ZK active ⇒ recovery wrap exists" invariant at the schema level, so the server cannot accidentally lock a user out.
|
||||||
|
|
||||||
|
## Recovery code rotation
|
||||||
|
|
||||||
|
Users can rotate their recovery code without disabling zero-knowledge mode. The settings page shows a "🔁 Recovery-Code rotieren" button when ZK is active. The flow:
|
||||||
|
|
||||||
|
1. Browser uses the cached master key bytes (stashed during the recovery-code unlock earlier in the session)
|
||||||
|
2. Generates a fresh 32-byte secret + new HKDF wrap key
|
||||||
|
3. Seals the same master key with the new wrap key
|
||||||
|
4. Posts the new wrap to `/recovery-wrap` (replaces the previous row)
|
||||||
|
5. Displays the new code; the old one is now permanently invalid
|
||||||
|
|
||||||
|
If the user is in standard mode, rotation re-fetches the master key from the server (same path as the initial setup).
|
||||||
|
|
||||||
|
## Deployment requirements
|
||||||
|
|
||||||
|
<Aside type="caution" title="MANA_AUTH_KEK is required in production">
|
||||||
|
mana-auth refuses to start without `MANA_AUTH_KEK` set to a base64-encoded 32-byte value. The dev fallback is 32 zero bytes, which prints a loud warning. Generate a real key with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
openssl rand -base64 32
|
||||||
|
```
|
||||||
|
|
||||||
|
Store it as a Docker secret, KMS-injected env var, or Vault-served value. The KEK never touches the database — it lives only in process memory.
|
||||||
|
</Aside>
|
||||||
|
|
||||||
|
### Key rotation
|
||||||
|
|
||||||
|
The KEK and the per-user master keys can rotate independently:
|
||||||
|
|
||||||
|
- **MK rotation** (`POST /encryption-vault/rotate`): mints a fresh master key, wipes the existing wrap. The old MK is lost — caller is responsible for re-encrypting any data sealed with it. Use case: suspected device compromise.
|
||||||
|
- **KEK rotation**: handled by deploying a new `MANA_AUTH_KEK` value with a new `kek_id`. Old vault rows keep their original `kek_id` until a background re-wrap job migrates them. (The migration job is a future addition — for now KEK rotation requires planned downtime.)
|
||||||
|
|
||||||
|
## Audit trail
|
||||||
|
|
||||||
|
Every vault access is recorded in `auth.encryption_vault_audit`:
|
||||||
|
|
||||||
|
| Action | When |
|
||||||
|
|--------|------|
|
||||||
|
| `init` | First-time vault creation |
|
||||||
|
| `fetch` | Each `GET /key` call (the hot path) |
|
||||||
|
| `failed_fetch` | Any 4xx/5xx on the fetch path |
|
||||||
|
| `rotate` | Master key rotation |
|
||||||
|
| `recovery_set` | New recovery wrap stored |
|
||||||
|
| `recovery_clear` | Recovery wrap removed |
|
||||||
|
| `zk_enable` | Zero-knowledge mode activated |
|
||||||
|
| `zk_disable` | Zero-knowledge mode deactivated |
|
||||||
|
|
||||||
|
Each row carries the IP address, user-agent, HTTP status code, and a free-form context string. Used for security investigations and compliance reporting.
|
||||||
|
|
||||||
|
## Threat model summary
|
||||||
|
|
||||||
|
| Threat | Standard | Zero-Knowledge |
|
||||||
|
|--------|----------|----------------|
|
||||||
|
| Browser-local malware reading IndexedDB | ✅ Protected (encrypted blobs) | ✅ Protected |
|
||||||
|
| Stolen device with no screen lock | ✅ Protected (key not persisted) | ✅ Protected |
|
||||||
|
| Database leak (encrypted_vaults dump) | ✅ Protected (KEK is in env, not DB) | ✅ Protected |
|
||||||
|
| Mana operator with full DB access | ⚠️ Could decrypt with KEK | ✅ Cannot decrypt |
|
||||||
|
| Mana operator with full DB + KEK access | ⚠️ Could decrypt | ✅ Cannot decrypt (no usable wrap) |
|
||||||
|
| Court order against Mana | ⚠️ Could be compelled to decrypt | ✅ Mana physically cannot comply |
|
||||||
|
| User loses recovery code | n/a | ❌ Data lost |
|
||||||
|
| User loses password but vault is in ZK mode | Recovery via password reset | ❌ Data lost (vault is keyed to recovery code) |
|
||||||
|
|
||||||
|
## Implementation references
|
||||||
|
|
||||||
|
For the architectural deep dive, code locations, and the complete rollout history (Phases 1–9 + the backlog sweep), see [`DATA_LAYER_AUDIT.md`](https://github.com/mana-how/mana-monorepo/blob/main/apps/mana/apps/web/src/lib/data/DATA_LAYER_AUDIT.md).
|
||||||
|
|
||||||
|
Key files:
|
||||||
|
|
||||||
|
| File | Role |
|
||||||
|
|------|------|
|
||||||
|
| `apps/mana/apps/web/src/lib/data/crypto/aes.ts` | AES-GCM-256 wrap/unwrap primitives |
|
||||||
|
| `apps/mana/apps/web/src/lib/data/crypto/registry.ts` | Allowlist of which fields on which tables get encrypted |
|
||||||
|
| `apps/mana/apps/web/src/lib/data/crypto/recovery.ts` | Recovery code generation, format/parse, HKDF wrap |
|
||||||
|
| `apps/mana/apps/web/src/lib/data/crypto/vault-client.ts` | Browser-side vault client + zero-knowledge state machine |
|
||||||
|
| `apps/mana/apps/web/src/lib/components/RecoveryCodeUnlockModal.svelte` | Lock-screen modal for the ZK unlock flow |
|
||||||
|
| `apps/mana/apps/web/src/routes/(app)/settings/security/+page.svelte` | Settings UI for setup, rotation, disable |
|
||||||
|
| `services/mana-auth/src/services/encryption-vault/index.ts` | Server-side vault service |
|
||||||
|
| `services/mana-auth/src/services/encryption-vault/kek.ts` | KEK loader + master-key wrap helpers |
|
||||||
|
| `services/mana-auth/sql/002_encryption_vaults.sql` | Vault table + RLS policies |
|
||||||
|
| `services/mana-auth/sql/003_recovery_wrap.sql` | Recovery wrap columns + CHECK constraints |
|
||||||
|
|
@ -120,6 +120,10 @@ services:
|
||||||
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
REDIS_PASSWORD: ${REDIS_PASSWORD}
|
||||||
JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY}
|
JWT_PRIVATE_KEY: ${JWT_PRIVATE_KEY}
|
||||||
JWT_PUBLIC_KEY: ${JWT_PUBLIC_KEY}
|
JWT_PUBLIC_KEY: ${JWT_PUBLIC_KEY}
|
||||||
|
# REQUIRED: encryption-vault Key Encryption Key.
|
||||||
|
# Generate with: openssl rand -base64 32
|
||||||
|
# See "Encryption Vault Setup" below.
|
||||||
|
MANA_AUTH_KEK: ${MANA_AUTH_KEK}
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
@ -160,6 +164,50 @@ echo "JWT_PRIVATE_KEY=$(cat private.pem | base64 -w 0)"
|
||||||
echo "JWT_PUBLIC_KEY=$(cat public.pem | base64 -w 0)"
|
echo "JWT_PUBLIC_KEY=$(cat public.pem | base64 -w 0)"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Encryption Vault Setup
|
||||||
|
|
||||||
|
mana-auth ships with a per-user encryption vault that wraps each user's master key with a service-wide **Key Encryption Key (KEK)**. The KEK is loaded from the `MANA_AUTH_KEK` environment variable on boot — **it is required in production** and refusing to set it leaves you running on a 32-zero-byte dev fallback that prints a loud warning every startup.
|
||||||
|
|
||||||
|
### Generating a KEK
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 32 random bytes, base64-encoded
|
||||||
|
openssl rand -base64 32
|
||||||
|
|
||||||
|
# Example output (DO NOT use this — generate your own!)
|
||||||
|
# 4n8jzXq2K9pL5mR7tY1wE3uI6oP0sD8fG2hJ4kL6nM8=
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the result to your `.env`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
MANA_AUTH_KEK=4n8jzXq2K9pL5mR7tY1wE3uI6oP0sD8fG2hJ4kL6nM8=
|
||||||
|
```
|
||||||
|
|
||||||
|
### Securing the KEK
|
||||||
|
|
||||||
|
Treat the KEK like a database root password — anyone with both the KEK and the database can decrypt every user's master key. Do not commit it to git, do not log it, do not paste it into chat.
|
||||||
|
|
||||||
|
Recommended storage:
|
||||||
|
|
||||||
|
- **Docker Secrets**: `secrets:` block in compose, mounted into the container at `/run/secrets/mana_auth_kek`
|
||||||
|
- **HashiCorp Vault / AWS Secrets Manager / Google Secret Manager**: inject at boot via init container or sidecar
|
||||||
|
- **systemd `LoadCredential=`**: when running mana-auth as a systemd service
|
||||||
|
|
||||||
|
Avoid plaintext `.env` files on production hosts where possible.
|
||||||
|
|
||||||
|
### What if I lose the KEK?
|
||||||
|
|
||||||
|
Every user's master key in `auth.encryption_vaults` becomes unrecoverable. The wrapped data on disk is mathematically opaque without the KEK. There is no backup path on the server side — by design.
|
||||||
|
|
||||||
|
The mitigation: users who care about that risk should opt into **Zero-Knowledge Mode** in **Settings → Sicherheit**, which moves the wrap from the server-side KEK to a client-held recovery code. After that, the KEK loss only affects new user sign-ups, not existing accounts.
|
||||||
|
|
||||||
|
### KEK rotation
|
||||||
|
|
||||||
|
KEK rotation today requires planned downtime — there is no background re-wrap job yet. The `kek_id` column on `auth.encryption_vaults` is reserved for the future migration path. Track issue [#TODO] for when this lands.
|
||||||
|
|
||||||
|
For now, treat the KEK as long-lived and rotate JWT signing keys (which are independent) on the regular schedule instead.
|
||||||
|
|
||||||
## Reverse Proxy Setup
|
## Reverse Proxy Setup
|
||||||
|
|
||||||
### Nginx
|
### Nginx
|
||||||
|
|
|
||||||
|
|
@ -4,82 +4,118 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||||
|
|
||||||
## Project Overview
|
## Project Overview
|
||||||
|
|
||||||
Mana Apps is a monorepo containing multiple applications that share a unified authentication system powered by Supabase. The repository includes:
|
**Mana** is the unified web application serving 27+ product modules (todo, calendar, contacts, chat, notes, dreams, memoro, cards, picture, presi, music, storage, …) under one SvelteKit app, one IndexedDB, one auth session, and one deployment at **mana.how**.
|
||||||
|
|
||||||
- **Web App** (`apps/web`): SvelteKit-based web application
|
- **Web App** (`apps/web`): SvelteKit 2 + Svelte 5 unified app — the main surface
|
||||||
- **Mobile App** (`apps/mobile`): React Native (Expo) application for iOS, Android, and web
|
- **Mobile App** (`apps/mobile`): Expo / React Native (work-in-progress, separate codebase)
|
||||||
- **Landing** (`apps/landing`): Landing page directory (currently minimal/empty)
|
- **Landing** (`apps/landing`): Astro static landing page deployed to Cloudflare Pages
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
### Multi-App Ecosystem
|
### Unified Module System
|
||||||
|
|
||||||
This is a multi-tenant system where a single authentication backend supports multiple branded applications (Memoro, Cards, Storyteller, Mana). Each app shares the same user database but can present different branding and features.
|
All modules share one SvelteKit build. Each module lives in `apps/web/src/lib/modules/{name}/` and registers itself via a `module.config.ts` file. The data layer uses a single Dexie IndexedDB (`mana`) containing all 120+ collections from every module — table names that collide across modules are prefixed (e.g. `todoProjects`, `cardDecks`, `presiDecks`).
|
||||||
|
|
||||||
**Key concept**: App configuration is centralized in `apps/web/src/lib/config/apps.ts` and defines branding, features, and routing for each application in the ecosystem.
|
Module state lives in three files per module:
|
||||||
|
|
||||||
### Authentication & Session Management
|
| File | Role |
|
||||||
|
|------|------|
|
||||||
|
| `collections.ts` | Dexie table references + (sometimes) seed data |
|
||||||
|
| `queries.ts` | Read-side: Dexie liveQuery hooks, type converters, pure helpers for `$derived` |
|
||||||
|
| `stores/*.svelte.ts` | Write-side: mutation methods, no reads (those go through queries.ts) |
|
||||||
|
|
||||||
Both web and mobile apps use Supabase for authentication with different approaches:
|
### Authentication
|
||||||
|
|
||||||
**Web App (SvelteKit)**:
|
**Mana Auth** is the central authentication service (`services/mana-auth/`, port 3001), built on Better Auth + Hono + Bun. The web app talks to it via the shared `@mana/shared-auth` client.
|
||||||
|
|
||||||
- Server-side authentication using `@supabase/ssr`
|
- **Token format**: EdDSA JWT with minimal claims (`sub`, `email`, `role`, `sid`, `tier`)
|
||||||
- Two-hook middleware system in `apps/web/src/hooks.server.ts`:
|
- **Session storage**: Cookies (`*.mana.how` domain) + JWT in memory
|
||||||
- `supabase` hook: Creates Supabase client per request with cookie management
|
- **Route protection**: `(app)` group is auth-gated via the `AuthGate` component in the layout
|
||||||
- `authGuard` hook: Validates JWT, protects `(app)` routes, redirects based on auth state
|
- **Cross-app SSO**: Same Mana Auth session works across all `*.mana.how` apps
|
||||||
- Safe session validation: Uses `safeGetSession()` which validates JWT via `getUser()` instead of just reading from cookies
|
- **Access tiers**: `guest` < `public` < `beta` < `alpha` < `founder` — apps can require a minimum tier via `mana-apps.ts`
|
||||||
- Route groups: `(auth)` for login/register, `(app)` for protected dashboard pages
|
|
||||||
|
|
||||||
**Mobile App (Expo)**:
|
The legacy Supabase integration was removed. Anything mentioning `@supabase/ssr`, `safeGetSession()`, or `event.locals.supabase` is a leftover from a much earlier iteration and should be deleted on sight.
|
||||||
|
|
||||||
- Client-side authentication using `@supabase/supabase-js`
|
### Data Layer (Local-First)
|
||||||
- Custom memory storage implementation (`apps/mobile/utils/memoryStorage.ts`) for session persistence
|
|
||||||
- `AuthProvider` component in `apps/mobile/app/_layout.tsx` handles auth state and navigation
|
|
||||||
- Platform-specific configuration (web build disables realtime to avoid import issues)
|
|
||||||
|
|
||||||
### Database Schema
|
The app reads and writes to IndexedDB **first**, then syncs to the server in the background via the **mana-sync** Go service (port 3050).
|
||||||
|
|
||||||
Key tables (inferred from queries):
|
Architecture diagram:
|
||||||
|
|
||||||
- `users`: User profiles linked via `auth_id` to Supabase Auth users
|
```
|
||||||
- `user_roles`: Junction table linking users to organizations (with role information)
|
User action (e.g. tasksStore.createTask)
|
||||||
- `organizations`: Organization entities
|
│
|
||||||
- `teams`: Team entities within organizations
|
▼
|
||||||
- `team_members`: Junction table linking users to teams
|
Module store builds the LocalRecord
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
encryptRecord(tableName, record) ← Phase 4–9 encryption layer
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
table.add(encryptedRecord) ← Dexie write
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Dexie hooks (database.ts):
|
||||||
|
- stamp userId
|
||||||
|
- stamp __fieldTimestamps per field
|
||||||
|
- record into _pendingChanges
|
||||||
|
- record into _activity
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Sync engine (sync.ts) — debounced 1s
|
||||||
|
- groups changes by appId
|
||||||
|
- POSTs to mana-sync
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
mana-sync → PostgreSQL with field-level LWW + RLS
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Other clients pull via SSE / polling
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
applyServerChanges → Dexie hooks (suppressed) → liveQuery → decryptRecord → UI
|
||||||
|
```
|
||||||
|
|
||||||
|
For the deep dive — sync engine, retry/backoff, quota recovery, telemetry, RLS, encryption rollout — read **`apps/web/src/lib/data/DATA_LAYER_AUDIT.md`**. This is the single most important file for understanding how the app works under the hood.
|
||||||
|
|
||||||
|
### At-Rest Encryption
|
||||||
|
|
||||||
|
User-typed content in **27 tables** is encrypted with **AES-GCM-256** before it touches IndexedDB. The master key lives in `mana-auth` (KEK-wrapped) and is fetched on login.
|
||||||
|
|
||||||
|
Two trust modes:
|
||||||
|
|
||||||
|
| Mode | Default | What Mana can decrypt |
|
||||||
|
|------|---------|----------------------|
|
||||||
|
| Standard | ✅ Yes | The user's master key, via the server-side KEK |
|
||||||
|
| Zero-Knowledge | Opt-in (Settings → Sicherheit) | Nothing — recovery code lives only with the user |
|
||||||
|
|
||||||
|
**When writing module code that touches sensitive fields:**
|
||||||
|
|
||||||
|
1. Add the table to `apps/web/src/lib/data/crypto/registry.ts` with the field allowlist
|
||||||
|
2. Wrap writes: `await encryptRecord(tableName, record)` before `table.add()` / `table.update()`
|
||||||
|
3. Wrap reads: `decryptRecords(tableName, visible)` after the Dexie query, before the type converter
|
||||||
|
4. The Dexie hook in `database.ts` does NOT auto-encrypt — every store does it explicitly. This is by design (Web Crypto is async, hooks are sync).
|
||||||
|
|
||||||
|
For new sensitive fields, default to **encrypt**. For new structural fields (IDs, timestamps, enums, sort/filter keys), default to **plaintext**.
|
||||||
|
|
||||||
|
User-facing docs at `apps/docs/src/content/docs/architecture/security.mdx`.
|
||||||
|
|
||||||
### Routing Structure
|
### Routing Structure
|
||||||
|
|
||||||
**Web App** (SvelteKit file-based routing):
|
|
||||||
|
|
||||||
```
|
```
|
||||||
routes/
|
apps/web/src/routes/
|
||||||
├── (auth)/ # Public authentication pages
|
├── (auth)/ # Public auth pages (login, register, recovery)
|
||||||
│ ├── login/
|
├── (app)/ # Auth-gated app surface — 27+ module routes
|
||||||
│ └── register/
|
│ ├── dashboard/ # Customizable widget grid
|
||||||
├── (app)/ # Protected application pages
|
|
||||||
│ ├── dashboard/
|
|
||||||
│ ├── organizations/
|
|
||||||
│ ├── settings/
|
│ ├── settings/
|
||||||
│ └── teams/
|
│ │ └── security/ # Vault status + recovery code + ZK opt-in
|
||||||
└── api/ # API endpoints
|
│ ├── todo/ # …and many more module routes
|
||||||
|
│ └── …
|
||||||
|
├── workbench/ # Multi-pane interface (ListView + DetailView per module)
|
||||||
|
└── api/ # SvelteKit API endpoints (rare; most data is local-first)
|
||||||
```
|
```
|
||||||
|
|
||||||
**Mobile App** (Expo Router):
|
### Path Aliases
|
||||||
|
|
||||||
```
|
|
||||||
app/
|
|
||||||
├── (drawer)/ # Main drawer navigation
|
|
||||||
│ ├── (tabs)/ # Nested tab navigation
|
|
||||||
│ ├── organizations/
|
|
||||||
│ ├── teams/
|
|
||||||
│ ├── settings/
|
|
||||||
│ └── apps/
|
|
||||||
├── auth/ # Auth-related screens
|
|
||||||
└── login # Login screen
|
|
||||||
```
|
|
||||||
|
|
||||||
### Path Aliases (Web App)
|
|
||||||
|
|
||||||
Defined in `apps/web/svelte.config.js`:
|
Defined in `apps/web/svelte.config.js`:
|
||||||
|
|
||||||
|
|
@ -102,13 +138,12 @@ pnpm dev # Start dev server on port 5173
|
||||||
|
|
||||||
# Building
|
# Building
|
||||||
pnpm build # Build for production
|
pnpm build # Build for production
|
||||||
pnpm preview # Preview production build on port 4173
|
pnpm preview # Preview production build
|
||||||
|
|
||||||
# Code Quality
|
# Code Quality
|
||||||
pnpm check # Type-check with svelte-check
|
pnpm check # Type-check with svelte-check
|
||||||
pnpm check:watch # Type-check in watch mode
|
pnpm lint # Format check + ESLint
|
||||||
pnpm lint # Check formatting and lint
|
pnpm format # Prettier write
|
||||||
pnpm format # Format code with Prettier
|
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
pnpm test # Run Vitest unit tests
|
pnpm test # Run Vitest unit tests
|
||||||
|
|
@ -116,113 +151,108 @@ pnpm test:ui # Run Vitest with UI
|
||||||
pnpm test:e2e # Run Playwright E2E tests
|
pnpm test:e2e # Run Playwright E2E tests
|
||||||
```
|
```
|
||||||
|
|
||||||
|
For full local-dev setup (Mana Auth + mana-sync + web together), use the root-level `dev:*:full` commands. See `docs/LOCAL_DEVELOPMENT.md` and the root `CLAUDE.md`.
|
||||||
|
|
||||||
### Mobile App (apps/mobile)
|
### Mobile App (apps/mobile)
|
||||||
|
|
||||||
```bash
|
The mobile app is currently lower-priority than the web app and may lag behind in features. Standard Expo commands apply (`pnpm start`, `pnpm ios`, `pnpm android`, EAS builds for production).
|
||||||
cd apps/mobile
|
|
||||||
|
|
||||||
# Development
|
|
||||||
npm start # Start Expo dev server with dev client
|
|
||||||
npm run ios # Run on iOS simulator
|
|
||||||
npm run android # Run on Android emulator
|
|
||||||
npm run web # Run web version on port 19006
|
|
||||||
|
|
||||||
# Building (EAS)
|
|
||||||
npm run build:dev # Build development client
|
|
||||||
npm run build:preview # Build preview/internal distribution
|
|
||||||
npm run build:prod # Build production (auto-increment version)
|
|
||||||
|
|
||||||
# Code Quality
|
|
||||||
npm run lint # Lint and check formatting
|
|
||||||
npm run format # Fix linting and format code
|
|
||||||
|
|
||||||
# Setup
|
|
||||||
npm run prebuild # Generate native projects
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment Configuration
|
## Environment Configuration
|
||||||
|
|
||||||
Both apps require Supabase credentials. Copy the `.env.example` files and configure:
|
Generated automatically from the root `.env.development` via `pnpm setup:env`. The relevant variables for the web app:
|
||||||
|
|
||||||
**Web App** (`apps/web/.env`):
|
```env
|
||||||
|
PUBLIC_MANA_AUTH_URL=http://localhost:3001
|
||||||
```
|
PUBLIC_MANA_SYNC_URL=http://localhost:3050
|
||||||
PUBLIC_SUPABASE_URL=your_supabase_url
|
|
||||||
PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
|
|
||||||
MIDDLEWARE_URL=https://mana-middleware-111768794939.europe-west3.run.app
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Mobile App** (`apps/mobile/.env`):
|
The web app does NOT read or store any database credentials directly — all server-side data goes through Mana Auth + mana-sync. See the root `CLAUDE.md` for the full env-var rundown.
|
||||||
|
|
||||||
```
|
|
||||||
EXPO_PUBLIC_SUPABASE_URL=your_supabase_url
|
|
||||||
EXPO_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
|
|
||||||
```
|
|
||||||
|
|
||||||
## Technology Stack
|
## Technology Stack
|
||||||
|
|
||||||
### Web App
|
### Web App
|
||||||
|
|
||||||
- **Framework**: SvelteKit 2 with Svelte 5
|
- **Framework**: SvelteKit 2 + Svelte 5 (runes mode throughout)
|
||||||
- **Styling**: TailwindCSS with PostCSS
|
- **Styling**: TailwindCSS
|
||||||
- **Database**: Supabase (PostgreSQL)
|
- **Auth**: Mana Auth (Better Auth + EdDSA JWT) via `@mana/shared-auth`
|
||||||
- **Auth**: Supabase Auth with SSR
|
- **Data**: Local-first with Dexie.js + mana-sync (Go) backend
|
||||||
- **Testing**: Vitest (unit), Playwright (E2E)
|
- **Encryption**: AES-GCM-256 via Web Crypto, server-wrapped MK with optional zero-knowledge mode
|
||||||
|
- **Testing**: Vitest (unit + integration with fake-indexeddb), Playwright (E2E)
|
||||||
- **Build**: Vite
|
- **Build**: Vite
|
||||||
|
|
||||||
### Mobile App
|
### Mobile App
|
||||||
|
|
||||||
- **Framework**: Expo 52 with React Native 0.76
|
- **Framework**: Expo + React Native
|
||||||
- **Routing**: Expo Router 4 (file-based)
|
- **Routing**: Expo Router (file-based)
|
||||||
- **Styling**: NativeWind (TailwindCSS for React Native)
|
- **Styling**: NativeWind
|
||||||
- **Navigation**: React Navigation (drawer, tabs)
|
|
||||||
- **Database**: Supabase
|
|
||||||
- **Build**: EAS Build
|
- **Build**: EAS Build
|
||||||
- **Platforms**: iOS, Android, Web
|
|
||||||
|
|
||||||
## Important Patterns
|
## Important Patterns
|
||||||
|
|
||||||
### Server-Side Data Loading (Web)
|
### Module Store Pattern
|
||||||
|
|
||||||
Use `+page.server.ts` files for server-side data fetching with automatic auth context:
|
Every module has a mutation-only store that handles writes + a queries file that handles reads. The store NEVER reads from Dexie via `await table.toArray()` for UI rendering — that's the queries file's job (via liveQuery hooks). The store only reads when it needs to mutate based on existing state (e.g. toggle, increment).
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export const load: PageServerLoad = async ({ locals: { supabase, session } }) => {
|
// modules/todo/stores/tasks.svelte.ts
|
||||||
if (!session) {
|
import { taskTable } from '../collections';
|
||||||
throw redirect(307, '/login');
|
import { encryptRecord } from '$lib/data/crypto';
|
||||||
}
|
import { toTask } from '../queries';
|
||||||
|
|
||||||
const { data } = await supabase.from('table_name').select('*').eq('user_id', session.user.id);
|
export const tasksStore = {
|
||||||
|
async createTask(input: {...}) {
|
||||||
return { data };
|
const newLocal: LocalTask = { ...input, id: crypto.randomUUID() };
|
||||||
|
const plaintextSnapshot = toTask({ ...newLocal });
|
||||||
|
await encryptRecord('tasks', newLocal);
|
||||||
|
await taskTable.add(newLocal);
|
||||||
|
return plaintextSnapshot;
|
||||||
|
},
|
||||||
|
// ...
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### Supabase Client Access
|
```typescript
|
||||||
|
// modules/todo/queries.ts
|
||||||
|
export function useAllTasks() {
|
||||||
|
return useLiveQueryWithDefault(async () => {
|
||||||
|
const locals = await db.table<LocalTask>('tasks').orderBy('order').toArray();
|
||||||
|
const visible = locals.filter((t) => !t.deletedAt);
|
||||||
|
const decrypted = await decryptRecords('tasks', visible);
|
||||||
|
return decrypted.map(toTask);
|
||||||
|
}, [] as Task[]);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
**Web**: Access via `event.locals.supabase` in server code, or use helper functions in `$server/supabase.ts`:
|
### Svelte 5 Runes Mode
|
||||||
|
|
||||||
- `getUser(event)`: Get current user
|
All routes and components use Svelte 5 runes:
|
||||||
- `getSession(event)`: Get current session
|
|
||||||
- `requireAuth(event)`: Require auth or throw error
|
|
||||||
- `getSupabaseServerClient(event)`: Get the Supabase client
|
|
||||||
|
|
||||||
**Mobile**: Import directly from `~/utils/supabase.ts`
|
```typescript
|
||||||
|
// CORRECT
|
||||||
|
let count = $state(0);
|
||||||
|
let doubled = $derived(count * 2);
|
||||||
|
$effect(() => {
|
||||||
|
console.log(count);
|
||||||
|
});
|
||||||
|
|
||||||
|
// WRONG (legacy Svelte 3/4)
|
||||||
|
let count = 0;
|
||||||
|
$: doubled = count * 2;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Auth Access
|
||||||
|
|
||||||
|
Auth state lives in `$lib/stores/auth.svelte.ts`. The current user id is also pushed into the data layer's `current-user.ts` so the Dexie creating-hook can auto-stamp `userId` on every record. Module stores never need to know who the current user is.
|
||||||
|
|
||||||
### Route Protection
|
### Route Protection
|
||||||
|
|
||||||
**Web**: Automatic via `authGuard` hook in `hooks.server.ts`. Routes in `(app)` group are protected.
|
The `(app)` group is wrapped by an `AuthGate` component that redirects unauthenticated users to `/login` and reads the access tier from the JWT to gate beta/alpha/founder-only modules.
|
||||||
|
|
||||||
**Mobile**: Handled by `AuthProvider` in `_layout.tsx` which redirects unauthenticated users to `/login`.
|
## Reference Documents
|
||||||
|
|
||||||
## Multi-App Branding
|
| Path | Purpose |
|
||||||
|
|------|---------|
|
||||||
When adding new apps to the ecosystem, update `apps/web/src/lib/config/apps.ts` with:
|
| `apps/web/src/lib/data/DATA_LAYER_AUDIT.md` | Complete data-layer architecture, sync engine, encryption rollout, threat model, backlog |
|
||||||
|
| `apps/docs/src/content/docs/architecture/security.mdx` | User-facing security & encryption walkthrough |
|
||||||
- App name and display name
|
| `apps/docs/src/content/docs/architecture/authentication.mdx` | Auth flow + JWT structure |
|
||||||
- Tagline and description
|
| Root `CLAUDE.md` | Monorepo overview, dev commands, sibling services |
|
||||||
- Logo emoji and colors
|
|
||||||
- Feature list for marketing
|
|
||||||
- Dashboard route
|
|
||||||
|
|
||||||
The welcome page will automatically render the appropriate branding based on app context.
|
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,26 @@ OpenID Connect provider for Matrix/Synapse SSO.
|
||||||
|
|
||||||
Aggregates data from 3 sources: auth DB (sessions, accounts, 2FA, passkeys), mana-credits (balance, transactions), mana-sync DB (entity counts per app).
|
Aggregates data from 3 sources: auth DB (sessions, accounts, 2FA, passkeys), mana-credits (balance, transactions), mana-sync DB (entity counts per app).
|
||||||
|
|
||||||
|
### Encryption Vault (`/api/v1/me/encryption-vault/*`)
|
||||||
|
|
||||||
|
Per-user master-key custody for the Mana data-layer encryption. The browser fetches its master key here on first login and re-fetches on each session start. The key itself never lives in the database — it's wrapped with the service-wide KEK (loaded from `MANA_AUTH_KEK`).
|
||||||
|
|
||||||
|
| Method | Path | Description |
|
||||||
|
|--------|------|-------------|
|
||||||
|
| GET | `/status` | Cheap metadata read: `{ vaultExists, hasRecoveryWrap, zeroKnowledge, recoverySetAt }`. No decryption, no audit row. Used by the settings page on mount. |
|
||||||
|
| POST | `/init` | Idempotent vault initialisation. Mints + KEK-wraps a fresh master key on first call, returns the existing one on subsequent calls. |
|
||||||
|
| GET | `/key` | Hot path. Returns either `{ masterKey, formatVersion, kekId }` (standard mode) or `{ requiresRecoveryCode: true, recoveryWrappedMk, recoveryIv }` (zero-knowledge mode). |
|
||||||
|
| POST | `/rotate` | Mints a fresh master key. Old MK is gone — caller must re-encrypt or accept loss. **Forbidden in zero-knowledge mode** (`409 ZK_ROTATE_FORBIDDEN`). |
|
||||||
|
| POST | `/recovery-wrap` | Stores a client-built recovery wrap: `{ recoveryWrappedMk, recoveryIv }`. The recovery secret itself NEVER touches the wire. Idempotent — replaces existing wrap. |
|
||||||
|
| DELETE | `/recovery-wrap` | Removes the recovery wrap. **Forbidden in zero-knowledge mode** (`409 ZK_ACTIVE`) — would lock the user out. |
|
||||||
|
| POST | `/zero-knowledge` | Toggles ZK mode. `{ enable: true }` requires a recovery wrap to be set first (else `400 RECOVERY_WRAP_MISSING`). `{ enable: false, masterKey: base64 }` requires the freshly-unwrapped MK from the client so the server can KEK-re-wrap it. |
|
||||||
|
|
||||||
|
All routes write to `auth.encryption_vault_audit` for security investigations. Three database CHECK constraints enforce vault consistency at the schema level (`encryption_vaults_has_wrap`, `encryption_vaults_wrap_iv_pair`, `encryption_vaults_zk_consistency`) so a code-level bug can't accidentally lock a user out.
|
||||||
|
|
||||||
|
Schema lives in `src/db/schema/encryption-vaults.ts`, service in `src/services/encryption-vault/`. Migration files: `sql/002_encryption_vaults.sql` (Phase 2: tables + RLS) and `sql/003_recovery_wrap.sql` (Phase 9: recovery columns + ZK constraints).
|
||||||
|
|
||||||
|
For the full architectural deep-dive, threat model, and rollout history (Phases 1–9 + backlog sweep), see `apps/mana/apps/web/src/lib/data/DATA_LAYER_AUDIT.md`. User-facing docs at `apps/docs/src/content/docs/architecture/security.mdx`.
|
||||||
|
|
||||||
### Admin (`/api/v1/admin/*`)
|
### Admin (`/api/v1/admin/*`)
|
||||||
| Method | Path | Description |
|
| Method | Path | Description |
|
||||||
|--------|------|-------------|
|
|--------|------|-------------|
|
||||||
|
|
@ -84,6 +104,13 @@ SMTP_PORT=587
|
||||||
SMTP_USER=...
|
SMTP_USER=...
|
||||||
SMTP_PASS=...
|
SMTP_PASS=...
|
||||||
SYNAPSE_OIDC_CLIENT_SECRET=...
|
SYNAPSE_OIDC_CLIENT_SECRET=...
|
||||||
|
|
||||||
|
# Encryption Vault — REQUIRED IN PRODUCTION
|
||||||
|
# Base64-encoded 32-byte AES-256 key. Generate with `openssl rand -base64 32`.
|
||||||
|
# The dev fallback is 32 zero bytes (prints a loud warning at startup).
|
||||||
|
# This key wraps every user's master key in auth.encryption_vaults — guard
|
||||||
|
# it like a database password. Provision via Docker secret / KMS / Vault.
|
||||||
|
MANA_AUTH_KEK=
|
||||||
```
|
```
|
||||||
|
|
||||||
## Critical Rules
|
## Critical Rules
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue