oxedyne/daimond/dev/verify_lockkeys.mjs
5.5 KiB, 1 run
created by r2519314175:513, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | // verify_lockkeys.mjs — a locked Daimond holds no readable key. |
| 2 | // |
| 3 | // That is the contract, and it used to be kept by one line: `cfg.apiKey = ''`, because there was |
| 4 | // one key and it lived there. There are now a key per PROVIDER, held in memory by DaimondModels, |
| 5 | // and a built agent for every chat and every Diamond with its key already handed to the wasm. |
| 6 | // `DaimondModels.lock()` was written to forget the first of those -- and never called. |
| 7 | // |
| 8 | // So the lock is tested from the outside: after locking, can anything still name a key, and can |
| 9 | // any built agent still reach a provider? Asking the app whether it FEELS locked would prove |
| 10 | // nothing; the question is whether the key is gone. |
| 11 | import { open, chat, errors } from './harness.mjs'; |
| 12 | import http from 'node:http'; |
| 13 | |
| 14 | const ok = [], bad = []; |
| 15 | const check = (name, pass, detail) => { |
| 16 | (pass ? ok : bad).push(name + (detail ? ' — ' + detail : '')); |
| 17 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 18 | }; |
| 19 | |
| 20 | // A provider that records every request it is sent. |
| 21 | const seen = []; |
| 22 | const srv = http.createServer((req, res) => { |
| 23 | if (req.method === 'OPTIONS') { |
| 24 | res.writeHead(204, { |
| 25 | 'access-control-allow-origin': '*', |
| 26 | 'access-control-allow-headers': '*', |
| 27 | 'access-control-allow-methods': 'POST, GET, OPTIONS', |
| 28 | }); |
| 29 | return res.end(); |
| 30 | } |
| 31 | let body = ''; |
| 32 | req.on('data', c => (body += c)); |
| 33 | req.on('end', () => { |
| 34 | if (req.url.endsWith('/models')) { |
| 35 | res.writeHead(200, { 'content-type': 'application/json', 'access-control-allow-origin': '*' }); |
| 36 | return res.end(JSON.stringify({ data: [{ id: 'test-model' }] })); |
| 37 | } |
| 38 | seen.push(req.headers.authorization || ''); |
| 39 | res.writeHead(200, { 'content-type': 'application/json', 'access-control-allow-origin': '*' }); |
| 40 | res.end(JSON.stringify({ |
| 41 | choices: [{ message: { role: 'assistant', content: 'hi' }, finish_reason: 'stop' }], |
| 42 | usage: { prompt_tokens: 1, completion_tokens: 1 }, |
| 43 | })); |
| 44 | }); |
| 45 | }); |
| 46 | srv.listen(9121, '127.0.0.1'); |
| 47 | const URL = 'http://127.0.0.1:9121/v1/chat/completions'; |
| 48 | |
| 49 | const s = await open({ name: 'lockkeys', connect: false }); |
| 50 | const p = s.page; |
| 51 | await p.waitForTimeout(1200); |
| 52 | |
| 53 | // A provider with a key, sealed under the passphrase, and a chat running on it. |
| 54 | await p.evaluate(async (u) => { |
| 55 | const M = window.DaimondModels; |
| 56 | M.addProvider('secret', { name: 'Secret Co', url: u }); |
| 57 | await M.setKey('secret', 'SUPERSECRET-KEY'); |
| 58 | await M.fetchModels('secret'); |
| 59 | M.setDefault('secret', 'test-model'); |
| 60 | }, URL); |
| 61 | await p.waitForTimeout(500); |
| 62 | |
| 63 | await chat(s, 'hello'); |
| 64 | check('while unlocked, the chat reaches the provider with its key', |
| 65 | seen.length === 1 && seen[0].includes('SUPERSECRET-KEY'), seen[0] || '(nothing sent)'); |
| 66 | |
| 67 | const unlocked = await p.evaluate(() => ({ |
| 68 | key: window.DaimondModels.keyFor('secret'), |
| 69 | apps: (window.__daimondChats || []).length, // not exposed; only the key matters here |
| 70 | })); |
| 71 | check('and the key is readable, as it must be to work', |
| 72 | unlocked.key === 'SUPERSECRET-KEY'); |
| 73 | |
| 74 | // ── Lock ─────────────────────────────────────────────────────────────── |
| 75 | |
| 76 | await p.evaluate(() => { |
| 77 | // The lock is "Log out" in the user menu. Drive the real one. |
| 78 | const row = document.querySelector('.user-row'); |
| 79 | if (row) row.click(); |
| 80 | }); |
| 81 | await p.waitForTimeout(400); |
| 82 | const locked = await p.evaluate(async () => { |
| 83 | const item = [...document.querySelectorAll('button, .menu-item, [role="menuitem"]')] |
| 84 | .find(b => /log out/i.test(b.textContent || '')); |
| 85 | if (!item) return { drove: false }; |
| 86 | item.click(); |
| 87 | await new Promise(r => setTimeout(r, 900)); |
| 88 | return { |
| 89 | drove: true, |
| 90 | key: window.DaimondModels.keyFor('secret'), |
| 91 | sealed: window.DaimondModels.isSealed('secret'), |
| 92 | hasKey: window.DaimondModels.hasKey('secret'), |
| 93 | gateUp: getComputedStyle(document.getElementById('identity-modal')).display !== 'none', |
| 94 | }; |
| 95 | }); |
| 96 | check('the lock is reachable from the user menu', locked.drove === true); |
| 97 | check('after locking, no plaintext key can be named', |
| 98 | locked.key === '', locked.key ? 'STILL READABLE: ' + locked.key : '(gone)'); |
| 99 | check('the key is still THERE, just sealed — locking is not deleting', |
| 100 | locked.hasKey === true && locked.sealed === true, |
| 101 | 'hasKey=' + locked.hasKey + ' sealed=' + locked.sealed); |
| 102 | check('and the passphrase gate is up', locked.gateUp === true); |
| 103 | |
| 104 | // The sharper test: an agent built BEFORE the lock had the key handed to it. If that agent |
| 105 | // survived, the lock is cosmetic — the key is still in the wasm and still reaches the provider. |
| 106 | const before = seen.length; |
| 107 | const survivor = await p.evaluate(async () => { |
| 108 | // Try to make the app that was serving the chat take another turn. If the lock nulled it, |
| 109 | // there is nothing here to run. |
| 110 | const input = document.getElementById('chat-input'); |
| 111 | if (input && input.offsetParent !== null) { |
| 112 | input.value = 'again'; |
| 113 | document.getElementById('chat-send').click(); |
| 114 | await new Promise(r => setTimeout(r, 2500)); |
| 115 | return 'the composer is still live'; |
| 116 | } |
| 117 | return 'no composer while locked'; |
| 118 | }); |
| 119 | await p.waitForTimeout(1500); |
| 120 | check('no agent built before the lock can still reach the provider', |
| 121 | seen.length === before, `${seen.length - before} request(s) after locking — ${survivor}`); |
| 122 | |
| 123 | const errs = errors(s).filter(e => !/favicon|404|401|502|Bad Gateway|net::ERR/.test(e)); |
| 124 | check('nothing throws while locking', errs.length === 0, errs[0] || ''); |
| 125 | |
| 126 | await s.close(); |
| 127 | srv.close(); |
| 128 | console.log(`\n${ok.length} passed, ${bad.length} failed`); |
| 129 | if (bad.length) console.log('FAILED:\n ' + bad.join('\n ')); |
| 130 | process.exit(bad.length ? 1 : 0); |