Skip to content

Commit 122da4c

Browse files
committed
ScriptVault v1.7.3: memory leaks, missing handlers, validation fixes
Background fixes: - Add missing GM_unregisterMenuCommand handler (commands persisted forever) - Clean up menu commands from session storage on script delete - Fix notification callback leak (cleanup on auto-timeout) - Fix XHR local request ID collision (sequential counter instead of Math.random) - Add url/name validation to GM_cookie_set and GM_cookie_delete Dashboard fixes: - Add try-catch to bulk enable/disable operations - Add .catch() to auto-delete .then() chain in closeScriptTab
1 parent ffe0199 commit 122da4c

14 files changed

Lines changed: 89 additions & 21 deletions

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
Modern userscript manager built with Chrome Manifest V3. Tampermonkey-inspired functionality with cloud sync, auto-updates, a full dashboard, Monaco editor, DevTools panel, and a persistent side panel.
55

66
## Version
7-
v1.7.2
7+
v1.7.3
88

99
## Tech Stack
1010
- Chrome MV3 extension (JavaScript)
@@ -194,7 +194,7 @@ v1.7.2
194194
- Fixed: NetworkLog duration calculation used `_netLogEntry.timestamp` which was undefined; replaced with dedicated `_netLogStartTime` variable
195195
- Fixed: `state.folders`, `state._collapsedFolders`, `state._lastCheckedId`, `state._quotaWarned` not initialized in dashboard state object
196196
- Fixed: `switchTab('help')` in command palette failed because help tab is a header icon, not a `.tm-tab`; added special case handling
197-
- Verified: All version strings match (v1.7.2 across manifest, manifest-firefox, content.js, popup.js, dashboard.js)
197+
- Verified: All version strings match (v1.7.3 across manifest, manifest-firefox, content.js, popup.js, dashboard.js)
198198
- Verified: All bg/ modules load before background.core.js in build output
199199
- Verified: `escapeHtml` available in popup.js (shared/utils.js loaded first)
200200
- Verified: Column index mapping still correct after pin button addition (pin is inside actions TD, not a new column)

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
</p>
1010

1111
<p align="center">
12-
<img src="https://img.shields.io/badge/version-1.7.2-22c55e?style=flat-square" alt="Version">
12+
<img src="https://img.shields.io/badge/version-1.7.3-22c55e?style=flat-square" alt="Version">
1313
<img src="https://img.shields.io/badge/manifest-v3-60a5fa?style=flat-square" alt="Manifest V3">
1414
<img src="https://img.shields.io/badge/license-MIT-orange?style=flat-square" alt="License">
1515
<img src="https://img.shields.io/badge/chrome-120%2B-blue?style=flat-square" alt="Chrome 120+">
@@ -325,6 +325,6 @@ MIT License &mdash; see [LICENSE](LICENSE) for details.
325325
---
326326

327327
<p align="center">
328-
<strong>ScriptVault v1.7.2</strong><br>
328+
<strong>ScriptVault v1.7.3</strong><br>
329329
<em>Your scripts, your rules &mdash; locked down and loaded</em>
330330
</p>

