Update server/cryptoUtils.js
Browse files- server/cryptoUtils.js +54 -495
server/cryptoUtils.js
CHANGED
|
@@ -1,503 +1,62 @@
|
|
| 1 |
-
|
| 2 |
-
import
|
| 3 |
-
import
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
function
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
document.removeEventListener('keydown', escHandler);
|
| 33 |
-
}
|
| 34 |
-
|
| 35 |
-
/** Open a secondary (stacked) modal on top of the primary one */
|
| 36 |
-
export function openSecondaryModal(html, opts = {}) {
|
| 37 |
-
// Remove existing secondary if any
|
| 38 |
-
closeSecondaryModal();
|
| 39 |
-
|
| 40 |
-
secondaryOverlay = document.createElement('div');
|
| 41 |
-
secondaryOverlay.className = 'modal-overlay';
|
| 42 |
-
secondaryOverlay.style.zIndex = 'calc(var(--z-modal) + 50)';
|
| 43 |
-
secondaryOverlay.style.animation = 'fadeIn 0.16s ease';
|
| 44 |
-
|
| 45 |
-
const secBox = document.createElement('div');
|
| 46 |
-
secBox.className = 'modal-box' + (opts.wide ? ' wide' : '');
|
| 47 |
-
secBox.innerHTML = html;
|
| 48 |
-
secondaryOverlay.appendChild(secBox);
|
| 49 |
-
document.body.appendChild(secondaryOverlay);
|
| 50 |
-
|
| 51 |
-
if (opts.onOpen) opts.onOpen(secBox);
|
| 52 |
-
|
| 53 |
-
secondaryOverlay.onclick = (e) => {
|
| 54 |
-
if (e.target === secondaryOverlay) closeSecondaryModal();
|
| 55 |
-
};
|
| 56 |
-
}
|
| 57 |
-
|
| 58 |
-
export function closeSecondaryModal() {
|
| 59 |
-
if (secondaryOverlay) {
|
| 60 |
-
secondaryOverlay.remove();
|
| 61 |
-
secondaryOverlay = null;
|
| 62 |
-
}
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
// ── Auth modal ────────────────────────────────────────────────────────────
|
| 66 |
-
|
| 67 |
-
export function openAuthModal(initialTab = 'signin') {
|
| 68 |
-
openModal(`
|
| 69 |
-
<div class="modal-header">
|
| 70 |
-
<span class="modal-title">Sign in to InferencePort AI</span>
|
| 71 |
-
<button class="modal-close" id="auth-close-btn">×</button>
|
| 72 |
-
</div>
|
| 73 |
-
<div class="modal-body">
|
| 74 |
-
<div class="auth-tabs">
|
| 75 |
-
<button class="auth-tab ${initialTab==='signin'?'active':''}" data-tab="signin">Sign In</button>
|
| 76 |
-
<button class="auth-tab ${initialTab==='signup'?'active':''}" data-tab="signup">Create Account</button>
|
| 77 |
-
</div>
|
| 78 |
-
|
| 79 |
-
<div id="auth-signin" style="${initialTab!=='signin'?'display:none':''}">
|
| 80 |
-
<div style="display:flex;flex-direction:column;gap:8px;margin-bottom:14px;">
|
| 81 |
-
<button class="social-btn" id="github-btn">
|
| 82 |
-
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82a7.7 7.7 0 012.01-.27c.68 0 1.36.09 2.01.27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>
|
| 83 |
-
Continue with GitHub
|
| 84 |
-
</button>
|
| 85 |
-
<button class="social-btn" id="google-btn">
|
| 86 |
-
<svg width="16" height="16" viewBox="0 0 48 48"><path fill="#EA4335" d="M24 9.5c3.54 0 6.02 1.53 7.4 2.8l5.4-5.4C33.52 3.7 29.1 1.5 24 1.5 14.64 1.5 6.58 6.88 2.66 14.7l6.64 5.15C11.2 13.6 17.08 9.5 24 9.5z"/><path fill="#4285F4" d="M46.5 24c0-1.64-.15-3.22-.43-4.74H24v9h12.7c-.55 2.95-2.21 5.45-4.7 7.12l7.23 5.6C43.38 36.9 46.5 31.1 46.5 24z"/><path fill="#FBBC05" d="M9.3 28.85A14.4 14.4 0 0 1 8.5 24c0-1.68.3-3.3.8-4.85l-6.64-5.15A23.96 23.96 0 0 0 1.5 24c0 3.9.94 7.58 2.66 10.7l6.64-5.15z"/><path fill="#34A853" d="M24 46.5c6.48 0 11.92-2.14 15.9-5.82l-7.23-5.6c-2.01 1.35-4.58 2.15-8.67 2.15-6.92 0-12.8-4.1-14.7-10.05l-6.64 5.15C6.58 41.12 14.64 46.5 24 46.5z"/></svg>
|
| 87 |
-
Continue with Google
|
| 88 |
-
</button>
|
| 89 |
-
</div>
|
| 90 |
-
<div class="auth-divider">or</div>
|
| 91 |
-
<div class="form-group">
|
| 92 |
-
<label class="form-label">Email</label>
|
| 93 |
-
<input class="form-input" id="signin-email" type="email" placeholder="you@example.com" />
|
| 94 |
-
</div>
|
| 95 |
-
<div class="form-group">
|
| 96 |
-
<label class="form-label">Password</label>
|
| 97 |
-
<input class="form-input" id="signin-password" type="password" placeholder="••••••••" />
|
| 98 |
-
</div>
|
| 99 |
-
<div id="signin-error" class="form-error" style="display:none;margin-bottom:8px;"></div>
|
| 100 |
-
<button class="btn-primary" id="signin-submit" style="width:100%;">Sign In</button>
|
| 101 |
-
<div style="margin-top:10px;text-align:center;">
|
| 102 |
-
<button style="font-size:13px;color:var(--blue-bright);" id="forgot-pw">Forgot password?</button>
|
| 103 |
-
</div>
|
| 104 |
-
</div>
|
| 105 |
-
|
| 106 |
-
<div id="auth-signup" style="${initialTab!=='signup'?'display:none':''}">
|
| 107 |
-
<div class="form-group">
|
| 108 |
-
<label class="form-label">Email</label>
|
| 109 |
-
<input class="form-input" id="signup-email" type="email" placeholder="you@example.com" />
|
| 110 |
-
</div>
|
| 111 |
-
<div class="form-group">
|
| 112 |
-
<label class="form-label">Password</label>
|
| 113 |
-
<input class="form-input" id="signup-password" type="password" placeholder="Min 6 characters" />
|
| 114 |
-
</div>
|
| 115 |
-
<div id="signup-error" class="form-error" style="display:none;margin-bottom:8px;"></div>
|
| 116 |
-
<button class="btn-primary" id="signup-submit" style="width:100%;">Create Account</button>
|
| 117 |
-
</div>
|
| 118 |
-
</div>
|
| 119 |
-
`, {
|
| 120 |
-
onOpen(b) {
|
| 121 |
-
b.querySelector('#auth-close-btn')?.addEventListener('click', closeModal);
|
| 122 |
-
|
| 123 |
-
// Tab switching
|
| 124 |
-
b.querySelectorAll('.auth-tab').forEach(tab => {
|
| 125 |
-
tab.addEventListener('click', () => {
|
| 126 |
-
b.querySelectorAll('.auth-tab').forEach(t => t.classList.remove('active'));
|
| 127 |
-
tab.classList.add('active');
|
| 128 |
-
const name = tab.dataset.tab;
|
| 129 |
-
b.querySelector('#auth-signin').style.display = name === 'signin' ? '' : 'none';
|
| 130 |
-
b.querySelector('#auth-signup').style.display = name === 'signup' ? '' : 'none';
|
| 131 |
-
});
|
| 132 |
-
});
|
| 133 |
-
|
| 134 |
-
// Sign in
|
| 135 |
-
b.querySelector('#signin-submit').addEventListener('click', async () => {
|
| 136 |
-
const email = b.querySelector('#signin-email').value.trim();
|
| 137 |
-
const pass = b.querySelector('#signin-password').value;
|
| 138 |
-
const errEl = b.querySelector('#signin-error');
|
| 139 |
-
errEl.style.display = 'none';
|
| 140 |
-
try {
|
| 141 |
-
await loginWithEmail(email, pass);
|
| 142 |
-
closeModal();
|
| 143 |
-
} catch (e) {
|
| 144 |
-
errEl.textContent = e.message; errEl.style.display = '';
|
| 145 |
-
}
|
| 146 |
-
});
|
| 147 |
-
|
| 148 |
-
// Sign up
|
| 149 |
-
b.querySelector('#signup-submit').addEventListener('click', async () => {
|
| 150 |
-
const email = b.querySelector('#signup-email').value.trim();
|
| 151 |
-
const pass = b.querySelector('#signup-password').value;
|
| 152 |
-
const errEl = b.querySelector('#signup-error');
|
| 153 |
-
errEl.style.display = 'none';
|
| 154 |
-
try {
|
| 155 |
-
const result = await signUpWithEmail(email, pass);
|
| 156 |
-
if (result.access_token) {
|
| 157 |
-
closeModal();
|
| 158 |
-
} else {
|
| 159 |
-
errEl.textContent = 'Check your email to confirm your account.'; errEl.style.display = '';
|
| 160 |
-
}
|
| 161 |
-
} catch (e) {
|
| 162 |
-
errEl.textContent = e.message; errEl.style.display = '';
|
| 163 |
-
}
|
| 164 |
-
});
|
| 165 |
-
|
| 166 |
-
b.querySelector('#github-btn').addEventListener('click', () => { loginWithOAuth('github'); closeModal(); });
|
| 167 |
-
b.querySelector('#google-btn').addEventListener('click', () => { loginWithOAuth('google'); closeModal(); });
|
| 168 |
-
b.querySelector('#forgot-pw').addEventListener('click', () => openForgotPasswordModal());
|
| 169 |
-
|
| 170 |
-
// Enter key
|
| 171 |
-
[['#signin-email','#signin-password','#signin-submit'],
|
| 172 |
-
['#signup-email','#signup-password','#signup-submit']].forEach(([e, p, s]) => {
|
| 173 |
-
[e, p].forEach(sel => {
|
| 174 |
-
b.querySelector(sel)?.addEventListener('keydown', ev => {
|
| 175 |
-
if (ev.key === 'Enter') b.querySelector(s)?.click();
|
| 176 |
-
});
|
| 177 |
-
});
|
| 178 |
-
});
|
| 179 |
-
}
|
| 180 |
-
});
|
| 181 |
-
}
|
| 182 |
-
|
| 183 |
-
function openForgotPasswordModal() {
|
| 184 |
-
openModal(`
|
| 185 |
-
<div class="modal-header">
|
| 186 |
-
<span class="modal-title">Reset Password</span>
|
| 187 |
-
<button class="modal-close" id="forgot-close-btn">×</button>
|
| 188 |
-
</div>
|
| 189 |
-
<div class="modal-body">
|
| 190 |
-
<div class="form-group">
|
| 191 |
-
<label class="form-label">Email</label>
|
| 192 |
-
<input class="form-input" id="reset-email" type="email" placeholder="you@example.com" />
|
| 193 |
-
</div>
|
| 194 |
-
<div id="reset-msg" style="font-size:13px;margin-bottom:8px;display:none;"></div>
|
| 195 |
-
</div>
|
| 196 |
-
<div class="modal-footer">
|
| 197 |
-
<button class="btn-ghost" id="forgot-cancel-btn">Cancel</button>
|
| 198 |
-
<button class="btn-primary" id="reset-submit">Send Reset Link</button>
|
| 199 |
-
</div>
|
| 200 |
-
`, {
|
| 201 |
-
onOpen(b) {
|
| 202 |
-
b.querySelector('#forgot-close-btn')?.addEventListener('click', closeModal);
|
| 203 |
-
b.querySelector('#forgot-cancel-btn')?.addEventListener('click', closeModal);
|
| 204 |
-
b.querySelector('#reset-submit').addEventListener('click', async () => {
|
| 205 |
-
const email = b.querySelector('#reset-email').value.trim();
|
| 206 |
-
const msgEl = b.querySelector('#reset-msg');
|
| 207 |
-
const SUPABASE_URL = 'https://dpixehhdbtzsbckfektd.supabase.co';
|
| 208 |
-
const SUPABASE_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRwaXhlaGhkYnR6c2Jja2Zla3RkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjExNDI0MjcsImV4cCI6MjA3NjcxODQyN30.nR1KCSRQj1E_evQWnE2VaZzg7PgLp2kqt4eDKP2PkpE';
|
| 209 |
-
try {
|
| 210 |
-
await fetch(`${SUPABASE_URL}/auth/v1/recover`, {
|
| 211 |
-
method: 'POST', headers: { 'Content-Type':'application/json','apikey':SUPABASE_KEY },
|
| 212 |
-
body: JSON.stringify({ email }),
|
| 213 |
-
});
|
| 214 |
-
msgEl.textContent = 'Reset link sent. Check your email.';
|
| 215 |
-
msgEl.style.color = 'var(--plan-core)'; msgEl.style.display = '';
|
| 216 |
-
} catch { msgEl.textContent = 'Error. Try again.'; msgEl.style.display = ''; }
|
| 217 |
-
});
|
| 218 |
-
}
|
| 219 |
-
});
|
| 220 |
-
}
|
| 221 |
-
|
| 222 |
-
// ── Share modal ───────────────────────────────────────────────────────────
|
| 223 |
-
|
| 224 |
-
export function showShareModal(sessionId) {
|
| 225 |
-
if (!isAuthenticated()) return openAuthModal('signin');
|
| 226 |
-
|
| 227 |
-
openModal(`
|
| 228 |
-
<div class="modal-header">
|
| 229 |
-
<span class="modal-title">Share Chat</span>
|
| 230 |
-
<button class="modal-close" id="share-close-btn">×</button>
|
| 231 |
-
</div>
|
| 232 |
-
<div class="modal-body">
|
| 233 |
-
<div class="share-warning">
|
| 234 |
-
<span style="font-size:18px">⚠️</span>
|
| 235 |
-
<span>You are about to share this whole session. Anyone with the link can import it into their account.</span>
|
| 236 |
-
</div>
|
| 237 |
-
<div id="share-url-wrap" style="display:none;">
|
| 238 |
-
<div class="form-label" style="margin-bottom:6px;">Share link</div>
|
| 239 |
-
<div style="display:flex;gap:8px;">
|
| 240 |
-
<input class="form-input" id="share-url-input" readonly style="flex:1;" />
|
| 241 |
-
<button class="btn-ghost" id="share-copy-btn">Copy</button>
|
| 242 |
-
</div>
|
| 243 |
-
</div>
|
| 244 |
-
<div id="share-loading" style="font-size:13px;color:var(--text-muted);display:none;">Generating link…</div>
|
| 245 |
-
</div>
|
| 246 |
-
<div class="modal-footer">
|
| 247 |
-
<button class="btn-ghost" id="share-close-footer">Close</button>
|
| 248 |
-
<button class="btn-primary" id="share-generate-btn">Generate Link</button>
|
| 249 |
-
</div>
|
| 250 |
-
`, {
|
| 251 |
-
onOpen(b) {
|
| 252 |
-
b.querySelector('#share-close-btn')?.addEventListener('click', closeModal);
|
| 253 |
-
b.querySelector('#share-close-footer')?.addEventListener('click', closeModal);
|
| 254 |
-
b.querySelector('#share-generate-btn').addEventListener('click', () => {
|
| 255 |
-
b.querySelector('#share-loading').style.display = '';
|
| 256 |
-
b.querySelector('#share-generate-btn').disabled = true;
|
| 257 |
-
send({ type: 'sessions:share', sessionId });
|
| 258 |
-
|
| 259 |
-
on('sessions:shareUrl', function handler(msg) {
|
| 260 |
-
if (msg.sessionId !== sessionId) return;
|
| 261 |
-
import('./ws.js').then(({ off }) => off('sessions:shareUrl', handler));
|
| 262 |
-
b.querySelector('#share-loading').style.display = 'none';
|
| 263 |
-
b.querySelector('#share-url-wrap').style.display = '';
|
| 264 |
-
const input = b.querySelector('#share-url-input');
|
| 265 |
-
input.value = msg.url;
|
| 266 |
-
|
| 267 |
-
b.querySelector('#share-copy-btn').addEventListener('click', async () => {
|
| 268 |
-
await navigator.clipboard.writeText(msg.url).catch(() => {});
|
| 269 |
-
b.querySelector('#share-copy-btn').textContent = 'Copied!';
|
| 270 |
-
});
|
| 271 |
-
});
|
| 272 |
-
});
|
| 273 |
-
}
|
| 274 |
-
});
|
| 275 |
-
}
|
| 276 |
-
|
| 277 |
-
// ── Tool call modal ───────────────────────────────────────────────────────
|
| 278 |
-
|
| 279 |
-
export function showToolCallModal(call) {
|
| 280 |
-
const names = {
|
| 281 |
-
ollama_search: 'Web Search', read_web_page: 'Read Web Page',
|
| 282 |
-
generate_image: 'Image Generation', generate_video: 'Video Generation', generate_audio: 'Audio Generation',
|
| 283 |
};
|
| 284 |
-
const displayName = names[call.name] || call.name;
|
| 285 |
-
|
| 286 |
-
let argsDisplay = call.args || call.arguments || '{}';
|
| 287 |
-
if (typeof argsDisplay !== 'string') argsDisplay = JSON.stringify(argsDisplay, null, 2);
|
| 288 |
-
|
| 289 |
-
let resultDisplay = call.result || '—';
|
| 290 |
-
if (typeof resultDisplay !== 'string') resultDisplay = JSON.stringify(resultDisplay, null, 2);
|
| 291 |
-
|
| 292 |
-
openModal(`
|
| 293 |
-
<div class="modal-header">
|
| 294 |
-
<span class="modal-title">🔧 ${escHtml(displayName)}</span>
|
| 295 |
-
<button class="modal-close" id="tool-close-btn">×</button>
|
| 296 |
-
</div>
|
| 297 |
-
<div class="modal-body">
|
| 298 |
-
<div class="tool-detail-section">
|
| 299 |
-
<div class="tool-detail-label">Tool</div>
|
| 300 |
-
<div class="tool-detail-content" style="font-family:var(--font-sans);">${escHtml(call.name)}</div>
|
| 301 |
-
</div>
|
| 302 |
-
<div class="tool-detail-section">
|
| 303 |
-
<div class="tool-detail-label">Request</div>
|
| 304 |
-
<div class="tool-detail-content">${escHtml(argsDisplay)}</div>
|
| 305 |
-
</div>
|
| 306 |
-
<div class="tool-detail-section">
|
| 307 |
-
<div class="tool-detail-label">Response</div>
|
| 308 |
-
<div class="tool-detail-content">${escHtml(resultDisplay.slice(0, 4000))}</div>
|
| 309 |
-
</div>
|
| 310 |
-
</div>
|
| 311 |
-
<div class="modal-footer">
|
| 312 |
-
<button class="btn-ghost" id="tool-close-footer">Close</button>
|
| 313 |
-
</div>
|
| 314 |
-
`, {
|
| 315 |
-
onOpen(b) {
|
| 316 |
-
b.querySelector('#tool-close-btn')?.addEventListener('click', closeModal);
|
| 317 |
-
b.querySelector('#tool-close-footer')?.addEventListener('click', closeModal);
|
| 318 |
-
}
|
| 319 |
-
});
|
| 320 |
}
|
| 321 |
|
| 322 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
<span></span>
|
| 328 |
-
<button class="modal-close" id="img-close-btn">×</button>
|
| 329 |
-
</div>
|
| 330 |
-
<div class="modal-body" style="padding-top:0;text-align:center;">
|
| 331 |
-
<img src="${escHtml(src)}" style="max-width:100%;max-height:70vh;border-radius:8px;" alt="Image" />
|
| 332 |
-
</div>
|
| 333 |
-
<div class="modal-footer">
|
| 334 |
-
<button class="btn-ghost" id="img-close-footer">Close</button>
|
| 335 |
-
<button class="btn-primary" id="img-dl-btn">Download</button>
|
| 336 |
-
</div>
|
| 337 |
-
`, {
|
| 338 |
-
onOpen(b) {
|
| 339 |
-
b.querySelector('#img-close-btn')?.addEventListener('click', closeModal);
|
| 340 |
-
b.querySelector('#img-close-footer')?.addEventListener('click', closeModal);
|
| 341 |
-
b.querySelector('#img-dl-btn').addEventListener('click', () => {
|
| 342 |
-
const a = document.createElement('a');
|
| 343 |
-
a.href = src; a.download = `image-${Date.now()}.png`;
|
| 344 |
-
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
| 345 |
-
});
|
| 346 |
-
}
|
| 347 |
-
});
|
| 348 |
}
|
| 349 |
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
* @param {object} opts - { name, content, editable, onSave }
|
| 355 |
-
*/
|
| 356 |
-
export function openFileViewerModal({ name, content, editable = false, onSave }) {
|
| 357 |
-
const title = editable ? `Edit: ${escHtml(name)}` : escHtml(name);
|
| 358 |
-
openSecondaryModal(`
|
| 359 |
-
<div class="modal-header">
|
| 360 |
-
<span class="modal-title" style="font-size:15px;display:flex;align-items:center;gap:8px;">
|
| 361 |
-
<span style="font-size:18px;">📄</span>${title}
|
| 362 |
-
</span>
|
| 363 |
-
<button class="modal-close" id="fv-close-btn">×</button>
|
| 364 |
-
</div>
|
| 365 |
-
<div class="modal-body" style="padding-top:12px;">
|
| 366 |
-
${editable
|
| 367 |
-
? `<textarea id="fv-editor" style="width:100%;min-height:280px;background:var(--input-bg);border:1px solid var(--input-border);border-radius:var(--radius-md);padding:10px 12px;color:var(--text);font-size:13px;font-family:var(--font-mono);resize:vertical;line-height:1.55;outline:none;">${escHtml(content)}</textarea>`
|
| 368 |
-
: `<pre style="background:var(--bg-raised);border:1px solid var(--border);border-radius:var(--radius-md);padding:12px;font-size:12px;font-family:var(--font-mono);white-space:pre-wrap;word-break:break-all;max-height:400px;overflow-y:auto;color:var(--text-dim);">${escHtml(content)}</pre>`
|
| 369 |
-
}
|
| 370 |
-
</div>
|
| 371 |
-
<div class="modal-footer">
|
| 372 |
-
<button class="btn-ghost" id="fv-cancel-btn">Cancel</button>
|
| 373 |
-
${editable ? `<button class="btn-primary" id="fv-save-btn">Save</button>` : ''}
|
| 374 |
-
</div>
|
| 375 |
-
`, {
|
| 376 |
-
onOpen(b) {
|
| 377 |
-
b.querySelector('#fv-close-btn')?.addEventListener('click', closeSecondaryModal);
|
| 378 |
-
b.querySelector('#fv-cancel-btn')?.addEventListener('click', closeSecondaryModal);
|
| 379 |
-
if (editable) {
|
| 380 |
-
b.querySelector('#fv-save-btn')?.addEventListener('click', () => {
|
| 381 |
-
const val = b.querySelector('#fv-editor')?.value ?? '';
|
| 382 |
-
onSave?.(val);
|
| 383 |
-
closeSecondaryModal();
|
| 384 |
-
});
|
| 385 |
-
// Focus and auto-resize
|
| 386 |
-
const ta = b.querySelector('#fv-editor');
|
| 387 |
-
if (ta) {
|
| 388 |
-
ta.focus();
|
| 389 |
-
ta.addEventListener('input', () => {
|
| 390 |
-
ta.style.height = 'auto';
|
| 391 |
-
ta.style.height = Math.min(ta.scrollHeight, window.innerHeight * 0.6) + 'px';
|
| 392 |
-
});
|
| 393 |
-
}
|
| 394 |
-
}
|
| 395 |
-
}
|
| 396 |
-
});
|
| 397 |
-
}
|
| 398 |
-
|
| 399 |
-
// ── Chat limit modal ──────────────────────────────────────────────────────
|
| 400 |
-
|
| 401 |
-
export function openLimitModal() {
|
| 402 |
-
openModal(`
|
| 403 |
-
<div class="modal-body" style="padding-top:28px;">
|
| 404 |
-
<div class="limit-modal-inner">
|
| 405 |
-
<div class="limit-icon">💬</div>
|
| 406 |
-
<div class="limit-title">Daily limit reached</div>
|
| 407 |
-
<div class="limit-desc">Sign in or create a free account to keep chatting.<br>Guest usage resets every 24 hours.</div>
|
| 408 |
-
<div style="display:flex;gap:10px;justify-content:center;flex-wrap:wrap;">
|
| 409 |
-
<button class="btn-primary" id="limit-signin">Sign In</button>
|
| 410 |
-
<button class="btn-ghost" id="limit-signup">Create Account</button>
|
| 411 |
-
</div>
|
| 412 |
-
</div>
|
| 413 |
-
</div>
|
| 414 |
-
`, {
|
| 415 |
-
onOpen(b) {
|
| 416 |
-
b.querySelector('#limit-signin').addEventListener('click', () => { closeModal(); openAuthModal('signin'); });
|
| 417 |
-
b.querySelector('#limit-signup').addEventListener('click', () => { closeModal(); openAuthModal('signup'); });
|
| 418 |
-
}
|
| 419 |
-
});
|
| 420 |
}
|
| 421 |
|
| 422 |
-
export function
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
An unusual amount of signed out requests has come from your device. Please sign in, or if this is an error, contact <a href="mailto:incognito.email.mode@gmail.com" style="color:inherit;text-decoration:underline;">incognito.email.mode@gmail.com</a>.
|
| 433 |
-
</div>
|
| 434 |
-
<div style="display:flex;gap:10px;justify-content:center;flex-wrap:wrap;margin-top:20px;">
|
| 435 |
-
<button class="btn-primary" id="guest-rate-limit-signin">Sign In</button>
|
| 436 |
-
<button class="btn-ghost" id="guest-rate-limit-close">Close</button>
|
| 437 |
-
</div>
|
| 438 |
-
</div>
|
| 439 |
-
</div>
|
| 440 |
-
`, {
|
| 441 |
-
onOpen(b) {
|
| 442 |
-
b.querySelector('#guest-rate-close-btn')?.addEventListener('click', closeModal);
|
| 443 |
-
b.querySelector('#guest-rate-limit-signin').addEventListener('click', () => { closeModal(); openAuthModal('signin'); });
|
| 444 |
-
b.querySelector('#guest-rate-limit-close').addEventListener('click', () => { closeModal(); });
|
| 445 |
-
}
|
| 446 |
-
});
|
| 447 |
-
}
|
| 448 |
-
|
| 449 |
-
// ── Device session detail modal ───────────────────────────────────────────
|
| 450 |
-
// Now opens as a SECONDARY modal (stacked on top of settings)
|
| 451 |
-
|
| 452 |
-
export function openDeviceSessionModal(session, isCurrentSession) {
|
| 453 |
-
openSecondaryModal(`
|
| 454 |
-
<div class="modal-header">
|
| 455 |
-
<span class="modal-title">Session Details</span>
|
| 456 |
-
<button class="modal-close" id="dev-sess-close-btn">×</button>
|
| 457 |
-
</div>
|
| 458 |
-
<div class="modal-body">
|
| 459 |
-
<div class="form-group">
|
| 460 |
-
<div class="form-label">IP Address</div>
|
| 461 |
-
<div style="font-size:14px;">${escHtml(session.ip || 'Unknown')}</div>
|
| 462 |
-
</div>
|
| 463 |
-
<div class="form-group">
|
| 464 |
-
<div class="form-label">Last seen</div>
|
| 465 |
-
<div style="font-size:14px;">${escHtml(session.lastSeen ? new Date(session.lastSeen).toLocaleString() : '—')}</div>
|
| 466 |
-
</div>
|
| 467 |
-
<div class="form-group">
|
| 468 |
-
<div class="form-label">First seen</div>
|
| 469 |
-
<div style="font-size:14px;">${escHtml(session.createdAt ? new Date(session.createdAt).toLocaleString() : '—')}</div>
|
| 470 |
-
</div>
|
| 471 |
-
<div class="form-group">
|
| 472 |
-
<div class="form-label">User Agent</div>
|
| 473 |
-
<div style="font-size:12px;word-break:break-all;color:var(--text-dim);">${escHtml(session.userAgent || 'Unknown')}</div>
|
| 474 |
-
</div>
|
| 475 |
-
${isCurrentSession ? '<div style="font-size:12px;color:var(--plan-core);margin-top:4px;">This is your current session.</div>' : ''}
|
| 476 |
-
</div>
|
| 477 |
-
<div class="modal-footer">
|
| 478 |
-
<button class="btn-ghost" id="dev-sess-cancel-btn">Close</button>
|
| 479 |
-
${!isCurrentSession ? `<button class="btn-danger" id="revoke-session-btn">Log Out This Session</button>` : ''}
|
| 480 |
-
</div>
|
| 481 |
-
`, {
|
| 482 |
-
onOpen(b) {
|
| 483 |
-
b.querySelector('#dev-sess-close-btn')?.addEventListener('click', closeSecondaryModal);
|
| 484 |
-
b.querySelector('#dev-sess-cancel-btn')?.addEventListener('click', closeSecondaryModal);
|
| 485 |
-
if (!isCurrentSession) {
|
| 486 |
-
b.querySelector('#revoke-session-btn')?.addEventListener('click', () => {
|
| 487 |
-
send({ type: 'account:revokeSession', token: session.token });
|
| 488 |
-
closeSecondaryModal();
|
| 489 |
-
});
|
| 490 |
-
}
|
| 491 |
-
}
|
| 492 |
-
});
|
| 493 |
-
}
|
| 494 |
-
|
| 495 |
-
// ── Pasted content editor ─────────────────────────────────────────────────
|
| 496 |
-
|
| 497 |
-
export function openPasteEditor(content, onSave) {
|
| 498 |
-
openFileViewerModal({ name: 'Edit Content', content, editable: true, onSave });
|
| 499 |
-
}
|
| 500 |
-
|
| 501 |
-
// Auto-handle limit events
|
| 502 |
-
on('chat:limitReached', () => openLimitModal());
|
| 503 |
-
on('guest:rateLimit', () => openGuestRateLimitModal());
|
|
|
|
| 1 |
+
import crypto from 'crypto';
|
| 2 |
+
import fs from 'fs/promises';
|
| 3 |
+
import path from 'path';
|
| 4 |
+
|
| 5 |
+
const ALGORITHM = 'aes-256-gcm';
|
| 6 |
+
const KEY_LENGTH = 32; // 256 bits
|
| 7 |
+
const IV_LENGTH = 16; // 128 bits for GCM
|
| 8 |
+
const AUTH_TAG_LENGTH = 16; // 128 bits
|
| 9 |
+
|
| 10 |
+
// Derive key from environment variable
|
| 11 |
+
function getKey() {
|
| 12 |
+
const keyEnv = process.env.DATA_ENCRYPTION_KEY;
|
| 13 |
+
if (!keyEnv) throw new Error('DATA_ENCRYPTION_KEY environment variable not set');
|
| 14 |
+
// Use SHA-256 to derive a 32-byte key from the env var
|
| 15 |
+
return crypto.createHash('sha256').update(keyEnv).digest();
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
export function encryptJson(data) {
|
| 19 |
+
const key = getKey();
|
| 20 |
+
const iv = crypto.randomBytes(IV_LENGTH);
|
| 21 |
+
const cipher = crypto.createCipher(ALGORITHM, key);
|
| 22 |
+
cipher.setAAD(Buffer.from('')); // Optional AAD
|
| 23 |
+
|
| 24 |
+
let encrypted = cipher.update(JSON.stringify(data), 'utf8', 'hex');
|
| 25 |
+
encrypted += cipher.final('hex');
|
| 26 |
+
|
| 27 |
+
const authTag = cipher.getAuthTag();
|
| 28 |
+
return {
|
| 29 |
+
iv: iv.toString('hex'),
|
| 30 |
+
encrypted,
|
| 31 |
+
authTag: authTag.toString('hex'),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
}
|
| 34 |
|
| 35 |
+
export function decryptJson(encryptedData) {
|
| 36 |
+
const key = getKey();
|
| 37 |
+
const { iv, encrypted, authTag } = encryptedData;
|
| 38 |
+
const decipher = crypto.createDecipher(ALGORITHM, key);
|
| 39 |
+
decipher.setAuthTag(Buffer.from(authTag, 'hex'));
|
| 40 |
+
decipher.setAAD(Buffer.from('')); // Match AAD
|
| 41 |
|
| 42 |
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
| 43 |
+
decrypted += decipher.final('utf8');
|
| 44 |
+
return JSON.parse(decrypted);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
}
|
| 46 |
|
| 47 |
+
export async function saveEncryptedJson(filePath, data) {
|
| 48 |
+
const encrypted = encryptJson(data);
|
| 49 |
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
| 50 |
+
await fs.writeFile(filePath, JSON.stringify(encrypted));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
|
| 53 |
+
export async function loadEncryptedJson(filePath) {
|
| 54 |
+
try {
|
| 55 |
+
const content = await fs.readFile(filePath, 'utf8');
|
| 56 |
+
const encrypted = JSON.parse(content);
|
| 57 |
+
return decryptJson(encrypted);
|
| 58 |
+
} catch (err) {
|
| 59 |
+
if (err.code === 'ENOENT') return null; // File not found
|
| 60 |
+
throw err;
|
| 61 |
+
}
|
| 62 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|