new file: docs/VAULT_SYNC_DESIGN.md
This commit is contained in:
parent
2fbefde281
commit
83c5066948
1 changed files with 868 additions and 0 deletions
868
docs/VAULT_SYNC_DESIGN.md
Normal file
868
docs/VAULT_SYNC_DESIGN.md
Normal file
|
|
@ -0,0 +1,868 @@
|
|||
# Vault Sync Design Document
|
||||
|
||||
Cross-site settings synchronization via optional uncloseai.com account linking.
|
||||
|
||||
**Status:** Proposed
|
||||
**Author:** Hermes Staff
|
||||
**Date:** 2026-01-23
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
### Current State
|
||||
|
||||
The UncloseAI widget uses a per-site encrypted vault stored in localStorage (`vault.js`). Each site embedding the widget has its own isolated vault:
|
||||
|
||||
- User sets up vault on site-a.com with password
|
||||
- Settings encrypted with AES-256, stored in localStorage
|
||||
- Session persistence (7-day TTL) for convenience
|
||||
- No data leaves the user's browser
|
||||
|
||||
### Proposed Enhancement
|
||||
|
||||
Add optional cross-site sync via uncloseai.com accounts. Users who want their settings everywhere can link their vault to an account. Users who prefer local-only storage continue as before.
|
||||
|
||||
### Strategic Goal: Viral Spreading
|
||||
|
||||
Users who enjoy the widget on one site become advocates:
|
||||
1. They want it on every site they visit
|
||||
2. They recommend it to site owners
|
||||
3. Site owners embed the widget to retain users
|
||||
4. More users discover the widget
|
||||
5. Cycle repeats
|
||||
|
||||
Cross-site sync is the catalyst that transforms one-site users into multi-site evangelists.
|
||||
|
||||
---
|
||||
|
||||
## User Flow
|
||||
|
||||
### Flow 1: First-Time User (Local Only)
|
||||
|
||||
```
|
||||
1. User visits site-a.com (first time with widget)
|
||||
2. Widget prompts: "Create a secure vault?"
|
||||
3. User creates vault with password
|
||||
4. Settings stored locally in localStorage
|
||||
5. Widget works normally (local vault only)
|
||||
```
|
||||
|
||||
### Flow 2: Local User Sees Sync Prompt
|
||||
|
||||
```
|
||||
1. User has been using widget on site-a.com for a while
|
||||
2. After N sessions OR manual settings access, show prompt:
|
||||
"Sync your settings across all sites?"
|
||||
[Link Account] [Not Now] [Don't Ask Again]
|
||||
3. If "Not Now": dismiss, ask again after more sessions
|
||||
4. If "Don't Ask Again": store preference, never ask again
|
||||
5. If "Link Account": proceed to account linking
|
||||
```
|
||||
|
||||
**Trigger conditions for sync prompt:**
|
||||
- User has opened settings modal at least 3 times
|
||||
- User has been using widget for at least 7 days
|
||||
- User has not dismissed "Don't Ask Again"
|
||||
- User does not already have a linked account
|
||||
|
||||
### Flow 3: Account Linking
|
||||
|
||||
```
|
||||
1. User clicks "Link Account"
|
||||
2. Modal shows options:
|
||||
- "Sign in with email (magic link)"
|
||||
- "Sign in with GitHub" (optional OAuth)
|
||||
- "Sign in with Google" (optional OAuth)
|
||||
3. User chooses email magic link
|
||||
4. User enters email address
|
||||
5. Email sent with one-time login link
|
||||
6. User clicks link, opens uncloseai.com/auth/verify?token=xxx
|
||||
7. Token validated, session cookie set
|
||||
8. Redirect back to original site with auth token in URL fragment
|
||||
9. Widget detects auth token, stores sync credentials
|
||||
10. Widget uploads encrypted vault to server
|
||||
11. Success message: "Settings will sync across all sites!"
|
||||
```
|
||||
|
||||
### Flow 4: Returning User with Synced Account
|
||||
|
||||
```
|
||||
1. User visits site-b.com (new site, same widget)
|
||||
2. Widget detects no local vault
|
||||
3. Widget checks for sync credentials (stored in localStorage)
|
||||
4. If credentials exist:
|
||||
a. Widget fetches encrypted vault from server
|
||||
b. User prompted for vault password (still needed for decryption)
|
||||
c. Settings decrypted locally, vault initialized
|
||||
5. User has all their settings without manual setup
|
||||
```
|
||||
|
||||
### Flow 5: Conflict Resolution
|
||||
|
||||
```
|
||||
1. User has synced account, uses widget on site-a.com
|
||||
2. User changes setting (e.g., switches model)
|
||||
3. Widget uploads new encrypted vault to server
|
||||
4. User simultaneously on site-b.com (second tab/device)
|
||||
5. site-b.com widget has stale local copy
|
||||
6. On next interaction, widget pulls from server
|
||||
7. Conflict detected: local timestamp != server timestamp
|
||||
8. Resolution: Last-write-wins (server version)
|
||||
9. User sees toast: "Settings updated from another session"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Technical Design
|
||||
|
||||
### Server-Side (uncloseai.com API)
|
||||
|
||||
#### Database Schema
|
||||
|
||||
```sql
|
||||
-- Users table (for accounts)
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
email_verified BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- OAuth connections (optional)
|
||||
CREATE TABLE oauth_connections (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
provider VARCHAR(50) NOT NULL, -- 'github', 'google'
|
||||
provider_user_id VARCHAR(255) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(provider, provider_user_id)
|
||||
);
|
||||
|
||||
-- Magic link tokens
|
||||
CREATE TABLE magic_links (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email VARCHAR(255) NOT NULL,
|
||||
token_hash VARCHAR(64) NOT NULL, -- SHA-256 of token
|
||||
expires_at TIMESTAMP NOT NULL,
|
||||
used BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Encrypted vaults (stores ciphertext only)
|
||||
CREATE TABLE vaults (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE UNIQUE,
|
||||
encrypted_data TEXT NOT NULL, -- AES-encrypted JSON blob
|
||||
version INTEGER DEFAULT 1,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- API keys for widget authentication
|
||||
CREATE TABLE api_keys (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
key_hash VARCHAR(64) NOT NULL, -- SHA-256 of API key
|
||||
name VARCHAR(100),
|
||||
last_used_at TIMESTAMP,
|
||||
expires_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
#### API Endpoints
|
||||
|
||||
```
|
||||
POST /api/auth/magic-link
|
||||
Request: { "email": "user@example.com" }
|
||||
Response: { "success": true, "message": "Check your email" }
|
||||
Action: Generate token, send email with link to /auth/verify?token=xxx
|
||||
|
||||
GET /api/auth/verify?token=xxx
|
||||
Response: { "success": true, "api_key": "vk_xxx...", "user_id": "uuid" }
|
||||
Action: Validate token, create/get user, generate API key
|
||||
Notes: API key returned only once, user must save it
|
||||
|
||||
POST /api/auth/oauth/github (optional)
|
||||
OAuth callback handler for GitHub
|
||||
|
||||
POST /api/auth/oauth/google (optional)
|
||||
OAuth callback handler for Google
|
||||
|
||||
POST /api/vault/sync
|
||||
Headers: Authorization: Bearer vk_xxx...
|
||||
Request: { "encrypted_data": "...", "version": 2 }
|
||||
Response: { "success": true, "version": 2, "updated_at": "..." }
|
||||
Action: Store encrypted blob, increment version if changed
|
||||
Notes: Server NEVER decrypts - stores ciphertext only
|
||||
|
||||
GET /api/vault/sync
|
||||
Headers: Authorization: Bearer vk_xxx...
|
||||
Response: { "encrypted_data": "...", "version": 2, "updated_at": "..." }
|
||||
Action: Return encrypted blob
|
||||
Notes: Returns 404 if no vault exists for this user
|
||||
|
||||
DELETE /api/vault/sync
|
||||
Headers: Authorization: Bearer vk_xxx...
|
||||
Response: { "success": true }
|
||||
Action: Delete vault (account remains)
|
||||
|
||||
GET /api/vault/version
|
||||
Headers: Authorization: Bearer vk_xxx...
|
||||
Response: { "version": 2, "updated_at": "..." }
|
||||
Action: Quick check for sync (lightweight polling)
|
||||
|
||||
DELETE /api/account
|
||||
Headers: Authorization: Bearer vk_xxx...
|
||||
Response: { "success": true }
|
||||
Action: Delete account and all associated data
|
||||
```
|
||||
|
||||
#### Rate Limiting
|
||||
|
||||
```
|
||||
/api/auth/magic-link: 5 requests per email per hour
|
||||
/api/vault/sync POST: 60 requests per minute per user
|
||||
/api/vault/sync GET: 120 requests per minute per user
|
||||
/api/vault/version: 300 requests per minute per user
|
||||
```
|
||||
|
||||
### Client-Side (vault.js Extensions)
|
||||
|
||||
#### New Constants
|
||||
|
||||
```javascript
|
||||
const UncloseVault = {
|
||||
// ... existing constants ...
|
||||
|
||||
// Sync-related localStorage keys
|
||||
SYNC_KEY: 'uncloseai_sync_credentials',
|
||||
SYNC_PROMPT_KEY: 'uncloseai_sync_prompt_state',
|
||||
|
||||
// API base URL
|
||||
SYNC_API_BASE: 'https://uncloseai.com/api',
|
||||
|
||||
// Sync check interval (5 minutes)
|
||||
SYNC_POLL_INTERVAL_MS: 5 * 60 * 1000,
|
||||
|
||||
// ... rest of existing code ...
|
||||
};
|
||||
```
|
||||
|
||||
#### New Methods
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Check if sync is enabled and credentials exist
|
||||
*/
|
||||
isSyncEnabled() {
|
||||
const creds = this.getSyncCredentials();
|
||||
return creds !== null && creds.apiKey !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stored sync credentials
|
||||
*/
|
||||
getSyncCredentials() {
|
||||
try {
|
||||
const json = localStorage.getItem(this.SYNC_KEY);
|
||||
return json ? JSON.parse(json) : null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Store sync credentials after successful auth
|
||||
*/
|
||||
setSyncCredentials(apiKey, userId) {
|
||||
localStorage.setItem(this.SYNC_KEY, JSON.stringify({
|
||||
apiKey,
|
||||
userId,
|
||||
linkedAt: new Date().toISOString()
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear sync credentials (unlink account)
|
||||
*/
|
||||
clearSyncCredentials() {
|
||||
localStorage.removeItem(this.SYNC_KEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Push local vault to server
|
||||
* Call after any local save() if sync is enabled
|
||||
*/
|
||||
async sync() {
|
||||
if (!this.isSyncEnabled() || !this.isUnlocked()) {
|
||||
return { success: false, error: 'Sync not available' };
|
||||
}
|
||||
|
||||
const creds = this.getSyncCredentials();
|
||||
const vault = this.getVault();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.SYNC_API_BASE}/vault/sync`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${creds.apiKey}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
encrypted_data: vault.encrypted_settings,
|
||||
version: vault.version || 1
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
// API key invalid, clear credentials
|
||||
this.clearSyncCredentials();
|
||||
return { success: false, error: 'Session expired' };
|
||||
}
|
||||
return { success: false, error: 'Sync failed' };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Update local version
|
||||
vault.version = data.version;
|
||||
vault.synced_at = data.updated_at;
|
||||
this.saveVault(vault);
|
||||
|
||||
return { success: true, version: data.version };
|
||||
} catch (e) {
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull vault from server
|
||||
* Returns encrypted data that still needs password to decrypt
|
||||
*/
|
||||
async pull() {
|
||||
if (!this.isSyncEnabled()) {
|
||||
return { success: false, error: 'Sync not enabled' };
|
||||
}
|
||||
|
||||
const creds = this.getSyncCredentials();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.SYNC_API_BASE}/vault/sync`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${creds.apiKey}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return { success: true, data: null }; // No vault on server yet
|
||||
}
|
||||
if (response.status === 401) {
|
||||
this.clearSyncCredentials();
|
||||
return { success: false, error: 'Session expired' };
|
||||
}
|
||||
return { success: false, error: 'Pull failed' };
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
encrypted_settings: data.encrypted_data,
|
||||
version: data.version,
|
||||
updated_at: data.updated_at
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
return { success: false, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if server has newer version
|
||||
*/
|
||||
async checkForUpdates() {
|
||||
if (!this.isSyncEnabled()) return false;
|
||||
|
||||
const creds = this.getSyncCredentials();
|
||||
const vault = this.getVault();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.SYNC_API_BASE}/vault/version`, {
|
||||
headers: { 'Authorization': `Bearer ${creds.apiKey}` }
|
||||
});
|
||||
|
||||
if (!response.ok) return false;
|
||||
|
||||
const data = await response.json();
|
||||
return data.version > (vault?.version || 0);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge server vault with local (last-write-wins)
|
||||
*/
|
||||
async syncFromServer(password) {
|
||||
const result = await this.pull();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Try to decrypt with provided password
|
||||
const decrypted = this.decrypt(result.data.encrypted_settings, password);
|
||||
if (!decrypted) {
|
||||
return { success: false, error: 'Wrong password' };
|
||||
}
|
||||
|
||||
// Replace local vault with server version
|
||||
const vault = {
|
||||
encrypted_settings: result.data.encrypted_settings,
|
||||
version: result.data.version,
|
||||
synced_at: result.data.updated_at,
|
||||
updated_at: new Date().toISOString()
|
||||
};
|
||||
|
||||
this.saveVault(vault);
|
||||
this._settings = decrypted;
|
||||
this._password = password;
|
||||
|
||||
window.dispatchEvent(new CustomEvent('uncloseai-vault-synced'));
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
```
|
||||
|
||||
#### Modified save() Method
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Save current settings to vault (modified for sync)
|
||||
*/
|
||||
async save() {
|
||||
if (!this.isUnlocked()) {
|
||||
console.error('Cannot save: vault not unlocked');
|
||||
return false;
|
||||
}
|
||||
|
||||
const vault = this.getVault() || {};
|
||||
const encrypted = this.encrypt(this._settings, this._password);
|
||||
|
||||
if (!encrypted) {
|
||||
console.error('Failed to encrypt settings');
|
||||
return false;
|
||||
}
|
||||
|
||||
vault.encrypted_settings = encrypted;
|
||||
vault.updated_at = new Date().toISOString();
|
||||
vault.version = (vault.version || 0) + 1;
|
||||
this.saveVault(vault);
|
||||
|
||||
// Auto-sync if enabled (non-blocking)
|
||||
if (this.isSyncEnabled()) {
|
||||
this.sync().catch(e => console.warn('Sync failed:', e));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Privacy and Zero-Knowledge Architecture
|
||||
|
||||
### Core Principle
|
||||
|
||||
**The server NEVER has the ability to decrypt user data.**
|
||||
|
||||
```
|
||||
User's Browser uncloseai.com Server
|
||||
| |
|
||||
| 1. User enters password |
|
||||
| (never leaves browser) |
|
||||
| |
|
||||
| 2. Settings encrypted with |
|
||||
| password using AES-256 |
|
||||
| |
|
||||
| 3. Encrypted blob sent to server |
|
||||
| POST /vault/sync |
|
||||
|----------------------------------->|
|
||||
| | 4. Server stores
|
||||
| | encrypted blob
|
||||
| | (ciphertext only)
|
||||
| |
|
||||
| 5. On new device, encrypted |
|
||||
| blob retrieved |
|
||||
|<-----------------------------------|
|
||||
| |
|
||||
| 6. User enters password |
|
||||
| (decryption happens locally) |
|
||||
| |
|
||||
```
|
||||
|
||||
### What the Server Stores
|
||||
|
||||
```
|
||||
{
|
||||
"user_id": "uuid",
|
||||
"encrypted_data": "U2FsdGVkX1+...(opaque ciphertext)...",
|
||||
"version": 3,
|
||||
"updated_at": "2026-01-23T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### What the Server CANNOT See
|
||||
|
||||
- User's vault password
|
||||
- Decrypted settings (API keys, model preferences, etc.)
|
||||
- Any plaintext user data
|
||||
|
||||
### Implications
|
||||
|
||||
1. **Password recovery is impossible** - If user forgets password, their synced vault is permanently inaccessible. This is a feature, not a bug.
|
||||
|
||||
2. **No password reset** - Account password reset would require vault re-encryption, which requires the old password.
|
||||
|
||||
3. **User responsibility** - Users must remember their vault password. We provide clear warnings during setup.
|
||||
|
||||
---
|
||||
|
||||
## Viral Mechanics
|
||||
|
||||
### "Powered by UncloseAI" Badge
|
||||
|
||||
Optional badge shown in widget corner:
|
||||
|
||||
```
|
||||
[ Settings ] [Voice] [Model] Powered by UncloseAI
|
||||
```
|
||||
|
||||
Badge behavior:
|
||||
- Shown only if site owner enables it (opt-in for site owners)
|
||||
- Or shown if user has free account (non-paying users get badge)
|
||||
- Clicking opens uncloseai.com in new tab
|
||||
- Badge can be hidden by site owners who pay/self-host
|
||||
|
||||
### "Get This Widget" Link
|
||||
|
||||
In settings modal footer:
|
||||
|
||||
```
|
||||
------------------------------------------
|
||||
| Want this AI on your website? |
|
||||
| [Add to Your Site] - Free, 2 min setup |
|
||||
------------------------------------------
|
||||
```
|
||||
|
||||
Link goes to: `uncloseai.com/embed?ref={site_domain}`
|
||||
|
||||
Referral tracking:
|
||||
- Store referring domain in analytics
|
||||
- Site owner gets credit when their users convert
|
||||
- Future: referral rewards (free premium features)
|
||||
|
||||
### Share Settings Link
|
||||
|
||||
Users can share their vault (encrypted) via URL:
|
||||
|
||||
```javascript
|
||||
async generateShareLink() {
|
||||
if (!this.isUnlocked()) return null;
|
||||
|
||||
// Create one-time export
|
||||
const exportData = {
|
||||
settings: this._settings,
|
||||
exported_at: new Date().toISOString(),
|
||||
// Exclude sensitive fields
|
||||
exclude: ['customAPIKey', 'unsandboxSecretKey']
|
||||
};
|
||||
|
||||
const filtered = { ...exportData.settings };
|
||||
exportData.exclude.forEach(k => delete filtered[k]);
|
||||
|
||||
// Encrypt with random key
|
||||
const shareKey = CryptoJS.lib.WordArray.random(16).toString();
|
||||
const encrypted = this.encrypt(filtered, shareKey);
|
||||
|
||||
// Encode in URL
|
||||
const payload = btoa(JSON.stringify({ e: encrypted }));
|
||||
return `https://uncloseai.com/share#${shareKey}:${payload}`;
|
||||
}
|
||||
```
|
||||
|
||||
Recipient flow:
|
||||
1. Opens share link
|
||||
2. Page decrypts settings from URL fragment (client-side only)
|
||||
3. Shows preview of shared settings
|
||||
4. User can import into their own vault
|
||||
|
||||
### Referral Program (Future)
|
||||
|
||||
Site owner rewards:
|
||||
- Each site owner gets referral code
|
||||
- When new site embeds widget via referral, both get benefits
|
||||
- Tiered rewards: 5 referrals = X, 10 referrals = Y
|
||||
|
||||
User rewards:
|
||||
- Users who share widget and drive signups get:
|
||||
- Extended session duration
|
||||
- Priority model access
|
||||
- Early access to new features
|
||||
|
||||
---
|
||||
|
||||
## MVP vs Full Version
|
||||
|
||||
### MVP (Phase 1)
|
||||
|
||||
**Goal:** Prove the sync concept works and users want it.
|
||||
|
||||
**Scope:**
|
||||
- Email magic link authentication only
|
||||
- Basic vault sync (push/pull)
|
||||
- Last-write-wins conflict resolution
|
||||
- "Powered by UncloseAI" badge
|
||||
- "Get This Widget" link
|
||||
|
||||
**Not in MVP:**
|
||||
- OAuth (GitHub, Google)
|
||||
- Share settings link
|
||||
- Referral program
|
||||
- Analytics for site owners
|
||||
- Paid tiers
|
||||
|
||||
**Timeline:** 2-3 weeks
|
||||
|
||||
**Success metrics:**
|
||||
- 100+ accounts created in first month
|
||||
- 20+ cross-site sync users (used on 2+ domains)
|
||||
- 10+ new site embeds from "Get This Widget"
|
||||
|
||||
### Full Version (Phase 2)
|
||||
|
||||
**Goal:** Scale viral mechanics and add monetization.
|
||||
|
||||
**Scope:**
|
||||
- OAuth providers (GitHub, Google)
|
||||
- Share settings link with preview
|
||||
- Referral program with tracking
|
||||
- Site owner analytics dashboard
|
||||
- Paid tiers (remove badge, priority support)
|
||||
- Real-time sync (WebSocket)
|
||||
- Multi-device session management
|
||||
|
||||
**Timeline:** 2-3 months after MVP validation
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### End-to-End Encryption
|
||||
|
||||
- All encryption/decryption happens in browser using CryptoJS
|
||||
- Server stores only ciphertext
|
||||
- No server-side key escrow
|
||||
- No ability to decrypt without user's password
|
||||
|
||||
### Password Requirements
|
||||
|
||||
- Minimum 8 characters (enforced client-side)
|
||||
- Encourage but don't require complexity
|
||||
- Show strength meter in UI
|
||||
- Clear warning: "This password cannot be recovered"
|
||||
|
||||
### Account Recovery
|
||||
|
||||
**Scenario: User forgets vault password**
|
||||
|
||||
Options:
|
||||
1. **Delete and restart** - User can delete their synced vault and create new one. Loses all settings.
|
||||
2. **Local fallback** - If local vault still works (different password), offer to re-sync from local.
|
||||
3. **No recovery** - By design. Zero-knowledge means no backdoor.
|
||||
|
||||
UI messaging:
|
||||
```
|
||||
Forgot your vault password?
|
||||
|
||||
Your vault is encrypted and cannot be recovered without your password.
|
||||
This is a security feature, not a bug.
|
||||
|
||||
Options:
|
||||
[ Delete Vault & Start Fresh ] - Lose all synced settings
|
||||
[ Cancel ] - Try to remember your password
|
||||
```
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
Prevent brute force and abuse:
|
||||
|
||||
```
|
||||
Endpoint Limit
|
||||
------------------------------------------
|
||||
POST /auth/magic-link 5/hour/email
|
||||
POST /vault/sync 60/minute/user
|
||||
GET /vault/sync 120/minute/user
|
||||
GET /vault/version 300/minute/user
|
||||
POST /auth/verify 10/minute/IP
|
||||
```
|
||||
|
||||
### API Key Security
|
||||
|
||||
- API keys prefixed with `vk_` for easy identification
|
||||
- Keys are 32 bytes of random data (256 bits)
|
||||
- Stored hashed (SHA-256) in database
|
||||
- Keys can be revoked from account settings
|
||||
- Keys have optional expiration
|
||||
|
||||
### CORS Policy
|
||||
|
||||
```
|
||||
Access-Control-Allow-Origin: *
|
||||
```
|
||||
|
||||
Widget runs on any domain, so CORS must be permissive. Security comes from:
|
||||
- API key authentication
|
||||
- Rate limiting
|
||||
- E2E encryption (server can't read data anyway)
|
||||
|
||||
### Magic Link Security
|
||||
|
||||
- Tokens are 32 bytes of random data
|
||||
- Tokens expire after 15 minutes
|
||||
- Tokens are single-use
|
||||
- Token hash stored in database (not plaintext)
|
||||
- Email delivery via trusted provider (SendGrid, Postmark)
|
||||
|
||||
---
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
### Server-Side
|
||||
|
||||
```
|
||||
[ ] Database schema (PostgreSQL)
|
||||
[ ] User model and migrations
|
||||
[ ] Magic link generation and verification
|
||||
[ ] Email sending integration (SendGrid)
|
||||
[ ] Vault CRUD endpoints
|
||||
[ ] API key generation and validation
|
||||
[ ] Rate limiting middleware
|
||||
[ ] CORS configuration
|
||||
[ ] Error handling and logging
|
||||
[ ] Health check endpoint
|
||||
```
|
||||
|
||||
### Client-Side
|
||||
|
||||
```
|
||||
[ ] Sync credentials storage
|
||||
[ ] sync() method
|
||||
[ ] pull() method
|
||||
[ ] checkForUpdates() method
|
||||
[ ] Modified save() with auto-sync
|
||||
[ ] Auth flow UI (modal)
|
||||
[ ] "Link Account" prompt logic
|
||||
[ ] "Powered by UncloseAI" badge
|
||||
[ ] "Get This Widget" link
|
||||
[ ] Sync status indicator
|
||||
[ ] Error handling and retry logic
|
||||
[ ] Offline queue for sync operations
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```
|
||||
[ ] Unit tests for sync methods
|
||||
[ ] Integration tests with mock server
|
||||
[ ] E2E test: full account linking flow
|
||||
[ ] E2E test: cross-site sync
|
||||
[ ] E2E test: conflict resolution
|
||||
[ ] Security audit: encryption verification
|
||||
[ ] Load testing: rate limits
|
||||
```
|
||||
|
||||
### Documentation
|
||||
|
||||
```
|
||||
[ ] User guide: setting up sync
|
||||
[ ] User guide: what happens if I forget my password
|
||||
[ ] Site owner guide: embedding options
|
||||
[ ] API documentation (for advanced users)
|
||||
[ ] Privacy policy updates
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Should vault password be separate from account password?**
|
||||
- Current design: Yes, vault has its own password
|
||||
- Alternative: Use account password for vault (simpler UX, less secure)
|
||||
|
||||
2. **How to handle vault password change?**
|
||||
- Need to re-encrypt and sync new ciphertext
|
||||
- What if user is on multiple devices during password change?
|
||||
|
||||
3. **Should we support multiple vaults per account?**
|
||||
- Use case: Different settings for work vs personal
|
||||
- Complexity: Significant, defer to Phase 2
|
||||
|
||||
4. **Real-time sync vs polling?**
|
||||
- MVP: Polling (simpler)
|
||||
- Phase 2: WebSocket for instant sync
|
||||
|
||||
5. **What settings should be excluded from sync?**
|
||||
- Definitely exclude: Nothing by default
|
||||
- User choice: Let users select what syncs
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Email Templates
|
||||
|
||||
### Magic Link Email
|
||||
|
||||
```
|
||||
Subject: Sign in to UncloseAI
|
||||
|
||||
Hi there,
|
||||
|
||||
Click the link below to sign in to your UncloseAI account:
|
||||
|
||||
[Sign In] - https://uncloseai.com/auth/verify?token=xxx
|
||||
|
||||
This link expires in 15 minutes and can only be used once.
|
||||
|
||||
If you didn't request this, you can safely ignore this email.
|
||||
|
||||
- The UncloseAI Team
|
||||
```
|
||||
|
||||
### Welcome Email (After First Sync)
|
||||
|
||||
```
|
||||
Subject: Your settings are now synced!
|
||||
|
||||
Welcome to UncloseAI!
|
||||
|
||||
Your vault is now synced across all sites. Here's what you can do:
|
||||
|
||||
1. Visit any website with the UncloseAI widget
|
||||
2. Click "Sync Settings" in the settings menu
|
||||
3. Enter your vault password
|
||||
4. Your preferences load automatically!
|
||||
|
||||
Remember: Your vault password is never sent to our servers.
|
||||
If you forget it, your synced data cannot be recovered.
|
||||
|
||||
Need help? Reply to this email.
|
||||
|
||||
- The UncloseAI Team
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue