yana-web 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PUBLISH.md +26 -0
- package/README.md +46 -0
- package/auth.js +204 -0
- package/bin/yana.js +20 -0
- package/desktop/app.jsx +240 -0
- package/desktop/chat.jsx +524 -0
- package/desktop/components.jsx +203 -0
- package/desktop/dashboard.jsx +304 -0
- package/desktop/index.html +29 -0
- package/desktop/login.html +474 -0
- package/desktop/spaces.jsx +248 -0
- package/desktop/system.jsx +540 -0
- package/desktop/themes.css +275 -0
- package/desktop/welcome.html +251 -0
- package/logo.png +0 -0
- package/memory.js +112 -0
- package/missions.js +140 -0
- package/mobile/index.html +34 -0
- package/mobile/m-app.jsx +152 -0
- package/mobile/m-chat.jsx +298 -0
- package/mobile/m-garden.jsx +72 -0
- package/mobile/m-lake.jsx +190 -0
- package/mobile/m-missions.jsx +81 -0
- package/mobile/m-shell.jsx +237 -0
- package/mobile/m-system.jsx +355 -0
- package/mobile/mobile.css +180 -0
- package/mobile/themes.css +228 -0
- package/package.json +29 -0
- package/server.js +977 -0
- package/shared/crypto-store.js +160 -0
- package/shared/data.js +154 -0
- package/shared/tweaks-panel.jsx +558 -0
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Yana Vault — provider API keys encrypted at rest (AES-256-GCM, WebCrypto).
|
|
3
|
+
//
|
|
4
|
+
// localStorage holds only ciphertext under "yana.enc.<provider>". The AES key
|
|
5
|
+
// is a NON-EXTRACTABLE CryptoKey persisted in IndexedDB — a localStorage dump,
|
|
6
|
+
// browser-sync backup, or disk inspection never yields usable secrets.
|
|
7
|
+
// Legacy plaintext "yana.key.<provider>" entries are migrated on first load.
|
|
8
|
+
//
|
|
9
|
+
// Exposes window.YanaVault:
|
|
10
|
+
// ready Promise — await before first render
|
|
11
|
+
// getKey(id) sync, from in-memory cache (null if absent)
|
|
12
|
+
// hasKey(id) sync
|
|
13
|
+
// setKey(id, val) async — encrypts + persists
|
|
14
|
+
// removeKey(id) sync — wipes ciphertext + legacy plaintext
|
|
15
|
+
|
|
16
|
+
(function () {
|
|
17
|
+
const DB_NAME = 'yana-vault';
|
|
18
|
+
const STORE = 'keys';
|
|
19
|
+
const MASTER_ID = 'master';
|
|
20
|
+
const ENC_PREFIX = 'yana.enc.';
|
|
21
|
+
const LEGACY_PREFIX = 'yana.key.';
|
|
22
|
+
|
|
23
|
+
const cache = Object.create(null); // id -> plaintext, in-memory only
|
|
24
|
+
let masterKey = null;
|
|
25
|
+
let fallback = false; // plaintext mode when WebCrypto/IDB missing
|
|
26
|
+
|
|
27
|
+
function idbOpen() {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const req = indexedDB.open(DB_NAME, 1);
|
|
30
|
+
req.onupgradeneeded = () => req.result.createObjectStore(STORE);
|
|
31
|
+
req.onsuccess = () => resolve(req.result);
|
|
32
|
+
req.onerror = () => reject(req.error);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function idbGet(db, k) {
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const req = db.transaction(STORE).objectStore(STORE).get(k);
|
|
39
|
+
req.onsuccess = () => resolve(req.result || null);
|
|
40
|
+
req.onerror = () => reject(req.error);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function idbPut(db, k, v) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
const req = db.transaction(STORE, 'readwrite').objectStore(STORE).put(v, k);
|
|
47
|
+
req.onsuccess = () => resolve();
|
|
48
|
+
req.onerror = () => reject(req.error);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function loadMasterKey() {
|
|
53
|
+
const db = await idbOpen();
|
|
54
|
+
let key = await idbGet(db, MASTER_ID);
|
|
55
|
+
if (!key) {
|
|
56
|
+
key = await crypto.subtle.generateKey(
|
|
57
|
+
{ name: 'AES-GCM', length: 256 },
|
|
58
|
+
false, // non-extractable
|
|
59
|
+
['encrypt', 'decrypt']
|
|
60
|
+
);
|
|
61
|
+
await idbPut(db, MASTER_ID, key);
|
|
62
|
+
}
|
|
63
|
+
db.close();
|
|
64
|
+
return key;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function b64(buf) {
|
|
68
|
+
let s = '';
|
|
69
|
+
const bytes = new Uint8Array(buf);
|
|
70
|
+
for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
|
|
71
|
+
return btoa(s);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function unb64(str) {
|
|
75
|
+
const bin = atob(str);
|
|
76
|
+
const out = new Uint8Array(bin.length);
|
|
77
|
+
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function encrypt(plain) {
|
|
82
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
83
|
+
const ct = await crypto.subtle.encrypt(
|
|
84
|
+
{ name: 'AES-GCM', iv }, masterKey, new TextEncoder().encode(plain)
|
|
85
|
+
);
|
|
86
|
+
const packed = new Uint8Array(12 + ct.byteLength);
|
|
87
|
+
packed.set(iv);
|
|
88
|
+
packed.set(new Uint8Array(ct), 12);
|
|
89
|
+
return b64(packed.buffer);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function decrypt(packedB64) {
|
|
93
|
+
const packed = unb64(packedB64);
|
|
94
|
+
const pt = await crypto.subtle.decrypt(
|
|
95
|
+
{ name: 'AES-GCM', iv: packed.slice(0, 12) }, masterKey, packed.slice(12)
|
|
96
|
+
);
|
|
97
|
+
return new TextDecoder().decode(pt);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function listStorageIds(prefix) {
|
|
101
|
+
const ids = [];
|
|
102
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
103
|
+
const k = localStorage.key(i);
|
|
104
|
+
if (k && k.startsWith(prefix)) ids.push(k.slice(prefix.length));
|
|
105
|
+
}
|
|
106
|
+
return ids;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function init() {
|
|
110
|
+
if (!window.crypto || !crypto.subtle || !window.indexedDB) {
|
|
111
|
+
fallback = true;
|
|
112
|
+
} else {
|
|
113
|
+
try { masterKey = await loadMasterKey(); }
|
|
114
|
+
catch (_) { fallback = true; }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (fallback) {
|
|
118
|
+
console.warn('[yana-vault] WebCrypto/IndexedDB unavailable — keys stored in plaintext');
|
|
119
|
+
for (const id of listStorageIds(LEGACY_PREFIX)) {
|
|
120
|
+
cache[id] = localStorage.getItem(LEGACY_PREFIX + id);
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Migrate legacy plaintext entries → encrypted, then wipe plaintext
|
|
126
|
+
for (const id of listStorageIds(LEGACY_PREFIX)) {
|
|
127
|
+
const plain = localStorage.getItem(LEGACY_PREFIX + id);
|
|
128
|
+
if (plain) {
|
|
129
|
+
cache[id] = plain;
|
|
130
|
+
localStorage.setItem(ENC_PREFIX + id, await encrypt(plain));
|
|
131
|
+
}
|
|
132
|
+
localStorage.removeItem(LEGACY_PREFIX + id);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Load existing ciphertext into the in-memory cache
|
|
136
|
+
for (const id of listStorageIds(ENC_PREFIX)) {
|
|
137
|
+
if (cache[id]) continue;
|
|
138
|
+
try { cache[id] = await decrypt(localStorage.getItem(ENC_PREFIX + id)); }
|
|
139
|
+
catch (_) { localStorage.removeItem(ENC_PREFIX + id); } // undecryptable (foreign profile) — drop
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const ready = init();
|
|
144
|
+
|
|
145
|
+
window.YanaVault = {
|
|
146
|
+
ready,
|
|
147
|
+
getKey(id) { return cache[id] || null; },
|
|
148
|
+
hasKey(id) { return !!cache[id]; },
|
|
149
|
+
async setKey(id, value) {
|
|
150
|
+
cache[id] = value;
|
|
151
|
+
if (fallback) { localStorage.setItem(LEGACY_PREFIX + id, value); return; }
|
|
152
|
+
localStorage.setItem(ENC_PREFIX + id, await encrypt(value));
|
|
153
|
+
},
|
|
154
|
+
removeKey(id) {
|
|
155
|
+
delete cache[id];
|
|
156
|
+
localStorage.removeItem(ENC_PREFIX + id);
|
|
157
|
+
localStorage.removeItem(LEGACY_PREFIX + id);
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
})();
|
package/shared/data.js
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Yana AI — runtime data shell (no demo numbers)
|
|
2
|
+
// Every screen reads real sources: /api/status (MANIFEST), /api/dashboard
|
|
3
|
+
// (L1 memory + audit log), /api/agents, /api/memories, /api/skills,
|
|
4
|
+
// /api/usage, and YanaVault for provider keys.
|
|
5
|
+
window.YANA = {
|
|
6
|
+
username: null, // filled from /api/auth/status
|
|
7
|
+
stats: {
|
|
8
|
+
// filled from /api/status below — 0 until the server answers
|
|
9
|
+
agents: 0,
|
|
10
|
+
skills: 0,
|
|
11
|
+
// the mobile shell reads these directly; hydrated from /api/dashboard,
|
|
12
|
+
// /api/missions and /api/usage when window.YANA_MOBILE is set
|
|
13
|
+
agentsActive: 0,
|
|
14
|
+
missionsActive: 0,
|
|
15
|
+
skillsUsedToday: 0,
|
|
16
|
+
memories: 0,
|
|
17
|
+
memoriesToday: 0,
|
|
18
|
+
uptimeDays: 0,
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
// collections the mobile screens iterate — empty until hydrated
|
|
22
|
+
agents: [],
|
|
23
|
+
models: [],
|
|
24
|
+
memories: [],
|
|
25
|
+
skillCategories: [],
|
|
26
|
+
safety: { checksToday: 0, blocked: 0, pendingReview: 0, lastIncident: "—" },
|
|
27
|
+
|
|
28
|
+
// Static catalog only — connection status, usage, latency, and keys are
|
|
29
|
+
// real values resolved at runtime (YanaVault + GET /api/usage)
|
|
30
|
+
providers: [
|
|
31
|
+
{ id: "claude", name: "Claude", company: "Anthropic", models: ["Opus 4.8", "Sonnet 4.6", "Haiku 4.5"], role: "Primary — reasoning & orchestration" },
|
|
32
|
+
{ id: "openai", name: "OpenAI", company: "OpenAI", models: ["GPT-4o", "GPT-4o mini"], role: "Drafting & vision" },
|
|
33
|
+
{ id: "gemini", name: "Gemini", company: "Google", models: ["2.0 Flash", "1.5 Pro"], role: "Long context & search" },
|
|
34
|
+
{ id: "groq", name: "Groq", company: "Groq", models: ["Llama 4 70B"], role: "Router — sub-300ms decisions" },
|
|
35
|
+
{ id: "deepseek", name: "DeepSeek", company: "DeepSeek", models: ["V3", "R1"], role: "Deep reasoning — cost-efficient" },
|
|
36
|
+
{ id: "openrouter", name: "OpenRouter", company: "OpenRouter", models: ["Fallback pool · 40+ models"], role: "Overflow & fallback routing" },
|
|
37
|
+
{ id: "9router", name: "9Router", company: "Local gateway", models: ["40+ providers · auto-fallback"], role: "Quota armor — localhost:20128, never hit limits" },
|
|
38
|
+
{ id: "ollama", name: "Ollama", company: "On-device", models: ["llama3.2", "qwen3", "gemma3"], role: "Sovereign tier — rule 68, text never leaves the machine" },
|
|
39
|
+
],
|
|
40
|
+
|
|
41
|
+
// missions are created at runtime by the Mission Composer — none preloaded
|
|
42
|
+
missions: [],
|
|
43
|
+
|
|
44
|
+
// real conversation only — starts empty, persists across page navigation
|
|
45
|
+
chat: [],
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Components read window.YANA at render time, so every hydration step below
|
|
49
|
+
// announces itself — the mobile app shell listens and re-renders.
|
|
50
|
+
function yanaTouch() {
|
|
51
|
+
try { window.dispatchEvent(new Event("yana:data")); } catch (_) {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Pull live stats from MANIFEST.json via server
|
|
55
|
+
fetch("/api/status").then(function(r) { return r.json(); }).then(function(d) {
|
|
56
|
+
if (d.skills) window.YANA.stats.skills = d.skills;
|
|
57
|
+
if (d.agents) window.YANA.stats.agents = d.agents;
|
|
58
|
+
yanaTouch();
|
|
59
|
+
}).catch(function() {});
|
|
60
|
+
|
|
61
|
+
// Username for avatar + greeting
|
|
62
|
+
fetch("/api/auth/status").then(function(r) { return r.json(); }).then(function(d) {
|
|
63
|
+
if (d.username) { window.YANA.username = d.username; yanaTouch(); }
|
|
64
|
+
}).catch(function() {});
|
|
65
|
+
|
|
66
|
+
// ── Mobile hydration ──────────────────────────────────────────────────────────
|
|
67
|
+
// Desktop screens fetch these APIs inside their own components; the mobile
|
|
68
|
+
// shell (yana-mobile/) reads window.YANA directly, so fill it here from the
|
|
69
|
+
// same real sources. mobile.html sets window.YANA_MOBILE before this file.
|
|
70
|
+
if (window.YANA_MOBILE) {
|
|
71
|
+
(function() {
|
|
72
|
+
var Y = window.YANA;
|
|
73
|
+
var KIND = { fact: "Fact", knowledge: "Knowledge", experience: "Experience", context: "Context" };
|
|
74
|
+
|
|
75
|
+
// L1 memory totals + audit-log safety counters + server uptime
|
|
76
|
+
fetch("/api/dashboard").then(function(r) { return r.json(); }).then(function(d) {
|
|
77
|
+
if (d.memories) {
|
|
78
|
+
Y.stats.memories = d.memories.total;
|
|
79
|
+
Y.stats.memoriesToday = d.memories.today;
|
|
80
|
+
}
|
|
81
|
+
if (d.safety) {
|
|
82
|
+
Y.safety.checksToday = d.safety.events_today;
|
|
83
|
+
Y.safety.blocked = d.safety.blocked_today;
|
|
84
|
+
Y.safety.lastIncident = d.safety.last_incident || "None";
|
|
85
|
+
}
|
|
86
|
+
if (d.uptime_s) Y.stats.uptimeDays = Math.floor(d.uptime_s / 86400);
|
|
87
|
+
yanaTouch();
|
|
88
|
+
}).catch(function() {});
|
|
89
|
+
|
|
90
|
+
// Every L1 atomic fact — kinds normalized to the mobile card vocabulary
|
|
91
|
+
fetch("/api/memories").then(function(r) { return r.json(); }).then(function(d) {
|
|
92
|
+
if (!d.memories) return;
|
|
93
|
+
Y.memories = d.memories.map(function(m) {
|
|
94
|
+
return {
|
|
95
|
+
id: m.id, text: m.text, source: m.source, fresh: m.fresh,
|
|
96
|
+
kind: KIND[String(m.kind).toLowerCase()] || "Context",
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
yanaTouch();
|
|
100
|
+
}).catch(function() {});
|
|
101
|
+
|
|
102
|
+
// Real agent catalog — no agent is "running" unless a mission says so
|
|
103
|
+
fetch("/api/agents").then(function(r) { return r.json(); }).then(function(d) {
|
|
104
|
+
if (!d.agents) return;
|
|
105
|
+
Y.agents = d.agents.map(function(a) {
|
|
106
|
+
return {
|
|
107
|
+
id: a.category + "/" + a.name, name: a.name, role: a.category,
|
|
108
|
+
specialty: a.description, status: "idle", load: "Standby",
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
yanaTouch();
|
|
112
|
+
}).catch(function() {});
|
|
113
|
+
|
|
114
|
+
// Skill packs → category bars (usage = share of installed skills)
|
|
115
|
+
fetch("/api/skills").then(function(r) { return r.json(); }).then(function(d) {
|
|
116
|
+
if (!d.packs) return;
|
|
117
|
+
var total = d.total || 1;
|
|
118
|
+
Y.skillCategories = d.packs.map(function(p) {
|
|
119
|
+
return { name: p.name, count: p.count, usage: Math.round((p.count / total) * 100), top: p.name };
|
|
120
|
+
});
|
|
121
|
+
yanaTouch();
|
|
122
|
+
}).catch(function() {});
|
|
123
|
+
|
|
124
|
+
// Real missions from the file-backed store
|
|
125
|
+
fetch("/api/missions").then(function(r) { return r.json(); }).then(function(d) {
|
|
126
|
+
if (!d.missions) return;
|
|
127
|
+
Y.missions = d.missions.map(function(m) {
|
|
128
|
+
return {
|
|
129
|
+
id: m.id, name: m.name || m.goal || "Mission", owner: m.owner || "Navigator",
|
|
130
|
+
status: m.status || "planning", progress: m.progress || 0,
|
|
131
|
+
due: m.due || "—", tasks: m.tasks || [],
|
|
132
|
+
};
|
|
133
|
+
});
|
|
134
|
+
Y.stats.missionsActive = Y.missions.filter(function(m) { return m.status !== "done"; }).length;
|
|
135
|
+
yanaTouch();
|
|
136
|
+
}).catch(function() {});
|
|
137
|
+
|
|
138
|
+
// Per-provider session usage → "Active AI Models" rows (real latency/requests)
|
|
139
|
+
fetch("/api/usage").then(function(r) { return r.json(); }).then(function(d) {
|
|
140
|
+
var usage = d.usage || {};
|
|
141
|
+
Y.models = Y.providers.map(function(p) {
|
|
142
|
+
var u = usage[p.id];
|
|
143
|
+
var active = !!(u && u.requests > 0);
|
|
144
|
+
return {
|
|
145
|
+
id: p.id, name: p.name, model: p.models[0] || "", role: p.role,
|
|
146
|
+
status: active ? "active" : "idle",
|
|
147
|
+
load: active ? Math.min(100, u.requests * 10) : 0,
|
|
148
|
+
latency: active ? (u.avg_latency_ms / 1000).toFixed(1) + "s" : "—",
|
|
149
|
+
};
|
|
150
|
+
});
|
|
151
|
+
yanaTouch();
|
|
152
|
+
}).catch(function() {});
|
|
153
|
+
})();
|
|
154
|
+
}
|