background.core.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,15 @@ async function handleMessage(message, sender) {
927927
await unregisterScript(scriptId);
928928
await ScriptStorage.delete(scriptId);
929929

930+
// Clean up menu commands for deleted script
931+
try {
932+
const cmdData = await chrome.storage.session.get('menuCommands');
933+
if (cmdData?.menuCommands?.[scriptId]) {
934+
delete cmdData.menuCommands[scriptId];
935+
await chrome.storage.session.set(cmdData);
936+
}
937+
} catch {}
938+
930939
// Record tombstone so sync won't re-import this script from remote
931940
const tombstoneData = await chrome.storage.local.get('syncTombstones');
932941
const tombstones = tombstoneData.syncTombstones || {};
@@ -2097,6 +2106,8 @@ async function handleMessage(message, sender) {
20972106
if (data.timeout && data.timeout > 0) {
20982107
setTimeout(() => {
20992108
chrome.notifications.clear(notifId).catch(() => {});
2109+
// Clean up callback tracker (onClosed listener may not fire on all platforms)
2110+
self._notifCallbacks.delete(notifId);
21002111
}, data.timeout);
21012112
}
21022113
return { success: true, id: notifId };
@@ -2197,6 +2208,21 @@ async function handleMessage(message, sender) {
21972208
return { success: true };
21982209
}
21992210

2211+
case 'unregisterMenuCommand':
2212+
case 'GM_unregisterMenuCommand': {
2213+
const commands = await chrome.storage.session.get('menuCommands') || {};
2214+
if (commands.menuCommands?.[data.scriptId]) {
2215+
commands.menuCommands[data.scriptId] = commands.menuCommands[data.scriptId].filter(
2216+
c => c.id !== data.commandId
2217+
);
2218+
if (commands.menuCommands[data.scriptId].length === 0) {
2219+
delete commands.menuCommands[data.scriptId];
2220+
}
2221+
await chrome.storage.session.set(commands);
2222+
}
2223+
return { success: true };
2224+
}
2225+
22002226
// Get menu commands
22012227
case 'getMenuCommands': {
22022228
const result = await chrome.storage.session.get('menuCommands');
@@ -2250,6 +2276,8 @@ async function handleMessage(message, sender) {
22502276

22512277
case 'GM_cookie_set': {
22522278
try {
2279+
if (!data.url) return { error: 'url is required for cookie set' };
2280+
if (!data.name) return { error: 'name is required for cookie set' };
22532281
const cookie = await chrome.cookies.set({
22542282
url: data.url,
22552283
name: data.name,
@@ -2269,6 +2297,7 @@ async function handleMessage(message, sender) {
22692297

22702298
case 'GM_cookie_delete': {
22712299
try {
2300+
if (!data.url || !data.name) return { error: 'url and name are required for cookie delete' };
22722301
await chrome.cookies.remove({
22732302
url: data.url,
22742303
name: data.name
@@ -4045,6 +4074,7 @@ ${req.code}
40454074
40464075
// XHR request tracking (like Violentmonkey's idMap)
40474076
const _xhrRequests = new Map(); // requestId -> { details, aborted }
4077+
let _xhrSeqId = 0;
40484078
40494079
// Value change listeners (like Tampermonkey)
40504080
const _valueChangeListeners = new Map(); // listenerId -> { key, callback }
@@ -4441,7 +4471,7 @@ ${req.code}
44414471
}
44424472
44434473
// Generate unique request ID
4444-
const localId = 'xhr_' + Math.random().toString(36).substring(2) + Date.now().toString(36);
4474+
const localId = 'xhr_' + (++_xhrSeqId) + '_' + Date.now().toString(36);
44454475
let requestId = null;
44464476
let aborted = false;
44474477
let currentMapKey = localId;

background.js

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// ScriptVault v1.7.2 - Background Service Worker
1+
// ScriptVault v1.7.3 - Background Service Worker
22
// Comprehensive userscript manager with cloud sync and auto-updates
33
// NOTE: This file is built from source modules. Edit the individual files in
44
// shared/, modules/, and lib/, then run build-background.sh to regenerate.
@@ -6093,6 +6093,15 @@ async function handleMessage(message, sender) {
60936093
await unregisterScript(scriptId);
60946094
await ScriptStorage.delete(scriptId);
60956095

6096+
// Clean up menu commands for deleted script
6097+
try {
6098+
const cmdData = await chrome.storage.session.get('menuCommands');
6099+
if (cmdData?.menuCommands?.[scriptId]) {
6100+
delete cmdData.menuCommands[scriptId];
6101+
await chrome.storage.session.set(cmdData);
6102+
}
6103+
} catch {}
6104+
60966105
// Record tombstone so sync won't re-import this script from remote
60976106
const tombstoneData = await chrome.storage.local.get('syncTombstones');
60986107
const tombstones = tombstoneData.syncTombstones || {};
@@ -7263,6 +7272,8 @@ async function handleMessage(message, sender) {
72637272
if (data.timeout && data.timeout > 0) {
72647273
setTimeout(() => {
72657274
chrome.notifications.clear(notifId).catch(() => {});
7275+
// Clean up callback tracker (onClosed listener may not fire on all platforms)
7276+
self._notifCallbacks.delete(notifId);
72667277
}, data.timeout);
72677278
}
72687279
return { success: true, id: notifId };
@@ -7363,6 +7374,21 @@ async function handleMessage(message, sender) {
73637374
return { success: true };
73647375
}
73657376

7377+
case 'unregisterMenuCommand':
7378+
case 'GM_unregisterMenuCommand': {
7379+
const commands = await chrome.storage.session.get('menuCommands') || {};
7380+
if (commands.menuCommands?.[data.scriptId]) {
7381+
commands.menuCommands[data.scriptId] = commands.menuCommands[data.scriptId].filter(
7382+
c => c.id !== data.commandId
7383+
);
7384+
if (commands.menuCommands[data.scriptId].length === 0) {
7385+
delete commands.menuCommands[data.scriptId];
7386+
}
7387+
await chrome.storage.session.set(commands);
7388+
}
7389+
return { success: true };
7390+
}
7391+
73667392
// Get menu commands
73677393
case 'getMenuCommands': {
73687394
const result = await chrome.storage.session.get('menuCommands');
@@ -7416,6 +7442,8 @@ async function handleMessage(message, sender) {
74167442

74177443
case 'GM_cookie_set': {
74187444
try {
7445+
if (!data.url) return { error: 'url is required for cookie set' };
7446+
if (!data.name) return { error: 'name is required for cookie set' };
74197447
const cookie = await chrome.cookies.set({
74207448
url: data.url,
74217449
name: data.name,
@@ -7435,6 +7463,7 @@ async function handleMessage(message, sender) {
74357463

74367464
case 'GM_cookie_delete': {
74377465
try {
7466+
if (!data.url || !data.name) return { error: 'url and name are required for cookie delete' };
74387467
await chrome.cookies.remove({
74397468
url: data.url,
74407469
name: data.name
@@ -9211,6 +9240,7 @@ ${req.code}
92119240
92129241
// XHR request tracking (like Violentmonkey's idMap)
92139242
const _xhrRequests = new Map(); // requestId -> { details, aborted }
9243+
let _xhrSeqId = 0;
92149244
92159245
// Value change listeners (like Tampermonkey)
92169246
const _valueChangeListeners = new Map(); // listenerId -> { key, callback }
@@ -9607,7 +9637,7 @@ ${req.code}
96079637
}
96089638
96099639
// Generate unique request ID
9610-
const localId = 'xhr_' + Math.random().toString(36).substring(2) + Date.now().toString(36);
9640+
const localId = 'xhr_' + (++_xhrSeqId) + '_' + Date.now().toString(36);
96119641
let requestId = null;
96129642
let aborted = false;
96139643
let currentMapKey = localId;

content.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// ScriptVault v1.7.2 - Content Script Bridge
1+
// ScriptVault v1.7.3 - Content Script Bridge
22
// Bridges messages between userscripts (USER_SCRIPT world) and background service worker
33

44
(function() {

manifest-firefox.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "__MSG_extName__",
4-
"version": "1.7.2",
4+
"version": "1.7.3",
55
"description": "__MSG_extDescription__",
66
"default_locale": "en",
77
"homepage_url": "https://github.com/SysAdminDoc/ScriptVault",

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"manifest_version": 3,
33
"name": "__MSG_extName__",
4-
"version": "1.7.2",
4+
"version": "1.7.3",
55
"description": "__MSG_extDescription__",
66
"default_locale": "en",
77
"minimum_chrome_version": "120",

offscreen.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// ScriptVault Offscreen Document v1.7.2
1+
// ScriptVault Offscreen Document v1.7.3
22
// Handles CPU-intensive tasks off the service worker:
33
// - AST-based script analysis (via Acorn)
44
// - 3-way text merge for sync conflict resolution

pages/dashboard.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// ScriptVault Dashboard v1.7.2 - Full-Featured Controller
1+
// ScriptVault Dashboard v1.7.3 - Full-Featured Controller
22
(function() {
33
'use strict';
44

@@ -1325,7 +1325,11 @@
13251325
for (let i = 0; i < ids.length; i++) {
13261326
const s = state.scripts.find(x => x.id === ids[i]);
13271327
updateProgress(i + 1, ids.length, `${s?.metadata?.name || ids[i]} (${i + 1}/${ids.length})`);
1328-
await chrome.runtime.sendMessage({ action: 'toggleScript', scriptId: ids[i], enabled: true });
1328+
try {
1329+
await chrome.runtime.sendMessage({ action: 'toggleScript', scriptId: ids[i], enabled: true });
1330+
} catch (e) {
1331+
console.warn('[ScriptVault] Enable failed for', ids[i], e.message);
1332+
}
13291333
}
13301334
await loadScripts();
13311335
updateStats();
@@ -1338,7 +1342,11 @@
13381342
for (let i = 0; i < ids.length; i++) {
13391343
const s = state.scripts.find(x => x.id === ids[i]);
13401344
updateProgress(i + 1, ids.length, `${s?.metadata?.name || ids[i]} (${i + 1}/${ids.length})`);
1341-
await chrome.runtime.sendMessage({ action: 'toggleScript', scriptId: ids[i], enabled: false });
1345+
try {
1346+
await chrome.runtime.sendMessage({ action: 'toggleScript', scriptId: ids[i], enabled: false });
1347+
} catch (e) {
1348+
console.warn('[ScriptVault] Disable failed for', ids[i], e.message);
1349+
}
13421350
}
13431351
await loadScripts();
13441352
updateStats();
@@ -2042,7 +2050,7 @@
20422050
chrome.runtime.sendMessage({ action: 'deleteScript', scriptId }).then(() => {
20432051
loadScripts();
20442052
updateStats();
2045-
});
2053+
}).catch(e => console.warn('[ScriptVault] Auto-delete failed:', e.message));
20462054
}
20472055

20482056
if (state.currentScriptId === scriptId) {

pages/devtools-panel.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// ScriptVault DevTools Panel v1.7.2
1+
// ScriptVault DevTools Panel v1.7.3
22
// Network inspection, execution profiling, and console capture
33

44
(function () {
@@ -232,7 +232,7 @@
232232
comment: e.scriptName || ''
233233
}));
234234

235-
const har = { log: { version: '1.2', creator: { name: 'ScriptVault', version: '1.7.2' }, entries } };
235+
const har = { log: { version: '1.2', creator: { name: 'ScriptVault', version: '1.7.3' }, entries } };
236236
const blob = new Blob([JSON.stringify(har, null, 2)], { type: 'application/json' });
237237
const url = URL.createObjectURL(blob);
238238
const a = document.createElement('a');

0 commit comments

Comments
 (0)