aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/audit-2026-06-18_chatstudio-overlay.md24
-rw-r--r--main.html6
-rw-r--r--modules/core/core-bundle.js57
-rw-r--r--modules/core/module-loader.js2
-rw-r--r--modules/core/virtual-fs.js57
-rw-r--r--modules/ext/wpm.js43
6 files changed, 163 insertions, 26 deletions
diff --git a/docs/audit-2026-06-18_chatstudio-overlay.md b/docs/audit-2026-06-18_chatstudio-overlay.md
index 8134caf..fa3805c 100644
--- a/docs/audit-2026-06-18_chatstudio-overlay.md
+++ b/docs/audit-2026-06-18_chatstudio-overlay.md
@@ -23,18 +23,18 @@ Files reviewed: module-chatstudio.js (1,064 lines), chat-commands.js (276 lines)
| 9 | `_csGlobalsInited` stuck true after init failure — placeholder stubs remain active on retry | module-chatstudio.js:94 vs 1054 | Retry silently does nothing |
| 10 | `generateModuleWithAI` uses deprecated global `event` | chat-modules.js:276 | Fails in strict mode |
-## P2 — UX & Architecture (8 issues) — NOT YET FIXED
-
-| # | Issue | File:Line | Notes |
-|---|-------|-----------|-------|
-| 11 | API key in base64 localStorage — not encryption, trivially decodable | module-chatstudio.js:419 | Should use secret-vault.js |
-| 12 | Inconsistent storage encoding — messages plain JSON, logs/settings base64 | module-chatstudio.js:505 | Should be consistent |
-| 13 | Chat history no max size — long conversations overflow localStorage | module-chatstudio.js:502-508 | Cap at N messages |
-| 14 | Static `?v=5` cache-busting — doesn't use NEWOS4_VERSION | module-chatstudio.js:10489,10550 | Manual version bump needed |
-| 15 | `buildChatContext` naive HTML strip — regex-based, easily fooled | module-chatstudio.js:888 | Use DOMParser |
-| 16 | User messages get no markdown formatting — asymmetric UX | module-chatstudio.js:548 | Apply formatContent to user msgs too |
-| 17 | Activity log hardcoded 50 limit | module-chatstudio.js:325 | Not configurable |
-| 18 | `CHATSTUDIO_VERSION` not synced to `NEWOS4_VERSION` | module-chatstudio.js:231 | Stale version info |
+## P2 — UX & Architecture (8 issues) — ALL FIXED in v4.5.74
+
+| # | Issue | File:Line | Status |
+|---|-------|-----------|--------|
+| 11 | API key in base64 localStorage | module-chatstudio.js:419 | ✅ Fixed — API key no longer persisted, in-memory only |
+| 12 | Inconsistent storage encoding | module-chatstudio.js:505 | ✅ Fixed — logs/settings now plain JSON, backward-compat base64 fallback |
+| 13 | Chat history no max size | module-chatstudio.js:502-508 | ✅ Fixed — capped at MAX_MESSAGES=100 |
+| 14 | Static `?v=5` cache-busting | module-chatstudio.js | ✅ Fixed — uses NEWOS4_VERSION |
+| 15 | `buildChatContext` naive HTML strip | module-chatstudio.js:888 | ✅ Fixed — DOMParser with regex fallback |
+| 16 | User messages get no markdown | module-chatstudio.js:548 | ✅ Fixed — formatContent applied to user msgs too |
+| 17 | Activity log hardcoded 50 limit | module-chatstudio.js:325 | ✅ Fixed — MAX_LOGS=200, configurable |
+| 18 | `CHATSTUDIO_VERSION` not synced | module-chatstudio.js:231 | ✅ Fixed — uses window.NEWOS4_VERSION |
## Tool-Use Optimization — Applied in v4.5.70
diff --git a/main.html b/main.html
index 8d8a931..1926d89 100644
--- a/main.html
+++ b/main.html
@@ -1310,11 +1310,11 @@
</script>
<!-- v4.5.63: A2A scratchpad fixes + agent-register arg fix -->
-<script src="modules/core/core-bundle.js?v=2026-06-18-v4.5.74"></script>
-<script src="modules/ext/critical-ext-bundle.js?v=2026-06-18-v4.5.74"></script>
+<script src="modules/core/core-bundle.js?v=2026-06-18-v4.5.75"></script>
+<script src="modules/ext/critical-ext-bundle.js?v=2026-06-18-v4.5.75"></script>
<!-- Async dependency-ordered module loader now only initializes terminal and loads optional ext modules -->
-<script src="modules/core/module-loader.js?v=2026-06-18-v4.5.74"></script>
+<script src="modules/core/module-loader.js?v=2026-06-18-v4.5.75"></script>
<script>
// ═══ ONE TRUE VERSION SOURCE ═══
diff --git a/modules/core/core-bundle.js b/modules/core/core-bundle.js
index af59d06..2e57be6 100644
--- a/modules/core/core-bundle.js
+++ b/modules/core/core-bundle.js
@@ -374,6 +374,20 @@ class VirtualFileSystem {
return this._loadLegacy();
}
}
+ // P0: Check IndexedDB fallback if localStorage is empty
+ try {
+ const idbData = await this._loadFromIndexedDB();
+ if (idbData) {
+ const text = await this._decompress(idbData);
+ const data = JSON.parse(text);
+ this.root = data.root || this.root;
+ this.currentPath = data.currentPath || [];
+ this._usingIndexedDB = true;
+ this._purgeTrash();
+ console.info('FS loaded from IndexedDB fallback');
+ return true;
+ }
+ } catch (e) { /* IndexedDB not available or empty */ }
return this._loadLegacy();
} catch (e) { console.error('FS load error:', e); return false; }
}
@@ -413,14 +427,50 @@ class VirtualFileSystem {
const json = JSON.stringify({ root: this.root, currentPath: this.currentPath });
const compressed = await this._compress(json);
if (compressed) {
- localStorage.setItem('webx11_filesystem_v2', compressed);
- localStorage.removeItem('webx11_filesystem');
+ try {
+ localStorage.setItem('webx11_filesystem_v2', compressed);
+ localStorage.removeItem('webx11_filesystem');
+ } catch (quotaErr) {
+ // P0: IndexedDB fallback when localStorage quota exceeded
+ console.warn('localStorage quota exceeded, falling back to IndexedDB:', quotaErr.message);
+ await this._saveToIndexedDB(compressed);
+ this._usingIndexedDB = true;
+ }
} else {
localStorage.setItem('webx11_filesystem', json);
}
} catch (e) { console.error('FS save error:', e); }
}, 2000);
}
+ // P0: IndexedDB fallback storage for large VFS data
+ _openIDB() {
+ return new Promise((resolve, reject) => {
+ const req = indexedDB.open('webx11_vfs', 1);
+ req.onupgradeneeded = () => {
+ req.result.createObjectStore('vfs');
+ };
+ req.onsuccess = () => resolve(req.result);
+ req.onerror = () => reject(req.error);
+ });
+ }
+ async _saveToIndexedDB(data) {
+ const db = await this._openIDB();
+ return new Promise((resolve, reject) => {
+ const tx = db.transaction('vfs', 'readwrite');
+ tx.objectStore('vfs').put(data, 'filesystem_v2');
+ tx.oncomplete = () => { db.close(); resolve(true); };
+ tx.onerror = () => { db.close(); reject(tx.error); };
+ });
+ }
+ async _loadFromIndexedDB() {
+ const db = await this._openIDB();
+ return new Promise((resolve, reject) => {
+ const tx = db.transaction('vfs', 'readonly');
+ const req = tx.objectStore('vfs').get('filesystem_v2');
+ req.onsuccess = () => { db.close(); resolve(req.result || null); };
+ req.onerror = () => { db.close(); reject(req.error); };
+ });
+ }
getCurrentDir() {
let dir = this.root;
for (let part of this.currentPath) {
@@ -919,6 +969,9 @@ class VirtualFileSystem {
this.currentPath = [];
// Wipe all localStorage keys
['webx11_filesystem', 'webx11_filesystem_v2'].forEach(k => localStorage.removeItem(k));
+ // P0: Also clear IndexedDB fallback
+ try { indexedDB.deleteDatabase('webx11_vfs'); } catch (e) {}
+ this._usingIndexedDB = false;
return true;
}
}
diff --git a/modules/core/module-loader.js b/modules/core/module-loader.js
index f1608ed..6927743 100644
--- a/modules/core/module-loader.js
+++ b/modules/core/module-loader.js
@@ -4,7 +4,7 @@
// Core module bundle loaded synchronously in main.html.
// This loader only handles version cache-busting, splash, terminal init, and optional ext modules.
- const LATEST_VER = '2026-06-18-v4.5.74';
+ const LATEST_VER = '2026-06-18-v4.5.75';
const CACHE = '?v=' + LATEST_VER;
const stored = localStorage.getItem('webx11_cache_ver');
if (stored !== LATEST_VER) {
diff --git a/modules/core/virtual-fs.js b/modules/core/virtual-fs.js
index 827c446..7f6b441 100644
--- a/modules/core/virtual-fs.js
+++ b/modules/core/virtual-fs.js
@@ -372,6 +372,20 @@ class VirtualFileSystem {
return this._loadLegacy();
}
}
+ // P0: Check IndexedDB fallback if localStorage is empty
+ try {
+ const idbData = await this._loadFromIndexedDB();
+ if (idbData) {
+ const text = await this._decompress(idbData);
+ const data = JSON.parse(text);
+ this.root = data.root || this.root;
+ this.currentPath = data.currentPath || [];
+ this._usingIndexedDB = true;
+ this._purgeTrash();
+ console.info('FS loaded from IndexedDB fallback');
+ return true;
+ }
+ } catch (e) { /* IndexedDB not available or empty */ }
return this._loadLegacy();
} catch (e) { console.error('FS load error:', e); return false; }
}
@@ -411,14 +425,50 @@ class VirtualFileSystem {
const json = JSON.stringify({ root: this.root, currentPath: this.currentPath });
const compressed = await this._compress(json);
if (compressed) {
- localStorage.setItem('webx11_filesystem_v2', compressed);
- localStorage.removeItem('webx11_filesystem');
+ try {
+ localStorage.setItem('webx11_filesystem_v2', compressed);
+ localStorage.removeItem('webx11_filesystem');
+ } catch (quotaErr) {
+ // P0: IndexedDB fallback when localStorage quota exceeded
+ console.warn('localStorage quota exceeded, falling back to IndexedDB:', quotaErr.message);
+ await this._saveToIndexedDB(compressed);
+ this._usingIndexedDB = true;
+ }
} else {
localStorage.setItem('webx11_filesystem', json);
}
} catch (e) { console.error('FS save error:', e); }
}, 2000);
}
+ // P0: IndexedDB fallback storage for large VFS data
+ _openIDB() {
+ return new Promise((resolve, reject) => {
+ const req = indexedDB.open('webx11_vfs', 1);
+ req.onupgradeneeded = () => {
+ req.result.createObjectStore('vfs');
+ };
+ req.onsuccess = () => resolve(req.result);
+ req.onerror = () => reject(req.error);
+ });
+ }
+ async _saveToIndexedDB(data) {
+ const db = await this._openIDB();
+ return new Promise((resolve, reject) => {
+ const tx = db.transaction('vfs', 'readwrite');
+ tx.objectStore('vfs').put(data, 'filesystem_v2');
+ tx.oncomplete = () => { db.close(); resolve(true); };
+ tx.onerror = () => { db.close(); reject(tx.error); };
+ });
+ }
+ async _loadFromIndexedDB() {
+ const db = await this._openIDB();
+ return new Promise((resolve, reject) => {
+ const tx = db.transaction('vfs', 'readonly');
+ const req = tx.objectStore('vfs').get('filesystem_v2');
+ req.onsuccess = () => { db.close(); resolve(req.result || null); };
+ req.onerror = () => { db.close(); reject(req.error); };
+ });
+ }
getCurrentDir() {
let dir = this.root;
for (let part of this.currentPath) {
@@ -917,6 +967,9 @@ class VirtualFileSystem {
this.currentPath = [];
// Wipe all localStorage keys
['webx11_filesystem', 'webx11_filesystem_v2'].forEach(k => localStorage.removeItem(k));
+ // P0: Also clear IndexedDB fallback
+ try { indexedDB.deleteDatabase('webx11_vfs'); } catch (e) {}
+ this._usingIndexedDB = false;
return true;
}
}
diff --git a/modules/ext/wpm.js b/modules/ext/wpm.js
index 314e124..68ac9cb 100644
--- a/modules/ext/wpm.js
+++ b/modules/ext/wpm.js
@@ -57,11 +57,39 @@ class WpmModule {
if (this.installed[name]) return this.terminal.print(`Package "${name}" already installed. Use wpm update to reinstall.`, 'warning');
this.terminal.print(`Installing ${pkg.name} v${pkg.version}...`, 'info');
this.terminal.print(` ${pkg.desc}`, 'info');
- const success = await this.terminal.moduleManager.loadModuleFromURL(pkg.url);
- if (success) {
- this.installed[name] = { version: pkg.version, installed: new Date().toISOString() };
- this.saveInstalled();
- this.terminal.print(`"${name}" installed successfully!`, 'success');
+ // P0: SRI — fetch source, compute SHA-256, verify integrity before loading
+ try {
+ const res = await fetch(pkg.url);
+ if (!res.ok) throw new Error('HTTP ' + res.status);
+ const source = await res.text();
+ // Compute SHA-256 via Web Crypto API
+ const encoder = new TextEncoder();
+ const data = encoder.encode(source);
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
+ const hashHex = Array.from(new Uint8Array(hashBuffer))
+ .map(b => b.toString(16).padStart(2, '0')).join('');
+ // Verify if package declares a hash
+ if (pkg.hash) {
+ if (hashHex !== pkg.hash) {
+ this.terminal.print(`❌ INTEGRITY CHECK FAILED: hash mismatch!`, 'error');
+ this.terminal.print(` Expected: ${pkg.hash}`, 'error');
+ this.terminal.print(` Got: ${hashHex}`, 'error');
+ this.terminal.print(` Package may have been tampered with. Aborting.`, 'error');
+ return;
+ }
+ this.terminal.print(`✅ Integrity verified (SHA-256: ${hashHex.substring(0, 16)}...)`, 'success');
+ } else {
+ this.terminal.print(`⚠ No hash declared. Computed SHA-256: ${hashHex.substring(0, 24)}...`, 'warning');
+ }
+ // Load the verified source
+ const success = await this.terminal.moduleManager.loadModuleFromURL(pkg.url);
+ if (success) {
+ this.installed[name] = { version: pkg.version, installed: new Date().toISOString(), hash: hashHex };
+ this.saveInstalled();
+ this.terminal.print(`"${name}" installed successfully!`, 'success');
+ }
+ } catch (e) {
+ this.terminal.print(`Install failed: ${e.message}`, 'error');
}
}
async list() {
@@ -143,7 +171,10 @@ class WpmModule {
this.terminal.print(`Commands: ${pkg.commands.join(', ')}`);
this.terminal.print(`URL: ${pkg.url}`);
const installed = this.installed[name];
- if (installed) this.terminal.print(`Installed: ${installed.installed}`, 'success');
+ if (installed) {
+ this.terminal.print(`Installed: ${installed.installed}`, 'success');
+ if (installed.hash) this.terminal.print(`Hash: ${installed.hash.substring(0, 24)}...`, 'info');
+ }
else this.terminal.print('Not installed. Use: wpm install ' + name, 'info');
}