incognitolm commited on
Commit
3eeb0cf
·
verified ·
1 Parent(s): f3eb8b7

Update server/cryptoUtils.js

Browse files
Files changed (1) hide show
  1. server/cryptoUtils.js +54 -495
server/cryptoUtils.js CHANGED
@@ -1,503 +1,62 @@
1
- // modals.js All modal dialogs
2
- import { send, on } from './ws.js';
3
- import { escHtml } from './ui.js';
4
- import { isAuthenticated, loginWithEmail, signUpWithEmail, loginWithOAuth, logout, currentUser, userProfile, userSettings } from './auth.js';
5
-
6
- // ── Modal stack support ───────────────────────────────────────────────────
7
- // Primary modal uses #modal-overlay / #modal-box.
8
- // Secondary (stacked) modal creates its own overlay on top.
9
-
10
- let overlay, box;
11
- function getOverlay() { return overlay || (overlay = document.getElementById('modal-overlay')); }
12
- function getBox() { return box || (box = document.getElementById('modal-box')); }
13
-
14
- let secondaryOverlay = null;
15
-
16
- export function openModal(html, opts = {}) {
17
- const o = getOverlay(), b = getBox();
18
- b.className = 'modal-box' + (opts.wide ? ' wide' : '');
19
- b.innerHTML = html;
20
- o.classList.remove('hidden');
21
- if (opts.onOpen) opts.onOpen(b);
22
- o.onclick = (e) => { if (e.target === o) closeModal(); };
23
- document.addEventListener('keydown', escHandler);
24
- }
25
-
26
- const escHandler = (e) => { if (e.key === 'Escape') { if (secondaryOverlay) closeSecondaryModal(); else closeModal(); } };
27
-
28
- export function closeModal() {
29
- if (secondaryOverlay) closeSecondaryModal();
30
- getOverlay().classList.add('hidden');
31
- getBox().innerHTML = '';
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
- // ── Image modal ───────────────────────────────────────────────────────────
 
 
 
 
 
323
 
324
- export function openImageModal(src) {
325
- openModal(`
326
- <div class="modal-header" style="border-bottom:none;">
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
- // ── File viewer modal ─────────────────────────────────────────────────────
351
-
352
- /**
353
- * Opens a modal to view/edit a text file attachment.
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 openGuestRateLimitModal() {
423
- openModal(`
424
- <div class="modal-header">
425
- <span class="modal-title">Unusual request activity detected</span>
426
- <button class="modal-close" id="guest-rate-close-btn">×</button>
427
- </div>
428
- <div class="modal-body" style="padding-top:18px;">
429
- <div class="limit-modal-inner">
430
- <div class="limit-title">Please sign in to continue</div>
431
- <div class="limit-desc" style="margin-top:10px;line-height:1.5;">
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
+ }