aboutsummaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-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
4 files changed, 148 insertions, 11 deletions
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');
}