Skip to content

Commit 5400cbe

Browse files
committed
ScriptVault v1.7.8: script registration engine fixes
Match pattern engine: - Fix isValidMatchPattern rejecting patterns with ports (e.g. http://localhost:3000/*) - Fix matchPattern wildcard subdomain comparison with ports (*.example.com:8080) - convertIncludeToMatch now validates output against isValidMatchPattern Script registration: - Fix inconsistent enabled checks: 4 locations used truthy s.enabled instead of s.enabled !== false, causing scripts with undefined enabled state to be incorrectly treated as disabled - Log failed script registrations in registerAllScripts (was silently swallowed by Promise.allSettled) DNR rules: - Check dynamic rule quota (30,000 limit) before applying GM_webRequest rules to prevent silent failures
1 parent 6a0a6d5 commit 5400cbe

File tree

14 files changed

+106
-80
lines changed

14 files changed

+106
-80
lines changed

CLAUDE.md

Lines changed: 6 additions & 6 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.7
7+
v1.7.8
88

99
## Tech Stack
1010
- Chrome MV3 extension (JavaScript)
@@ -88,8 +88,8 @@ v1.7.7
8888
- `cleanupStaleCaches()` runs on init to prune expired `require_cache_*`, `res_cache_*`, trash entries, and tombstones >30 days
8989
- Lint: `@grant none` + GM API usage shows `warning` severity (upgraded from `info` in v1.7.2). Unknown `@grant` values and invalid `@sandbox` values are `error` severity.
9090
- **Side panel / DevTools must use `action:` key** (not `type:`) for background messages. Background returns `{ scripts: [...] }` — callers need `res?.scripts`. `setScriptSettings` expects `scriptId` not `id`.
91-
- **GM_cookie_set/GM_cookie_delete** require `url` and `name` parameters (validated in v1.7.7)
92-
- **GM_unregisterMenuCommand** handler added in v1.7.7 — previously calls were silently dropped
91+
- **GM_cookie_set/GM_cookie_delete** require `url` and `name` parameters (validated in v1.7.8)
92+
- **GM_unregisterMenuCommand** handler added in v1.7.8 — previously calls were silently dropped
9393
- **XHR local request IDs** use sequential counter `_xhrSeqId++` (not `Math.random`) to prevent collision
9494
- **Notification callbacks** cleaned up on auto-timeout (not all platforms fire `onClosed`)
9595
- **Menu commands** cleaned from session storage when a script is deleted
@@ -163,7 +163,7 @@ v1.7.7
163163
- **Side panel**: responds to `chrome.tabs.onActivated` and `chrome.tabs.onUpdated` to refresh script list on navigation. Uses same `sendToBackground` pattern as popup.
164164
- **DevTools panel**: auto-refreshes every 3s. `getNetworkLog` returns flat array; `getNetworkLogStats` for totals. HAR export uses `URL.createObjectURL` + programmatic `<a>` click.
165165

166-
## v1.7.0 → v1.7.7 Audit (2026-03-24, 4 rounds)
166+
## v1.7.0 → v1.7.8 Audit (2026-03-24, 4 rounds)
167167

168168
### v1.7.0 — Major Feature Release
169169
- DevTools panel, Side panel, Script signing (Ed25519), Monaco editor adapter, Offscreen document
@@ -185,7 +185,7 @@ v1.7.7
185185
- GM_webRequest added to hints + grant values, duplicate hint directives removed
186186
- Dashboard: updated column defaults to desc sort, network log cap 500 -> 2000
187187

188-
### v1.7.7 — Memory Leaks & Validation
188+
### v1.7.8 — Memory Leaks & Validation
189189
- Added missing GM_unregisterMenuCommand handler (calls were silently dropped)
190190
- Menu commands cleaned from session storage on script delete
191191
- Notification callback cleanup on auto-timeout
@@ -232,7 +232,7 @@ v1.7.7
232232
- Fixed: NetworkLog duration calculation used `_netLogEntry.timestamp` which was undefined; replaced with dedicated `_netLogStartTime` variable
233233
- Fixed: `state.folders`, `state._collapsedFolders`, `state._lastCheckedId`, `state._quotaWarned` not initialized in dashboard state object
234234
- Fixed: `switchTab('help')` in command palette failed because help tab is a header icon, not a `.tm-tab`; added special case handling
235-
- Verified: All version strings match (v1.7.7 across manifest, manifest-firefox, content.js, popup.js, dashboard.js)
235+
- Verified: All version strings match (v1.7.8 across manifest, manifest-firefox, content.js, popup.js, dashboard.js)
236236
- Verified: All bg/ modules load before background.core.js in build output
237237
- Verified: `escapeHtml` available in popup.js (shared/utils.js loaded first)
238238
- 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.7-22c55e?style=flat-square" alt="Version">
12+
<img src="https://img.shields.io/badge/version-1.7.8-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+">
@@ -412,6 +412,6 @@ MIT License &mdash; see [LICENSE](LICENSE) for details.
412412
---
413413

414414
<p align="center">
415-
<strong>ScriptVault v1.7.7</strong><br>
415+
<strong>ScriptVault v1.7.8</strong><br>
416416
<em>Your scripts, your rules &mdash; locked down and loaded</em>
417417
</p>

background.core.js

Lines changed: 43 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -853,7 +853,7 @@ async function handleMessage(message, sender) {
853853

854854
// Re-register the script with userScripts API
855855
await unregisterScript(id);
856-
if (script.enabled) {
856+
if (script.enabled !== false) {
857857
await registerScript(script);
858858
}
859859

@@ -973,7 +973,7 @@ async function handleMessage(message, sender) {
973973
trash.splice(idx, 1);
974974
await chrome.storage.local.set({ trash });
975975
await ScriptStorage.set(script.id, script);
976-
if (script.enabled) await registerScript(script);
976+
if (script.enabled !== false) await registerScript(script);
977977
await updateBadge();
978978
return { success: true };
979979
}
@@ -1445,7 +1445,7 @@ async function handleMessage(message, sender) {
14451445
const needsReregister = EXEC_KEYS.some(k =>
14461446
JSON.stringify(oldSettings[k]) !== JSON.stringify(data.settings[k])
14471447
);
1448-
if (needsReregister && script.enabled) {
1448+
if (needsReregister && script.enabled !== false) {
14491449
await unregisterScript(data.scriptId);
14501450
await registerScript(script);
14511451
}
@@ -2660,12 +2660,20 @@ function matchPattern(pattern, url, urlObj) {
26602660

26612661
// Check host (use urlObj.host when pattern includes port, urlObj.hostname otherwise)
26622662
if (host !== '*') {
2663-
const urlHost = host.includes(':') ? urlObj.host : urlObj.hostname;
2663+
const hasPort = host.includes(':');
2664+
const urlHost = hasPort ? urlObj.host : urlObj.hostname;
26642665
if (host.startsWith('*.')) {
26652666
const baseDomain = host.slice(2);
2666-
const compareHost = host.includes(':') ? urlObj.host : urlObj.hostname;
2667-
if (compareHost !== baseDomain && !compareHost.endsWith('.' + baseDomain)) {
2668-
return false;
2667+
if (hasPort) {
2668+
// For *.example.com:8080, compare host (includes port) against baseDomain
2669+
if (urlHost !== baseDomain && !urlHost.endsWith('.' + baseDomain)) {
2670+
return false;
2671+
}
2672+
} else {
2673+
// For *.example.com, compare hostname only
2674+
if (urlObj.hostname !== baseDomain && !urlObj.hostname.endsWith('.' + baseDomain)) {
2675+
return false;
2676+
}
26692677
}
26702678
} else if (host !== urlHost) {
26712679
return false;
@@ -3263,7 +3271,7 @@ async function registerAllScripts() {
32633271
return;
32643272
}
32653273

3266-
const enabledScripts = scripts.filter(s => s.enabled);
3274+
const enabledScripts = scripts.filter(s => s.enabled !== false);
32673275

32683276
// Sort by @priority (higher = first), then position
32693277
enabledScripts.sort((a, b) => {
@@ -3276,7 +3284,11 @@ async function registerAllScripts() {
32763284
console.log(`[ScriptVault] Registering ${enabledScripts.length} scripts`);
32773285

32783286
// Register all scripts in parallel — significantly faster on large script collections
3279-
await Promise.allSettled(enabledScripts.map(script => registerScript(script)));
3287+
const results = await Promise.allSettled(enabledScripts.map(script => registerScript(script)));
3288+
const failures = results.filter(r => r.status === 'rejected');
3289+
if (failures.length > 0) {
3290+
console.warn(`[ScriptVault] ${failures.length} script(s) failed to register:`, failures.map(r => r.reason?.message || r.reason));
3291+
}
32803292
} catch (e) {
32813293
console.error('[ScriptVault] Failed to register scripts:', e);
32823294
}
@@ -3879,6 +3891,12 @@ async function applyWebRequestRules(scriptId, rules) {
38793891
});
38803892

38813893
if (dnrRules.length > 0) {
3894+
// Check dynamic rule quota (Chrome limit: 30,000)
3895+
const existing = await chrome.declarativeNetRequest.getDynamicRules();
3896+
if (existing.length + dnrRules.length > 30000) {
3897+
console.warn(`[ScriptVault] DNR rule limit would be exceeded: ${existing.length} + ${dnrRules.length} > 30000`);
3898+
return;
3899+
}
38823900
await chrome.declarativeNetRequest.updateDynamicRules({ addRules: dnrRules });
38833901
_webRequestRuleMap.set(scriptId, ruleIds);
38843902
debugLog(`[GM_webRequest] Applied ${dnrRules.length} rules for script ${scriptId}`);
@@ -5362,9 +5380,9 @@ ${libraryExports}
53625380
function isValidMatchPattern(pattern) {
53635381
if (!pattern) return false;
53645382
if (pattern === '<all_urls>') return true;
5365-
5366-
// Basic match pattern validation
5367-
const matchRegex = /^(\*|https?|file|ftp):\/\/(\*|\*\.[^/*]+|[^/*]+)\/.*$/;
5383+
5384+
// Match pattern validation (allows ports: http://localhost:3000/*)
5385+
const matchRegex = /^(\*|https?|file|ftp):\/\/(\*|\*\.[^/*]+|[^/*:]+(?::\d+)?)\/.*$/;
53685386
return matchRegex.test(pattern);
53695387
}
53705388

@@ -5440,34 +5458,29 @@ function convertIncludeToMatch(include) {
54405458

54415459
// Handle patterns like *://example.com/*
54425460
if (pattern.startsWith('*://')) {
5443-
// Ensure pattern has a path component
5444-
const afterScheme = pattern.slice(4); // after *://
5445-
if (!afterScheme.includes('/')) {
5446-
pattern += '/*';
5447-
}
5448-
return pattern;
5461+
const afterScheme = pattern.slice(4);
5462+
if (!afterScheme.includes('/')) pattern += '/*';
5463+
return isValidMatchPattern(pattern) ? pattern : null;
54495464
}
5450-
5465+
54515466
// Handle patterns like http://example.com/*
54525467
if (pattern.match(/^https?:\/\//)) {
5453-
// Add wildcard path if not present
5454-
if (!pattern.includes('/*') && !pattern.endsWith('/')) {
5455-
pattern += '/*';
5456-
}
5457-
return pattern;
5468+
if (!pattern.includes('/*') && !pattern.endsWith('/')) pattern += '/*';
5469+
return isValidMatchPattern(pattern) ? pattern : null;
54585470
}
5459-
5471+
54605472
// Handle patterns like *.example.com
54615473
if (pattern.startsWith('*.')) {
5462-
return '*://' + pattern + '/*';
5474+
const result = '*://' + pattern + '/*';
5475+
return isValidMatchPattern(result) ? result : null;
54635476
}
5464-
5477+
54655478
// Handle patterns like example.com
54665479
if (!pattern.includes('://') && !pattern.startsWith('/')) {
5467-
return '*://' + pattern + '/*';
5480+
const result = '*://' + pattern + '/*';
5481+
return isValidMatchPattern(result) ? result : null;
54685482
}
5469-
5470-
// Can't convert, return null
5483+
54715484
return null;
54725485
}
54735486

background.js

Lines changed: 44 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// ScriptVault v1.7.7 - Background Service Worker
1+
// ScriptVault v1.7.8 - 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.
@@ -6020,7 +6020,7 @@ async function handleMessage(message, sender) {
60206020

60216021
// Re-register the script with userScripts API
60226022
await unregisterScript(id);
6023-
if (script.enabled) {
6023+
if (script.enabled !== false) {
60246024
await registerScript(script);
60256025
}
60266026

@@ -6140,7 +6140,7 @@ async function handleMessage(message, sender) {
61406140
trash.splice(idx, 1);
61416141
await chrome.storage.local.set({ trash });
61426142
await ScriptStorage.set(script.id, script);
6143-
if (script.enabled) await registerScript(script);
6143+
if (script.enabled !== false) await registerScript(script);
61446144
await updateBadge();
61456145
return { success: true };
61466146
}
@@ -6612,7 +6612,7 @@ async function handleMessage(message, sender) {
66126612
const needsReregister = EXEC_KEYS.some(k =>
66136613
JSON.stringify(oldSettings[k]) !== JSON.stringify(data.settings[k])
66146614
);
6615-
if (needsReregister && script.enabled) {
6615+
if (needsReregister && script.enabled !== false) {
66166616
await unregisterScript(data.scriptId);
66176617
await registerScript(script);
66186618
}
@@ -7827,12 +7827,20 @@ function matchPattern(pattern, url, urlObj) {
78277827

78287828
// Check host (use urlObj.host when pattern includes port, urlObj.hostname otherwise)
78297829
if (host !== '*') {
7830-
const urlHost = host.includes(':') ? urlObj.host : urlObj.hostname;
7830+
const hasPort = host.includes(':');
7831+
const urlHost = hasPort ? urlObj.host : urlObj.hostname;
78317832
if (host.startsWith('*.')) {
78327833
const baseDomain = host.slice(2);
7833-
const compareHost = host.includes(':') ? urlObj.host : urlObj.hostname;
7834-
if (compareHost !== baseDomain && !compareHost.endsWith('.' + baseDomain)) {
7835-
return false;
7834+
if (hasPort) {
7835+
// For *.example.com:8080, compare host (includes port) against baseDomain
7836+
if (urlHost !== baseDomain && !urlHost.endsWith('.' + baseDomain)) {
7837+
return false;
7838+
}
7839+
} else {
7840+
// For *.example.com, compare hostname only
7841+
if (urlObj.hostname !== baseDomain && !urlObj.hostname.endsWith('.' + baseDomain)) {
7842+
return false;
7843+
}
78367844
}
78377845
} else if (host !== urlHost) {
78387846
return false;
@@ -8430,7 +8438,7 @@ async function registerAllScripts() {
84308438
return;
84318439
}
84328440

8433-
const enabledScripts = scripts.filter(s => s.enabled);
8441+
const enabledScripts = scripts.filter(s => s.enabled !== false);
84348442

84358443
// Sort by @priority (higher = first), then position
84368444
enabledScripts.sort((a, b) => {
@@ -8443,7 +8451,11 @@ async function registerAllScripts() {
84438451
console.log(`[ScriptVault] Registering ${enabledScripts.length} scripts`);
84448452

84458453
// Register all scripts in parallel — significantly faster on large script collections
8446-
await Promise.allSettled(enabledScripts.map(script => registerScript(script)));
8454+
const results = await Promise.allSettled(enabledScripts.map(script => registerScript(script)));
8455+
const failures = results.filter(r => r.status === 'rejected');
8456+
if (failures.length > 0) {
8457+
console.warn(`[ScriptVault] ${failures.length} script(s) failed to register:`, failures.map(r => r.reason?.message || r.reason));
8458+
}
84478459
} catch (e) {
84488460
console.error('[ScriptVault] Failed to register scripts:', e);
84498461
}
@@ -9046,6 +9058,12 @@ async function applyWebRequestRules(scriptId, rules) {
90469058
});
90479059

90489060
if (dnrRules.length > 0) {
9061+
// Check dynamic rule quota (Chrome limit: 30,000)
9062+
const existing = await chrome.declarativeNetRequest.getDynamicRules();
9063+
if (existing.length + dnrRules.length > 30000) {
9064+
console.warn(`[ScriptVault] DNR rule limit would be exceeded: ${existing.length} + ${dnrRules.length} > 30000`);
9065+
return;
9066+
}
90499067
await chrome.declarativeNetRequest.updateDynamicRules({ addRules: dnrRules });
90509068
_webRequestRuleMap.set(scriptId, ruleIds);
90519069
debugLog(`[GM_webRequest] Applied ${dnrRules.length} rules for script ${scriptId}`);
@@ -10529,9 +10547,9 @@ ${libraryExports}
1052910547
function isValidMatchPattern(pattern) {
1053010548
if (!pattern) return false;
1053110549
if (pattern === '<all_urls>') return true;
10532-
10533-
// Basic match pattern validation
10534-
const matchRegex = /^(\*|https?|file|ftp):\/\/(\*|\*\.[^/*]+|[^/*]+)\/.*$/;
10550+
10551+
// Match pattern validation (allows ports: http://localhost:3000/*)
10552+
const matchRegex = /^(\*|https?|file|ftp):\/\/(\*|\*\.[^/*]+|[^/*:]+(?::\d+)?)\/.*$/;
1053510553
return matchRegex.test(pattern);
1053610554
}
1053710555

@@ -10607,34 +10625,29 @@ function convertIncludeToMatch(include) {
1060710625

1060810626
// Handle patterns like *://example.com/*
1060910627
if (pattern.startsWith('*://')) {
10610-
// Ensure pattern has a path component
10611-
const afterScheme = pattern.slice(4); // after *://
10612-
if (!afterScheme.includes('/')) {
10613-
pattern += '/*';
10614-
}
10615-
return pattern;
10628+
const afterScheme = pattern.slice(4);
10629+
if (!afterScheme.includes('/')) pattern += '/*';
10630+
return isValidMatchPattern(pattern) ? pattern : null;
1061610631
}
10617-
10632+
1061810633
// Handle patterns like http://example.com/*
1061910634
if (pattern.match(/^https?:\/\//)) {
10620-
// Add wildcard path if not present
10621-
if (!pattern.includes('/*') && !pattern.endsWith('/')) {
10622-
pattern += '/*';
10623-
}
10624-
return pattern;
10635+
if (!pattern.includes('/*') && !pattern.endsWith('/')) pattern += '/*';
10636+
return isValidMatchPattern(pattern) ? pattern : null;
1062510637
}
10626-
10638+
1062710639
// Handle patterns like *.example.com
1062810640
if (pattern.startsWith('*.')) {
10629-
return '*://' + pattern + '/*';
10641+
const result = '*://' + pattern + '/*';
10642+
return isValidMatchPattern(result) ? result : null;
1063010643
}
10631-
10644+
1063210645
// Handle patterns like example.com
1063310646
if (!pattern.includes('://') && !pattern.startsWith('/')) {
10634-
return '*://' + pattern + '/*';
10647+
const result = '*://' + pattern + '/*';
10648+
return isValidMatchPattern(result) ? result : null;
1063510649
}
10636-
10637-
// Can't convert, return null
10650+
1063810651
return null;
1063910652
}
1064010653

content.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// ScriptVault v1.7.7 - Content Script Bridge
1+
// ScriptVault v1.7.8 - 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.7",
4+
"version": "1.7.8",
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.7",
4+
"version": "1.7.8",
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.7
1+
// ScriptVault Offscreen Document v1.7.8
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

0 commit comments

Comments
 (0)