868 lines
22 KiB
Markdown
868 lines
22 KiB
Markdown
# 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
|
|
```
|