Files
the-nexus/nostr-identity.js
Alexander Whitestone cdefa24111
Some checks failed
CI / validate (pull_request) Has been cancelled
feat: edge intelligence — browser model + silent Nostr signing (#15)
- 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>
2026-03-23 23:24:34 -04:00

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
}