spb/zyquo-cloud-web Public MIT
Zyquo Cloud Web — every cloud model, one beautiful chat, entirely in your browser.
TypeScript 81.9%
CSS 8.9%
JavaScript 7.5%
Shell 1.1%
HTML 0.6%
1/*2 * gate-phase3.mjs3 * Zyquo Cloud Web4 *5 * Author: Simon-Pierre Boucher6 * Mail: contact@spboucher.ai7 *8 * Phase 3 gate: in a real browser, verify that (1) keys + conversations9 * survive a page reload AND a full browser restart (fresh context, same10 * storage dir), (2) export → clear → import restores exactly, (3) the11 * passphrase vault encrypts at rest and unlocks correctly.12 * Usage: node scripts/gate-phase3.mjs [baseURL]13 */1415import { chromium } from 'playwright'16import { mkdtempSync } from 'node:fs'17import { tmpdir } from 'node:os'18import { join } from 'node:path'1920const baseURL = process.argv[2] ?? 'http://localhost:5173'21const userDataDir = mkdtempSync(join(tmpdir(), 'zyquo-gate3-'))22const results = []23const check = (name, ok) => {24 results.push([name, ok])25 console.log(`${ok ? 'PASS' : 'FAIL'} ${name}`)26}2728const conversationFixture = {29 id: 'gate3-conv',30 title: 'Gate 3 conversation',31 createdAt: 1000,32 updatedAt: 2000,33 modelID: 'gpt-5.6-terra',34 provider: 'openai',35 parameters: { temperature: 0.7 },36 messages: [37 { id: 'm1', role: 'user', text: 'hello persistence', createdAt: 1000 },38 { id: 'm2', role: 'assistant', text: 'hello back', createdAt: 1500, usage: { inputTokens: 3, outputTokens: 2 } },39 ],40 pinned: true,41 hasAutoTitle: false,42}4344// --- Session 1: write data ---45let browser = await chromium.launchPersistentContext(userDataDir, { baseURL })46let page = await browser.newPage()47await page.goto(baseURL)48await page.evaluate(async (conv) => {49 const z = window.__zyquo50 z.keys.setKey('openai', 'sk-test-persistence-1234')51 z.settings.saveSettings({ ...z.settings.loadSettings(), chatFontSize: 15, theme: 'light' })52 await z.conversations.saveConversation(conv)53}, conversationFixture)5455// Reload (same session)56await page.reload()57const afterReload = await page.evaluate(async () => {58 const z = window.__zyquo59 const convs = await z.conversations.loadConversations()60 return {61 key: z.keys.getKey('openai'),62 fontSize: z.settings.loadSettings().chatFontSize,63 conv: convs.find((c) => c.id === 'gate3-conv') ?? null,64 }65})66check('keys survive reload', afterReload.key === 'sk-test-persistence-1234')67check('settings survive reload', afterReload.fontSize === 15)68check(69 'conversation survives reload (full round-trip)',70 afterReload.conv !== null &&71 afterReload.conv.messages.length === 2 &&72 afterReload.conv.messages[1].usage.outputTokens === 2 &&73 afterReload.conv.pinned === true74)7576// --- Browser restart (close everything, reopen same profile) ---77await browser.close()78browser = await chromium.launchPersistentContext(userDataDir, { baseURL })79page = await browser.newPage()80await page.goto(baseURL)81const afterRestart = await page.evaluate(async () => {82 const z = window.__zyquo83 const convs = await z.conversations.loadConversations()84 return { key: z.keys.getKey('openai'), count: convs.length, title: convs[0]?.title }85})86check('keys survive browser restart', afterRestart.key === 'sk-test-persistence-1234')87check(88 'conversations survive browser restart',89 afterRestart.count === 1 && afterRestart.title === 'Gate 3 conversation'90)9192// --- Export → clear → import ---93const roundTrip = await page.evaluate(async () => {94 const z = window.__zyquo95 const exported = await z.backup.exportAll(true)96 await z.backup.clearEverything()97 const emptyAfterClear =98 (await z.conversations.loadConversations()).length === 0 && z.keys.getKey('openai') === undefined99 const imported = await z.backup.importAll(exported)100 const convs = await z.conversations.loadConversations()101 return {102 emptyAfterClear,103 imported,104 key: z.keys.getKey('openai'),105 conv: convs.find((c) => c.id === 'gate3-conv') ?? null,106 fontSize: z.settings.loadSettings().chatFontSize,107 }108})109check('clear empties everything', roundTrip.emptyAfterClear)110check(111 'export→clear→import restores exactly',112 roundTrip.key === 'sk-test-persistence-1234' &&113 roundTrip.conv !== null &&114 roundTrip.conv.messages.length === 2 &&115 roundTrip.fontSize === 15 &&116 roundTrip.imported.conversations === 1117)118119// --- Passphrase vault ---120const vaultResult = await page.evaluate(async () => {121 const z = window.__zyquo122 await z.vault.enableVault('correct horse battery staple')123 const raw = z.keys.rawKeysRecord()124 const encryptedAtRest = raw.includes('__zyquoVault') && !raw.includes('sk-test-persistence-1234')125 const readableWhileUnlocked = z.keys.getKey('openai') === 'sk-test-persistence-1234'126 z.vault.lockVault()127 const lockedNow = z.vault.isLocked()128 let wrongPassFailed = false129 try {130 await z.vault.unlockVault('wrong passphrase')131 } catch {132 wrongPassFailed = true133 }134 await z.vault.unlockVault('correct horse battery staple')135 const unlockedKey = z.keys.getKey('openai')136 await z.vault.disableVault('correct horse battery staple')137 const plaintextAgain = z.keys.rawKeysRecord().includes('sk-test-persistence-1234')138 return { encryptedAtRest, readableWhileUnlocked, lockedNow, wrongPassFailed, unlockedKey, plaintextAgain }139})140check('vault encrypts keys at rest (AES-GCM)', vaultResult.encryptedAtRest)141check('keys readable while unlocked', vaultResult.readableWhileUnlocked)142check('lock drops the in-memory overlay', vaultResult.lockedNow)143check('wrong passphrase rejected', vaultResult.wrongPassFailed)144check('correct passphrase unlocks', vaultResult.unlockedKey === 'sk-test-persistence-1234')145check('disable restores plaintext', vaultResult.plaintextAgain)146147await browser.close()148149const failures = results.filter(([, ok]) => !ok)150if (failures.length === 0) {151 console.log(`GATE PASS: ${results.length}/${results.length} persistence checks green`)152} else {153 console.error(`GATE FAIL: ${failures.length} failures`)154 process.exit(1)155}156