diff --git a/the-matrix/js/edge-worker.js b/the-matrix/js/edge-worker.js index d65e221..fd5e178 100644 --- a/the-matrix/js/edge-worker.js +++ b/the-matrix/js/edge-worker.js @@ -11,9 +11,23 @@ * Lifecycle events (no id): * { type: 'ready' } — both models loaded and warm * { type: 'error', message } — fatal model-load failure + * + * Model caching: + * @xenova/transformers v2 caches model weights in the browser's Cache API + * (via fetch() → opaque cache). After the first load (~80 MB combined), + * subsequent page loads serve models from the cache without network round-trips. + * We configure useBrowserCache: true (the default) and disable the filesystem + * backend so only the browser cache is used. The existing service worker at + * sw.js uses a cache-first strategy that extends coverage to these assets. */ -import { pipeline } from '@xenova/transformers'; +import { pipeline, env } from '@xenova/transformers'; + +// ── Transformers.js caching config ─────────────────────────────────────────── +// Use browser Cache API for model weights (default behaviour, made explicit). +// Disable Node.js filesystem path so it falls back to browser cache only. +env.useBrowserCache = true; // cache model weights via browser Cache API +env.allowLocalModels = false; // no filesystem — browser-only environment const LOCAL_LABELS = ['greeting', 'small-talk', 'simple-question']; const SERVER_LABELS = ['technical-task', 'creative-work', 'complex-question', 'code-request']; diff --git a/the-matrix/js/nostr-identity.js b/the-matrix/js/nostr-identity.js index c3d5eeb..a71291e 100644 --- a/the-matrix/js/nostr-identity.js +++ b/the-matrix/js/nostr-identity.js @@ -18,6 +18,7 @@ import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure'; import { hexToBytes, bytesToHex } from '@noble/hashes/utils.js'; +import { decode as nip19Decode } from 'nostr-tools/nip19'; const LS_KEYPAIR_KEY = 'timmy_nostr_keypair_v1'; const LS_TOKEN_KEY = 'timmy_nostr_token_v1'; @@ -213,15 +214,127 @@ function _updateIdentityHUD() { // ── Keypair helpers ─────────────────────────────────────────────────────────── -function _loadKeypair() { +/** + * _scanExistingNostrKeys — looks for pre-existing Nostr keys in localStorage + * using common patterns used by Nostr clients: + * - Our own LS_KEYPAIR_KEY format { pubkey, privkey } + * - npub1... / nsec1... bech32 strings stored under common key names + * - Raw 64-char hex pubkeys / privkeys + * + * Returns { pubkey: hex, privkey: hex|null } or null if nothing found. + * Does NOT generate new keys — only discovers existing ones. + */ +function _scanExistingNostrKeys() { + // 1. Our own format first try { const stored = localStorage.getItem(LS_KEYPAIR_KEY); - if (!stored) return null; - const { pubkey } = JSON.parse(stored); - return pubkey ?? null; - } catch { - return null; + if (stored) { + const parsed = JSON.parse(stored); + if (parsed?.pubkey) return { pubkey: parsed.pubkey, privkey: parsed.privkey ?? null }; + } + } catch { /* corrupt */ } + + // 2. Common key names used by Nostr clients (nos2x-style, nostrid, Alby export, etc.) + const COMMON_LS_KEYS = [ + 'nostr_privkey', 'privkey', 'nsec', 'npub', + 'nostr-privkey', 'nostr-nsec', 'nostr_nsec', + 'nostr_keys', 'nostrKeys', 'nostr:privkey', + // nos2x stores its key here: + 'nostr-extension:privkey', + ]; + + for (const key of COMMON_LS_KEYS) { + try { + const val = localStorage.getItem(key); + if (!val) continue; + + // Try bech32 nsec1... + if (val.startsWith('nsec1')) { + try { + const decoded = nip19Decode(val); + if (decoded.type === 'nsec') { + const privBytes = decoded.data; + const pubkeyHex = getPublicKey(privBytes); + const privkeyHex = bytesToHex(privBytes); + // Save in our format for subsequent loads + _saveDiscoveredKeypair(pubkeyHex, privkeyHex); + console.info(`[nostr] Discovered nsec key at localStorage['${key}']`); + return { pubkey: pubkeyHex, privkey: privkeyHex }; + } + } catch { /* invalid bech32 */ } + } + + // Try bech32 npub1... (pubkey only — no signing possible without privkey) + if (val.startsWith('npub1')) { + try { + const decoded = nip19Decode(val); + if (decoded.type === 'npub') { + console.info(`[nostr] Discovered npub key at localStorage['${key}'] (view-only)`); + return { pubkey: decoded.data, privkey: null }; + } + } catch { /* invalid */ } + } + + // Try raw hex privkey (64 chars) + if (/^[0-9a-f]{64}$/.test(val)) { + try { + const privBytes = hexToBytes(val); + const pubkeyHex = getPublicKey(privBytes); + _saveDiscoveredKeypair(pubkeyHex, val); + console.info(`[nostr] Discovered hex privkey at localStorage['${key}']`); + return { pubkey: pubkeyHex, privkey: val }; + } catch { /* bad key */ } + } + + // Try JSON objects with common shapes + if (val.startsWith('{')) { + try { + const obj = JSON.parse(val); + const nsec = obj.nsec || obj.privkey || obj.private_key || obj.secret; + const npub = obj.npub || obj.pubkey || obj.public_key; + if (nsec && nsec.startsWith('nsec1')) { + const decoded = nip19Decode(nsec); + if (decoded.type === 'nsec') { + const privBytes = decoded.data; + const pubkeyHex = getPublicKey(privBytes); + const privkeyHex = bytesToHex(privBytes); + _saveDiscoveredKeypair(pubkeyHex, privkeyHex); + console.info(`[nostr] Discovered nsec in JSON at localStorage['${key}']`); + return { pubkey: pubkeyHex, privkey: privkeyHex }; + } + } + if (npub && npub.startsWith('npub1')) { + const decoded = nip19Decode(npub); + if (decoded.type === 'npub') { + return { pubkey: decoded.data, privkey: null }; + } + } + if (typeof obj.pubkey === 'string' && /^[0-9a-f]{64}$/.test(obj.pubkey)) { + const pkey = obj.privkey ?? obj.private_key ?? null; + return { pubkey: obj.pubkey, privkey: pkey }; + } + } catch { /* bad JSON */ } + } + } catch { /* localStorage access failed */ } } + + return null; +} + +function _saveDiscoveredKeypair(pubkeyHex, privkeyHex) { + try { + localStorage.setItem(LS_KEYPAIR_KEY, JSON.stringify({ pubkey: pubkeyHex, privkey: privkeyHex })); + } catch { /* storage full */ } +} + +function _loadKeypair() { + const found = _scanExistingNostrKeys(); + if (found) { + // If only npub found (no privkey), store pubkey but signing won't work + // (the prompt will offer extension or key generation for signing) + return found.pubkey; + } + return null; } function _generateAndSaveKeypair() { diff --git a/the-matrix/js/ui.js b/the-matrix/js/ui.js index 1d19713..c824102 100644 --- a/the-matrix/js/ui.js +++ b/the-matrix/js/ui.js @@ -94,9 +94,12 @@ async function _fetchEstimate(text) { try { const token = await getOrRefreshToken('/api'); const params = new URLSearchParams({ request: text }); - if (token) params.set('nostr_token', token); + const fetchOpts = {}; + if (token) { + fetchOpts.headers = { 'X-Nostr-Token': token }; + } - const res = await fetch(`/api/estimate?${params}`); + const res = await fetch(`/api/estimate?${params}`, fetchOpts); if (!res.ok) return; const data = await res.json();