task-28 fix2: common Nostr key discovery, header-only token transport, explicit model caching
1. nostr-identity.js: _scanExistingNostrKeys() discovers pre-existing Nostr keys in localStorage using common patterns: nsec1/npub1 bech32, raw hex privkey, JSON objects with nsec/npub/privkey fields. Scans common client key names (nostr_privkey, privkey, nsec, nostr-nsec, nostrKeys, etc.) before showing the identity prompt. Keys discovered are re-saved in app format for next load. 2. ui.js: _fetchEstimate() now sends nostr_token as X-Nostr-Token header instead of query param, consistent with all other authenticated API calls. 3. edge-worker.js: explicit env.useBrowserCache=true + env.allowLocalModels=false so model weights are cached in browser Cache API after first download.
This commit is contained in:
@@ -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'];
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user