Some checks failed
CI / validate (pull_request) Has been cancelled
- edge-intelligence.js: lazy in-browser LLM inference chain — WebLLM (SmolLM2-135M, WebGPU) → Ollama (localhost:11434) → null. Nothing downloads until the user clicks the HUD button. All inference is async; Three.js render loop never blocked. - nostr-identity.js: silent Nostr keypair on first visit. Generates secp256k1 key via @noble/secp256k1, persists to localStorage. Signs NIP-01 events locally — zero extension popup. Detects and prefers NIP-07 extension when available. - app.js: chat pipeline now tries edgeQuery() first, then local fallbacks. Animated thinking cursor while inference runs. Nostr identity initialised at startup; npub badge shown in HUD. Edge AI status badge wired to HUD button. - index.html + style.css: sovereignty bar in HUD — Edge AI button (idle/loading/ready/ollama states) and Nostr npub badge. Fixes #15 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
144 lines
5.3 KiB
JavaScript
144 lines
5.3 KiB
JavaScript
// ═══════════════════════════════════════════
|
|
// NOSTR IDENTITY — Silent Signing, No Extension Popup
|
|
// ═══════════════════════════════════════════
|
|
// Generates a secp256k1 keypair on first visit and persists it in localStorage.
|
|
// Signs NIP-01 events locally — no window.nostr extension required.
|
|
// If window.nostr (NIP-07) is present it is preferred; our key acts as fallback.
|
|
|
|
const NOBLE_SECP_CDN = 'https://cdn.jsdelivr.net/npm/@noble/secp256k1@2.1.0/+esm';
|
|
const LS_KEY = 'nexus-nostr-privkey';
|
|
|
|
// ─── module state ────────────────────────────────────────────────────────────
|
|
let _privKeyBytes = null; // Uint8Array (32 bytes)
|
|
let _pubKeyHex = null; // hex string (32 bytes / x-only)
|
|
let _usingNip07 = false;
|
|
let _secp = null; // lazy-loaded @noble/secp256k1
|
|
|
|
// ─── helpers ─────────────────────────────────────────────────────────────────
|
|
|
|
function hexToBytes(hex) {
|
|
const b = new Uint8Array(hex.length / 2);
|
|
for (let i = 0; i < hex.length; i += 2) b[i / 2] = parseInt(hex.slice(i, i + 2), 16);
|
|
return b;
|
|
}
|
|
|
|
function bytesToHex(bytes) {
|
|
return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
|
|
async function sha256Hex(data) {
|
|
const buf = await crypto.subtle.digest('SHA-256', data);
|
|
return bytesToHex(new Uint8Array(buf));
|
|
}
|
|
|
|
async function loadSecp() {
|
|
if (!_secp) _secp = await import(NOBLE_SECP_CDN);
|
|
return _secp;
|
|
}
|
|
|
|
/** Serialize a Nostr event to canonical JSON bytes for ID hashing (NIP-01). */
|
|
function serializeEvent(ev) {
|
|
const arr = [0, ev.pubkey, ev.created_at, ev.kind, ev.tags, ev.content];
|
|
return new TextEncoder().encode(JSON.stringify(arr));
|
|
}
|
|
|
|
// ─── public API ──────────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Initialise the identity.
|
|
* Loads or generates a keypair; checks for NIP-07 extension.
|
|
* @returns {{ pubKey: string, npub: string, source: 'nip07'|'local' }}
|
|
*/
|
|
export async function init() {
|
|
const secp = await loadSecp();
|
|
|
|
// Load or generate local keypair
|
|
const stored = localStorage.getItem(LS_KEY);
|
|
if (stored && /^[0-9a-f]{64}$/i.test(stored)) {
|
|
_privKeyBytes = hexToBytes(stored);
|
|
} else {
|
|
_privKeyBytes = secp.utils.randomPrivateKey();
|
|
localStorage.setItem(LS_KEY, bytesToHex(_privKeyBytes));
|
|
}
|
|
|
|
// x-only Schnorr public key (32 bytes)
|
|
_pubKeyHex = bytesToHex(secp.schnorr.getPublicKey(_privKeyBytes));
|
|
|
|
let source = 'local';
|
|
|
|
// Prefer NIP-07 extension when available (no popup needed for getPublicKey)
|
|
if (window.nostr) {
|
|
try {
|
|
const ext = await window.nostr.getPublicKey();
|
|
if (ext && /^[0-9a-f]{64}$/i.test(ext)) {
|
|
_pubKeyHex = ext;
|
|
_usingNip07 = true;
|
|
source = 'nip07';
|
|
}
|
|
} catch {
|
|
// Extension rejected or errored — fall back to local key
|
|
}
|
|
}
|
|
|
|
return { pubKey: _pubKeyHex, npub: getNpub(), source };
|
|
}
|
|
|
|
/** Hex public key (x-only / 32 bytes). Null until init() resolves. */
|
|
export function getPublicKey() { return _pubKeyHex; }
|
|
|
|
/**
|
|
* Human-readable abbreviated npub for HUD display.
|
|
* We show a truncated hex with an npub… prefix to avoid a full bech32 dep.
|
|
*/
|
|
export function getNpub() {
|
|
if (!_pubKeyHex) return null;
|
|
return 'npub…' + _pubKeyHex.slice(-8);
|
|
}
|
|
|
|
/**
|
|
* Sign a Nostr event (NIP-01).
|
|
* Fills in id and sig. Uses NIP-07 extension if it was detected at init time,
|
|
* otherwise signs locally with our generated key — zero popups either way.
|
|
*
|
|
* @param {{ kind?: number, tags?: string[][], content: string, created_at?: number }} partial
|
|
* @returns {Promise<object>} Complete signed event
|
|
*/
|
|
export async function signEvent(partial) {
|
|
const event = {
|
|
pubkey: _pubKeyHex,
|
|
created_at: partial.created_at ?? Math.floor(Date.now() / 1000),
|
|
kind: partial.kind ?? 1,
|
|
tags: partial.tags ?? [],
|
|
content: partial.content ?? '',
|
|
};
|
|
|
|
event.id = await sha256Hex(serializeEvent(event));
|
|
|
|
// Try NIP-07 first when present (it signed getPublicKey silently, so it may
|
|
// also sign events silently depending on the extension configuration)
|
|
if (_usingNip07 && window.nostr) {
|
|
try {
|
|
return await window.nostr.signEvent(event);
|
|
} catch {
|
|
// Extension declined — sign locally as fallback
|
|
}
|
|
}
|
|
|
|
// Local Schnorr signing with @noble/secp256k1
|
|
const secp = await loadSecp();
|
|
const sig = secp.schnorr.sign(hexToBytes(event.id), _privKeyBytes);
|
|
event.sig = bytesToHex(sig);
|
|
return event;
|
|
}
|
|
|
|
/**
|
|
* Replace the stored private key (e.g. imported from another client).
|
|
* The module must be re-initialised (call init() again) after importing.
|
|
* @param {string} hexPrivKey 64-char hex string
|
|
*/
|
|
export function importKey(hexPrivKey) {
|
|
if (!/^[0-9a-f]{64}$/i.test(hexPrivKey)) throw new Error('Invalid private key: expected 64 hex chars');
|
|
localStorage.setItem(LS_KEY, hexPrivKey.toLowerCase());
|
|
// Caller should await init() to refresh public key
|
|
}
|