sourcemonkey 0.0.0-stage → 0.5.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/LICENSE +21 -0
- package/README.md +87 -2
- package/dist/bytes.js +37 -0
- package/dist/channel-placeholder.js +5 -0
- package/dist/collection-scan.js +17 -0
- package/dist/collections.html +353 -0
- package/dist/compare.html +358 -0
- package/dist/composed-source.js +205 -0
- package/dist/connect-prompt.html +47 -0
- package/dist/debug/icons.html +211 -0
- package/dist/debug/storage.html +163 -0
- package/dist/debug-log.js +75 -0
- package/dist/diagnostic-location.js +64 -0
- package/dist/eval-hook.js +308 -0
- package/dist/extension/background.js +5263 -0
- package/dist/extension/channel.js +133 -0
- package/dist/extension/code-links.js +187 -0
- package/dist/extension/code-tabs.js +460 -0
- package/dist/extension/collection-dialog.js +619 -0
- package/dist/extension/collection-display.js +322 -0
- package/dist/extension/collections.js +2905 -0
- package/dist/extension/compare.js +867 -0
- package/dist/extension/concurrency.js +186 -0
- package/dist/extension/connect-grants.js +109 -0
- package/dist/extension/connect-prompt-page.js +70 -0
- package/dist/extension/connect-prompt.js +266 -0
- package/dist/extension/copy-button.js +63 -0
- package/dist/extension/copy-dialog.js +502 -0
- package/dist/extension/copy-preference.js +66 -0
- package/dist/extension/debug/icons.js +203 -0
- package/dist/extension/debug/storage.js +365 -0
- package/dist/extension/deep-link.js +77 -0
- package/dist/extension/delete-dialog.js +432 -0
- package/dist/extension/delete-icon.js +40 -0
- package/dist/extension/details-tooltips.js +66 -0
- package/dist/extension/directories.js +679 -0
- package/dist/extension/export-backup.js +407 -0
- package/dist/extension/export-dialog.js +142 -0
- package/dist/extension/file-access.js +74 -0
- package/dist/extension/file-changes.js +346 -0
- package/dist/extension/frame-info.js +85 -0
- package/dist/extension/github-provider.js +336 -0
- package/dist/extension/glyph.js +84 -0
- package/dist/extension/gm-bindings.js +601 -0
- package/dist/extension/gm-download.js +419 -0
- package/dist/extension/gm-menu.js +228 -0
- package/dist/extension/gm-notification.js +141 -0
- package/dist/extension/gm-originator-map.js +23 -0
- package/dist/extension/gm-tab-data.js +121 -0
- package/dist/extension/gm-tab.js +160 -0
- package/dist/extension/gm-url-change.js +111 -0
- package/dist/extension/gm-url-scheme.js +27 -0
- package/dist/extension/gm-values.js +176 -0
- package/dist/extension/gm-window-control.js +104 -0
- package/dist/extension/gm-xhr-headers.js +333 -0
- package/dist/extension/gm-xhr.js +584 -0
- package/dist/extension/injector.js +3145 -0
- package/dist/extension/install-capture.js +277 -0
- package/dist/extension/line-diff.js +233 -0
- package/dist/extension/local-copy.js +560 -0
- package/dist/extension/local-delete.js +276 -0
- package/dist/extension/location-kinds.js +108 -0
- package/dist/extension/log-format.js +158 -0
- package/dist/extension/managed-changes.js +361 -0
- package/dist/extension/managed-items.js +57 -0
- package/dist/extension/match-index.js +375 -0
- package/dist/extension/missing-scripts.js +66 -0
- package/dist/extension/more-menu.js +109 -0
- package/dist/extension/move-dialog.js +197 -0
- package/dist/extension/offscreen.js +37 -0
- package/dist/extension/options.js +504 -0
- package/dist/extension/page-heading.js +170 -0
- package/dist/extension/page-links.js +199 -0
- package/dist/extension/parse-check-client.js +70 -0
- package/dist/extension/parse-check.js +161 -0
- package/dist/extension/parse-sandbox-host.js +172 -0
- package/dist/extension/parse-sandbox.js +175 -0
- package/dist/extension/popup-commands-open.js +41 -0
- package/dist/extension/popup-view.js +387 -0
- package/dist/extension/popup.js +703 -0
- package/dist/extension/prism-userscript.js +53 -0
- package/dist/extension/refresh-error.js +136 -0
- package/dist/extension/refresh-targets.js +160 -0
- package/dist/extension/related-versions.js +1144 -0
- package/dist/extension/resize-observer-loop.js +19 -0
- package/dist/extension/row-cache.js +387 -0
- package/dist/extension/script-blobs.js +413 -0
- package/dist/extension/script-details.js +658 -0
- package/dist/extension/script-enablement.js +285 -0
- package/dist/extension/script-identity.js +310 -0
- package/dist/extension/script-keys.js +54 -0
- package/dist/extension/script-log-id.js +155 -0
- package/dist/extension/script-log.js +290 -0
- package/dist/extension/script-rejection.js +104 -0
- package/dist/extension/script-sites.js +158 -0
- package/dist/extension/script-store.js +3507 -0
- package/dist/extension/script-updater.js +991 -0
- package/dist/extension/script-url-hash.js +61 -0
- package/dist/extension/select-all.js +37 -0
- package/dist/extension/select-open-guard.js +90 -0
- package/dist/extension/site-icon.js +1261 -0
- package/dist/extension/sm-dev-link.js +302 -0
- package/dist/extension/solo-click.js +58 -0
- package/dist/extension/sticky-numbers.js +188 -0
- package/dist/extension/storage-breakdown.js +253 -0
- package/dist/extension/storage-scan.js +86 -0
- package/dist/extension/storage-usage.js +194 -0
- package/dist/extension/structural-keys.js +29 -0
- package/dist/extension/sw-teardown.js +36 -0
- package/dist/extension/tab-status.js +403 -0
- package/dist/extension/toolbar-summary.js +105 -0
- package/dist/extension/user-scripts-access.js +43 -0
- package/dist/extension/userscript.js +3672 -0
- package/dist/extension/value-store.js +204 -0
- package/dist/extension/version-probe-fetch.js +68 -0
- package/dist/extension/worker-client.js +109 -0
- package/dist/extension/worker-ops.js +171 -0
- package/dist/extension/zip-writer.js +24 -0
- package/dist/external-files.js +878 -0
- package/dist/file-loader.js +436 -0
- package/dist/file-locations.js +439 -0
- package/dist/generated-wrappers.js +5 -0
- package/dist/github-auth.js +148 -0
- package/dist/github-urls.js +46 -0
- package/dist/gm-api-catalog.js +217 -0
- package/dist/gm-connect.js +273 -0
- package/dist/gm-xhr-types.js +5 -0
- package/dist/http-read.js +142 -0
- package/dist/icons/icon-128.png +0 -0
- package/dist/icons/icon-16.png +0 -0
- package/dist/icons/icon-48.png +0 -0
- package/dist/local-file-overrides.js +558 -0
- package/dist/manifest.json +38 -0
- package/dist/markup-response.js +126 -0
- package/dist/match-targets.js +305 -0
- package/dist/menu-captions.js +22 -0
- package/dist/offscreen.html +39 -0
- package/dist/options.html +197 -0
- package/dist/parse-check-protocol.js +51 -0
- package/dist/parse-location.js +171 -0
- package/dist/parse-sandbox.html +56 -0
- package/dist/phase-timing.js +155 -0
- package/dist/popup.html +197 -0
- package/dist/public-suffixes.js +6957 -0
- package/dist/run-context.js +31 -0
- package/dist/runtime-messages.js +34 -0
- package/dist/sandboxed-page.js +35 -0
- package/dist/script-build.js +787 -0
- package/dist/script-display.js +383 -0
- package/dist/script-icon.js +43 -0
- package/dist/script-links.js +204 -0
- package/dist/script-source-url.js +214 -0
- package/dist/script-world.js +65 -0
- package/dist/sm-dev-protocol.js +84 -0
- package/dist/source-map.js +177 -0
- package/dist/stack-link.js +138 -0
- package/dist/styles.css +825 -0
- package/dist/unsafe-window-use.js +229 -0
- package/dist/url-match.js +734 -0
- package/dist/userscript-header.js +248 -0
- package/dist/userscript.html +1165 -0
- package/dist/vendor/fflate.LICENSE +21 -0
- package/dist/vendor/fflate.js +2692 -0
- package/dist/vendor/prism/prism-line-numbers.css +1 -0
- package/dist/vendor/prism/prism-line-numbers.js +1 -0
- package/dist/vendor/prism/prism.css +1 -0
- package/dist/vendor/prism/prism.js +1946 -0
- package/dist/version-compare.js +141 -0
- package/dist/version-probe.js +120 -0
- package/lib/build-info.json +1 -0
- package/lib/bytes.d.ts +3 -0
- package/lib/bytes.js +37 -0
- package/lib/channel-placeholder.d.ts +1 -0
- package/lib/channel-placeholder.js +5 -0
- package/lib/collection-scan.d.ts +8 -0
- package/lib/collection-scan.js +17 -0
- package/lib/composed-source.d.ts +28 -0
- package/lib/composed-source.js +205 -0
- package/lib/debug-log.d.ts +9 -0
- package/lib/debug-log.js +75 -0
- package/lib/diagnostic-location.d.ts +8 -0
- package/lib/diagnostic-location.js +64 -0
- package/lib/eval-hook.d.ts +34 -0
- package/lib/eval-hook.js +308 -0
- package/lib/external-files.d.ts +111 -0
- package/lib/external-files.js +878 -0
- package/lib/file-loader.d.ts +48 -0
- package/lib/file-loader.js +436 -0
- package/lib/file-locations.d.ts +44 -0
- package/lib/file-locations.js +439 -0
- package/lib/generated-wrappers.d.ts +11 -0
- package/lib/generated-wrappers.js +5 -0
- package/lib/github-auth.d.ts +12 -0
- package/lib/github-auth.js +148 -0
- package/lib/github-urls.d.ts +1 -0
- package/lib/github-urls.js +46 -0
- package/lib/gm-api-catalog.d.ts +24 -0
- package/lib/gm-api-catalog.js +217 -0
- package/lib/gm-connect.d.ts +34 -0
- package/lib/gm-connect.js +273 -0
- package/lib/gm-xhr-types.d.ts +68 -0
- package/lib/gm-xhr-types.js +5 -0
- package/lib/harness/browser-extensions.d.ts +73 -0
- package/lib/harness/browser-extensions.js +285 -0
- package/lib/harness/browser-lang.d.ts +20 -0
- package/lib/harness/browser-lang.js +86 -0
- package/lib/harness/browser-launch.d.ts +14 -0
- package/lib/harness/browser-launch.js +184 -0
- package/lib/harness/checks.d.ts +110 -0
- package/lib/harness/checks.js +321 -0
- package/lib/harness/cli.d.ts +2 -0
- package/lib/harness/cli.js +2259 -0
- package/lib/harness/command-args.d.ts +91 -0
- package/lib/harness/command-args.js +369 -0
- package/lib/harness/command-lines.d.ts +80 -0
- package/lib/harness/command-lines.js +180 -0
- package/lib/harness/command-list.d.ts +206 -0
- package/lib/harness/command-list.js +1475 -0
- package/lib/harness/console-commands.d.ts +463 -0
- package/lib/harness/console-commands.js +3216 -0
- package/lib/harness/download-watch.d.ts +21 -0
- package/lib/harness/download-watch.js +156 -0
- package/lib/harness/extension-backend.d.ts +41 -0
- package/lib/harness/extension-backend.js +100 -0
- package/lib/harness/extension-host.d.ts +112 -0
- package/lib/harness/extension-host.js +552 -0
- package/lib/harness/extension-launch.d.ts +67 -0
- package/lib/harness/extension-launch.js +285 -0
- package/lib/harness/extension-link.d.ts +315 -0
- package/lib/harness/extension-link.js +607 -0
- package/lib/harness/external-manager.d.ts +146 -0
- package/lib/harness/external-manager.js +414 -0
- package/lib/harness/fixture.d.ts +165 -0
- package/lib/harness/fixture.js +461 -0
- package/lib/harness/frame-sessions.d.ts +71 -0
- package/lib/harness/frame-sessions.js +248 -0
- package/lib/harness/frames.d.ts +68 -0
- package/lib/harness/frames.js +216 -0
- package/lib/harness/help-format.d.ts +17 -0
- package/lib/harness/help-format.js +127 -0
- package/lib/harness/host.d.ts +352 -0
- package/lib/harness/host.js +423 -0
- package/lib/harness/index.d.ts +8 -0
- package/lib/harness/index.js +10 -0
- package/lib/harness/input-file.d.ts +18 -0
- package/lib/harness/input-file.js +61 -0
- package/lib/harness/json-output.d.ts +565 -0
- package/lib/harness/json-output.js +71 -0
- package/lib/harness/manager-actions.d.ts +21 -0
- package/lib/harness/manager-actions.js +129 -0
- package/lib/harness/manager-commands.d.ts +142 -0
- package/lib/harness/manager-commands.js +1196 -0
- package/lib/harness/manager-fetch.d.ts +33 -0
- package/lib/harness/manager-fetch.js +239 -0
- package/lib/harness/managers.d.ts +24 -0
- package/lib/harness/managers.js +27 -0
- package/lib/harness/output-style.d.ts +125 -0
- package/lib/harness/output-style.js +322 -0
- package/lib/harness/page-commands.d.ts +134 -0
- package/lib/harness/page-commands.js +1141 -0
- package/lib/harness/page-output.d.ts +67 -0
- package/lib/harness/page-output.js +139 -0
- package/lib/harness/page-watch.d.ts +171 -0
- package/lib/harness/page-watch.js +607 -0
- package/lib/harness/parse-check-node.d.ts +3 -0
- package/lib/harness/parse-check-node.js +132 -0
- package/lib/harness/path-args.d.ts +22 -0
- package/lib/harness/path-args.js +148 -0
- package/lib/harness/playwright-chromium.d.ts +16 -0
- package/lib/harness/playwright-chromium.js +86 -0
- package/lib/harness/prepare.d.ts +168 -0
- package/lib/harness/prepare.js +364 -0
- package/lib/harness/prompt-editor.d.ts +17 -0
- package/lib/harness/prompt-editor.js +354 -0
- package/lib/harness/prompt-history.d.ts +13 -0
- package/lib/harness/prompt-history.js +89 -0
- package/lib/harness/rebuild-advice.d.ts +6 -0
- package/lib/harness/rebuild-advice.js +39 -0
- package/lib/harness/run-log.d.ts +47 -0
- package/lib/harness/run-log.js +140 -0
- package/lib/harness/run-logs.d.ts +132 -0
- package/lib/harness/run-logs.js +241 -0
- package/lib/harness/run.d.ts +194 -0
- package/lib/harness/run.js +1973 -0
- package/lib/harness/runs.d.ts +229 -0
- package/lib/harness/runs.js +726 -0
- package/lib/harness/save-html.d.ts +39 -0
- package/lib/harness/save-html.js +89 -0
- package/lib/harness/saved-pages.d.ts +120 -0
- package/lib/harness/saved-pages.js +275 -0
- package/lib/harness/script-mark.d.ts +17 -0
- package/lib/harness/script-mark.js +56 -0
- package/lib/harness/snapshot.d.ts +143 -0
- package/lib/harness/snapshot.js +474 -0
- package/lib/harness/stuck-tabs.d.ts +19 -0
- package/lib/harness/stuck-tabs.js +131 -0
- package/lib/harness/tab-refs.d.ts +20 -0
- package/lib/harness/tab-refs.js +80 -0
- package/lib/harness/tampermonkey.d.ts +71 -0
- package/lib/harness/tampermonkey.js +356 -0
- package/lib/harness/tm-ids.d.ts +18 -0
- package/lib/harness/tm-ids.js +88 -0
- package/lib/harness/violentmonkey.d.ts +34 -0
- package/lib/harness/violentmonkey.js +162 -0
- package/lib/harness/wait.d.ts +77 -0
- package/lib/harness/wait.js +458 -0
- package/lib/http-read.d.ts +30 -0
- package/lib/http-read.js +142 -0
- package/lib/local-file-overrides.d.ts +35 -0
- package/lib/local-file-overrides.js +558 -0
- package/lib/manifest.json +38 -0
- package/lib/markup-response.d.ts +11 -0
- package/lib/markup-response.js +126 -0
- package/lib/match-targets.d.ts +18 -0
- package/lib/match-targets.js +305 -0
- package/lib/menu-captions.d.ts +1 -0
- package/lib/menu-captions.js +22 -0
- package/lib/parse-check-protocol.d.ts +27 -0
- package/lib/parse-check-protocol.js +51 -0
- package/lib/parse-location.d.ts +12 -0
- package/lib/parse-location.js +171 -0
- package/lib/phase-timing.d.ts +12 -0
- package/lib/phase-timing.js +155 -0
- package/lib/public-suffixes.d.ts +1 -0
- package/lib/public-suffixes.js +6957 -0
- package/lib/run-context.d.ts +2 -0
- package/lib/run-context.js +31 -0
- package/lib/runtime-messages.d.ts +10 -0
- package/lib/runtime-messages.js +34 -0
- package/lib/sandboxed-page.d.ts +5 -0
- package/lib/sandboxed-page.js +35 -0
- package/lib/script-build.d.ts +111 -0
- package/lib/script-build.js +787 -0
- package/lib/script-display.d.ts +51 -0
- package/lib/script-display.js +383 -0
- package/lib/script-icon.d.ts +16 -0
- package/lib/script-icon.js +43 -0
- package/lib/script-links.d.ts +24 -0
- package/lib/script-links.js +204 -0
- package/lib/script-source-url.d.ts +8 -0
- package/lib/script-source-url.js +214 -0
- package/lib/script-world.d.ts +6 -0
- package/lib/script-world.js +65 -0
- package/lib/sm-dev-protocol.d.ts +251 -0
- package/lib/sm-dev-protocol.js +84 -0
- package/lib/source-map.d.ts +32 -0
- package/lib/source-map.js +177 -0
- package/lib/stack-link.d.ts +15 -0
- package/lib/stack-link.js +138 -0
- package/lib/unsafe-window-use.d.ts +1 -0
- package/lib/unsafe-window-use.js +229 -0
- package/lib/url-match.d.ts +40 -0
- package/lib/url-match.js +734 -0
- package/lib/userscript-header.d.ts +33 -0
- package/lib/userscript-header.js +248 -0
- package/lib/version-compare.d.ts +6 -0
- package/lib/version-compare.js +141 -0
- package/lib/version-probe.d.ts +17 -0
- package/lib/version-probe.js +120 -0
- package/package.json +55 -3
- package/skills/SourceMonkey-dev/SKILL.md +868 -0
- package/skills/SourceMonkey-dev/references/cdp.md +293 -0
- package/skills/SourceMonkey-dev/references/commands.md +297 -0
- package/skills/SourceMonkey-dev/references/other-managers.md +63 -0
- package/skills/SourceMonkey-dev/references/specs.md +46 -0
- package/skills/SourceMonkey-friction/SKILL.md +103 -0
- package/skills/SourceMonkey-friction/references/template.md +59 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// SW-side persistent storage for `GM_setValue` / `GM_getValue` /
|
|
2
|
+
// `GM_deleteValue` / `GM_listValues` and their GM4 aliases.
|
|
3
|
+
//
|
|
4
|
+
// Layout. Each `(scriptId, key)` lives at its own
|
|
5
|
+
// `gmval:<scriptId>:<key>` row in chrome.storage.local. Rows are
|
|
6
|
+
// independent of `script:<id>` rows so a write doesn't churn the
|
|
7
|
+
// match-index cache; conversely, deleting a script doesn't lose its
|
|
8
|
+
// stored values without an explicit sweep.
|
|
9
|
+
//
|
|
10
|
+
// Why per-key rows (not a per-script object): chrome.storage.local
|
|
11
|
+
// rejects values larger than ~8 KB per row in some Chrome builds,
|
|
12
|
+
// but the per-row quota is much higher than the per-extension quota.
|
|
13
|
+
// Per-key rows let a single 64 KB value land without forcing the
|
|
14
|
+
// whole script's storage into one row that bumps against the
|
|
15
|
+
// per-row limit; they also let us write a single key delta without
|
|
16
|
+
// rewriting siblings.
|
|
17
|
+
//
|
|
18
|
+
// Type tagging happens PAGE-SIDE, not here. chrome.storage
|
|
19
|
+
// round-trips via JSON, which is lossy for Date / Map / Set / NaN /
|
|
20
|
+
// undefined — but so is `chrome.runtime.sendMessage`, so by the time
|
|
21
|
+
// a value reaches this module the damage would already be done. The
|
|
22
|
+
// codec therefore lives in `wrapper-gm-runtime.js`, which tags a
|
|
23
|
+
// value before it goes on the wire and revives it after the snapshot
|
|
24
|
+
// comes back. This module stores whatever it is handed, opaquely.
|
|
25
|
+
//
|
|
26
|
+
// The tagged form is a plain `{ __smv__: 1, v: … }` object, and only
|
|
27
|
+
// values JSON would have mangled get wrapped at all — so rows this
|
|
28
|
+
// module wrote before the codec existed are byte-identical to what
|
|
29
|
+
// it writes for the same values now. See
|
|
30
|
+
// docs/GM-APIs-storage.md#value-type-fidelity.
|
|
31
|
+
//
|
|
32
|
+
// Reading one script's values. chrome.storage has no prefix query, so
|
|
33
|
+
// the shape is `getKeys()` → filter → `get(thoseKeys)`. That matters
|
|
34
|
+
// more than it looks: the alternative, `get(null)`, returns every row
|
|
35
|
+
// the extension owns and is paid for on the reload fast path. See
|
|
36
|
+
// `loadAllValues`.
|
|
37
|
+
import { scanAllStorage, timeStorageRead } from './storage-scan.js';
|
|
38
|
+
export const VALUE_PREFIX = 'gmval:';
|
|
39
|
+
// Compose / parse the storage key shape: `gmval:<scriptId>:<key>`.
|
|
40
|
+
// `scriptId` is a UUID so no `:` in it; `key` may contain `:`. We
|
|
41
|
+
// only split on the FIRST two colons to preserve any colons in the
|
|
42
|
+
// user-supplied key.
|
|
43
|
+
export function valueKey(scriptId, key) {
|
|
44
|
+
return `${VALUE_PREFIX}${scriptId}:${key}`;
|
|
45
|
+
}
|
|
46
|
+
export function valueKeyPrefix(scriptId) {
|
|
47
|
+
return `${VALUE_PREFIX}${scriptId}:`;
|
|
48
|
+
}
|
|
49
|
+
// Extract the user-supplied key from a `gmval:<scriptId>:<key>` row
|
|
50
|
+
// name. Returns `null` for rows that don't belong to this script.
|
|
51
|
+
export function parseValueKey(rowKey, scriptId) {
|
|
52
|
+
const prefix = valueKeyPrefix(scriptId);
|
|
53
|
+
if (!rowKey.startsWith(prefix))
|
|
54
|
+
return null;
|
|
55
|
+
return rowKey.slice(prefix.length);
|
|
56
|
+
}
|
|
57
|
+
// Load every stored value for a script as a plain object. The value
|
|
58
|
+
// store calls it the first time it needs a script's values.
|
|
59
|
+
//
|
|
60
|
+
// There is no prefix query, so the shape has to be "narrow the key set,
|
|
61
|
+
// then read only those rows". `getKeys()` (Chrome 130+) returns names
|
|
62
|
+
// without values, which is the cheap half; the fallback reads
|
|
63
|
+
// everything the way this always did.
|
|
64
|
+
//
|
|
65
|
+
// Why it matters that this is narrow: the caller on the reload fast
|
|
66
|
+
// path runs it INSIDE the 500 ms swap window, and a full-namespace
|
|
67
|
+
// read is not merely bigger — deserializing it occupies the worker's
|
|
68
|
+
// single thread, so it delays the swap decision and every unrelated
|
|
69
|
+
// callback waiting alongside it. Measured on a 266-row profile: ~155 ms
|
|
70
|
+
// for the whole namespace versus a handful of `gmval:` rows.
|
|
71
|
+
// See docs/diagnostics.md#storage-scans.
|
|
72
|
+
export async function loadAllValues(scriptId) {
|
|
73
|
+
const prefix = valueKeyPrefix(scriptId);
|
|
74
|
+
const out = {};
|
|
75
|
+
// Timed around BOTH legs — the key listing is part of what the narrow
|
|
76
|
+
// path costs, and the line exists to be compared against the full
|
|
77
|
+
// scan it replaced.
|
|
78
|
+
const narrow = await timeStorageRead('gm snapshot (narrow)', async () => {
|
|
79
|
+
const keys = await listValueKeys(prefix);
|
|
80
|
+
if (!keys)
|
|
81
|
+
return null;
|
|
82
|
+
return keys.length === 0 ? {} : await chrome.storage.local.get(keys);
|
|
83
|
+
});
|
|
84
|
+
if (narrow) {
|
|
85
|
+
for (const [k, v] of Object.entries(narrow))
|
|
86
|
+
out[k.slice(prefix.length)] = v;
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
const all = await scanAllStorage('gm snapshot');
|
|
90
|
+
for (const [k, v] of Object.entries(all)) {
|
|
91
|
+
if (k.startsWith(prefix))
|
|
92
|
+
out[k.slice(prefix.length)] = v;
|
|
93
|
+
}
|
|
94
|
+
return out;
|
|
95
|
+
}
|
|
96
|
+
// How many values a script has stored, read without the values.
|
|
97
|
+
export async function countStoredValues(scriptId) {
|
|
98
|
+
const prefix = valueKeyPrefix(scriptId);
|
|
99
|
+
const keys = await listValueKeys(prefix)
|
|
100
|
+
?? Object.keys(await scanAllStorage('gm value count')).filter((k) => k.startsWith(prefix));
|
|
101
|
+
return keys.length;
|
|
102
|
+
}
|
|
103
|
+
// Every stored key starting with `prefix`, or null when the browser has
|
|
104
|
+
// no `getKeys` — in which case the caller falls back to a full scan.
|
|
105
|
+
// A throw is treated the same as absence: this is an optimisation, and
|
|
106
|
+
// nothing about it should be able to fail a value read.
|
|
107
|
+
async function listValueKeys(prefix) {
|
|
108
|
+
const area = chrome.storage.local;
|
|
109
|
+
if (typeof area.getKeys !== 'function')
|
|
110
|
+
return null;
|
|
111
|
+
try {
|
|
112
|
+
return (await area.getKeys()).filter((k) => k.startsWith(prefix));
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// The deletion sentinel. Compared by reference in the SW handler so a
|
|
119
|
+
// userscript that happens to set the value to the same string literal
|
|
120
|
+
// is not treated as a delete.
|
|
121
|
+
export const DELETED_SENTINEL = Object.freeze({ __sm_deleted__: true });
|
|
122
|
+
export function isDeletionSentinel(v) {
|
|
123
|
+
return v === DELETED_SENTINEL;
|
|
124
|
+
}
|
|
125
|
+
// Apply a batched mixed set/delete operation. Returns the changed
|
|
126
|
+
// keys so a future broadcast phase can ship the delta to other tabs.
|
|
127
|
+
//
|
|
128
|
+
// Pure semantics: if the same key appears multiple times in the
|
|
129
|
+
// batch, the LAST write wins (matches what a sequence of GM_setValue
|
|
130
|
+
// + GM_deleteValue calls would do).
|
|
131
|
+
export async function applyBatchValues(scriptId, writes) {
|
|
132
|
+
if (writes.length === 0)
|
|
133
|
+
return { setKeys: [], deletedKeys: [] };
|
|
134
|
+
// Resolve last-write-wins per key.
|
|
135
|
+
const finalSets = new Map();
|
|
136
|
+
const finalDeletes = new Set();
|
|
137
|
+
for (const w of writes) {
|
|
138
|
+
if (isDeletionSentinel(w.value)) {
|
|
139
|
+
finalDeletes.add(w.key);
|
|
140
|
+
finalSets.delete(w.key);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
finalDeletes.delete(w.key);
|
|
144
|
+
finalSets.set(w.key, w.value);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// chrome.storage.local.set + remove are independent calls; do them
|
|
148
|
+
// in parallel. We never want a partial application — if either
|
|
149
|
+
// rejects, the caller should re-try the whole batch. We don't
|
|
150
|
+
// attempt rollback because chrome.storage doesn't expose that.
|
|
151
|
+
const setObj = {};
|
|
152
|
+
for (const [k, v] of finalSets)
|
|
153
|
+
setObj[valueKey(scriptId, k)] = v;
|
|
154
|
+
const removeKeys = [...finalDeletes].map((k) => valueKey(scriptId, k));
|
|
155
|
+
const ops = [];
|
|
156
|
+
if (Object.keys(setObj).length > 0)
|
|
157
|
+
ops.push(chrome.storage.local.set(setObj));
|
|
158
|
+
if (removeKeys.length > 0)
|
|
159
|
+
ops.push(chrome.storage.local.remove(removeKeys));
|
|
160
|
+
await Promise.all(ops);
|
|
161
|
+
return {
|
|
162
|
+
setKeys: [...finalSets.keys()],
|
|
163
|
+
deletedKeys: [...finalDeletes],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
// Sweep every `gmval:<scriptId>:*` row. Called only through the value
|
|
167
|
+
// store's `wipeValues`, which also drops the store's own copy.
|
|
168
|
+
export async function wipeScriptValues(scriptId) {
|
|
169
|
+
const prefix = valueKeyPrefix(scriptId);
|
|
170
|
+
const keys = await listValueKeys(prefix)
|
|
171
|
+
?? Object.keys(await scanAllStorage('gm value wipe')).filter((k) => k.startsWith(prefix));
|
|
172
|
+
if (keys.length === 0)
|
|
173
|
+
return 0;
|
|
174
|
+
await chrome.storage.local.remove(keys);
|
|
175
|
+
return keys.length;
|
|
176
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// SW-side driver for `@grant window.close` and `@grant window.focus`.
|
|
2
|
+
//
|
|
3
|
+
// Why the service worker is involved: both methods exist in the page
|
|
4
|
+
// already, and both are hobbled there.
|
|
5
|
+
//
|
|
6
|
+
// - `window.close()` is refused for any tab the script didn't open
|
|
7
|
+
// itself with `window.open` ("Scripts may close only the windows
|
|
8
|
+
// that were opened by them").
|
|
9
|
+
// - `window.focus()` on a background tab does nothing; nothing in a
|
|
10
|
+
// page can select another tab or raise the OS window.
|
|
11
|
+
//
|
|
12
|
+
// An extension is not bound by either rule, so the granted call is
|
|
13
|
+
// routed here and re-issued as `chrome.tabs.remove` /
|
|
14
|
+
// `chrome.tabs.update` + `chrome.windows.update`. That is also how TM
|
|
15
|
+
// implements the same two grants — the page-side override is a thin
|
|
16
|
+
// shim over the extension's own tab/window control.
|
|
17
|
+
//
|
|
18
|
+
// Wire protocol (page → SW, `chrome.runtime.sendMessage`):
|
|
19
|
+
// `{ type: 'sm:gm-window-close' | 'sm:gm-window-focus', scriptId }`
|
|
20
|
+
// → `{ ok: true }` or `{ ok: false, error }`.
|
|
21
|
+
//
|
|
22
|
+
// The target is never named in the payload: it is always the sender's
|
|
23
|
+
// own tab. A script can only close/focus the tab it is running in, so
|
|
24
|
+
// there is nothing to spoof — unlike GM_openInTab, whose URL comes
|
|
25
|
+
// from the page.
|
|
26
|
+
export const SM_WINDOW_CLOSE_MSG = 'sm:gm-window-close';
|
|
27
|
+
export const SM_WINDOW_FOCUS_MSG = 'sm:gm-window-focus';
|
|
28
|
+
function reason(err) {
|
|
29
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
30
|
+
// Capitalized because it lands after a `: ` — see the error-message
|
|
31
|
+
// rules in CLAUDE.md.
|
|
32
|
+
return msg === '' ? 'The browser gave no reason' : msg;
|
|
33
|
+
}
|
|
34
|
+
// The last tab standing is not closable. Chrome would allow it — and
|
|
35
|
+
// take the whole browser down with it, since closing the final tab
|
|
36
|
+
// quits. Handing a userscript the ability to quit the browser is a
|
|
37
|
+
// different order of capability from closing a tab, so it is refused.
|
|
38
|
+
// TM 5.5.0 refuses it the same way (`fp` in its `background.js`:
|
|
39
|
+
// `tabs.query({windowType:'normal'})`, then "refused to close last
|
|
40
|
+
// tab!" when one or fewer come back).
|
|
41
|
+
export const LAST_TAB_NOT_CLOSABLE = 'The last open tab cannot be closed';
|
|
42
|
+
// `window.close()` → close the sender's tab.
|
|
43
|
+
//
|
|
44
|
+
// Nothing is reported back on success: the document that asked is gone
|
|
45
|
+
// by then. The tab count is read across every normal window, not just
|
|
46
|
+
// the sender's, so closing the last tab of one window while another is
|
|
47
|
+
// open still works.
|
|
48
|
+
export async function closeGmWindow(tabId) {
|
|
49
|
+
try {
|
|
50
|
+
const tabs = await chrome.tabs.query({ windowType: 'normal' });
|
|
51
|
+
if (tabs.length <= 1) {
|
|
52
|
+
return { ok: false, error: LAST_TAB_NOT_CLOSABLE };
|
|
53
|
+
}
|
|
54
|
+
await chrome.tabs.remove(tabId);
|
|
55
|
+
return { ok: true };
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
return { ok: false, error: `This tab could not be closed: ${reason(err)}` };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// `window.focus()` → bring the sender's tab to the front: select it
|
|
62
|
+
// within its window, and raise (and un-minimize) that window.
|
|
63
|
+
//
|
|
64
|
+
// Both halves are needed for the call to mean anything. Raising alone
|
|
65
|
+
// is invisible in the common single-window case — the window is
|
|
66
|
+
// already frontmost and the user is simply looking at another tab —
|
|
67
|
+
// and selecting alone leaves the tab hidden behind another window.
|
|
68
|
+
//
|
|
69
|
+
// A minimized window has to be restored explicitly — `focused: true`
|
|
70
|
+
// alone leaves it minimized — so the current state is read first.
|
|
71
|
+
// `chrome.windows.update` rejects `{ focused: true, state:
|
|
72
|
+
// 'minimized' }`, which is why the two are never combined.
|
|
73
|
+
//
|
|
74
|
+
// This is step-for-step what TM 5.5.0 does (`focusTab` in its
|
|
75
|
+
// `background.js`): `tabs.update({active:true})`, then `windows.get`,
|
|
76
|
+
// then `windows.update({focused:true, state})` with `state` set to
|
|
77
|
+
// `'normal'` only for a minimized window.
|
|
78
|
+
//
|
|
79
|
+
// Whether the window actually comes forward is ultimately the OS
|
|
80
|
+
// window manager's call: several Linux WMs answer a programmatic
|
|
81
|
+
// raise by flashing the taskbar entry instead. Nothing an extension
|
|
82
|
+
// can do about that, and Chrome reports no error for it.
|
|
83
|
+
export async function focusGmWindow(tabId, windowId) {
|
|
84
|
+
if (!chrome.windows) {
|
|
85
|
+
return { ok: false, error: 'Window focus is not available in this browser' };
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
await chrome.tabs.update(tabId, { active: true });
|
|
89
|
+
let minimized = false;
|
|
90
|
+
try {
|
|
91
|
+
const win = await chrome.windows.get(windowId);
|
|
92
|
+
minimized = win.state === 'minimized';
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Non-fatal: worst case we skip the un-minimize and the plain
|
|
96
|
+
// `focused: true` below still runs.
|
|
97
|
+
}
|
|
98
|
+
await chrome.windows.update(windowId, minimized ? { focused: true, state: 'normal' } : { focused: true });
|
|
99
|
+
return { ok: true };
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
return { ok: false, error: `This tab could not be focused: ${reason(err)}` };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
// Request-header surgery for `GM_xmlhttpRequest`.
|
|
2
|
+
//
|
|
3
|
+
// The problem: `fetch()` silently drops any header the Fetch Standard
|
|
4
|
+
// (the web standard defining `fetch`, at fetch.spec.whatwg.org) calls
|
|
5
|
+
// a *forbidden request header* — `Origin`, `Referer`, `Cookie`,
|
|
6
|
+
// `Host`, `User-Agent`, and everything with a `Sec-` or `Proxy-`
|
|
7
|
+
// prefix. A userscript that needs one of those (and userscripts do:
|
|
8
|
+
// sites gate APIs on `Sec-Fetch-Site`, `Referer`, or a hand-built
|
|
9
|
+
// `Cookie`) has no way to set it from JS at all — the restriction
|
|
10
|
+
// applies in the service worker exactly as it does on a page.
|
|
11
|
+
//
|
|
12
|
+
// The fix, and why it looks like this:
|
|
13
|
+
//
|
|
14
|
+
// - Manifest V3 has no blocking `webRequest`, so headers can only be
|
|
15
|
+
// rewritten by `declarativeNetRequest` (DNR — Chrome's rule-based
|
|
16
|
+
// request-rewriting API). Rules are data, evaluated by the browser
|
|
17
|
+
// after our `fetch` call has already handed the request over.
|
|
18
|
+
// - So the caller's forbidden headers are *not* passed to `fetch`.
|
|
19
|
+
// They're compiled into a short-lived DNR session rule that sets
|
|
20
|
+
// them on the way out, and the rule is torn down when the request
|
|
21
|
+
// settles.
|
|
22
|
+
// - DNR cannot match on a request header, so the rule can't be
|
|
23
|
+
// targeted by a header tag. It's targeted by a tag in the URL's
|
|
24
|
+
// *fragment* instead: the request goes out as `<url>#sm-xhr-<id>`
|
|
25
|
+
// and the rule matches that exact string. A fragment is never
|
|
26
|
+
// transmitted, so the server still sees the URL the script asked
|
|
27
|
+
// for, byte for byte — and `Response.url` drops it again, so
|
|
28
|
+
// `finalUrl` is unaffected.
|
|
29
|
+
// - Measured, not assumed: for a service-worker `fetch`, DNR matches
|
|
30
|
+
// the fragment literally. A fragment'd rule does not match a
|
|
31
|
+
// fragment-less request, a fragment-less rule does not match a
|
|
32
|
+
// fragment'd one, and two different fragments never match each
|
|
33
|
+
// other. The full table is in docs/xhr-header-surgery.md.
|
|
34
|
+
// - So the tag is a genuine per-request discriminator, which is what
|
|
35
|
+
// makes the rule apply to exactly the request that installed it.
|
|
36
|
+
// Two same-URL requests in flight at once — the case that would
|
|
37
|
+
// otherwise have one picking up the other's `Cookie` — differ in
|
|
38
|
+
// their tag and so in their rule.
|
|
39
|
+
// - `tabIds: [-1]` ("no tab", true of every extension request and no
|
|
40
|
+
// page load) and `initiatorDomains: [<our id>]` stay on as defence
|
|
41
|
+
// in depth. Violentmonkey's MV3 path uses the same fragment trick
|
|
42
|
+
// with `tabIds` alone.
|
|
43
|
+
//
|
|
44
|
+
// Tags are *not* globally unique: `nextRuleId` restarts at
|
|
45
|
+
// XHR_RULE_ID_MIN in every worker life, so a rule stranded by a killed
|
|
46
|
+
// worker holds a tag the next worker will hand out again. What keeps
|
|
47
|
+
// that harmless is the install path, and both halves of it matter:
|
|
48
|
+
// `installHeaderRule` waits for `clearStaleHeaderRules` (so the
|
|
49
|
+
// stranded rule is normally gone before any tagged request exists) and
|
|
50
|
+
// then adds its rule in the same `updateSessionRules` call that clears
|
|
51
|
+
// its own id (so a sweep that failed can't leave a colliding rule
|
|
52
|
+
// live). Don't drop either as redundant.
|
|
53
|
+
// Headers `fetch()` refuses to send. Close to the Fetch Standard's
|
|
54
|
+
// forbidden-request-header list, but the authority here is what Chrome
|
|
55
|
+
// actually drops, which is not the same set: `user-agent` came off the
|
|
56
|
+
// spec list and Chrome still drops it, so it stays. Prefix rules
|
|
57
|
+
// (`Sec-`, `Proxy-`) are handled separately in `isForbiddenHeader`.
|
|
58
|
+
//
|
|
59
|
+
// Routing a header here means a script can now genuinely set it —
|
|
60
|
+
// including framing headers like `content-length` and
|
|
61
|
+
// `transfer-encoding`, which `fetch` used to swallow harmlessly. That
|
|
62
|
+
// is the same latitude Tampermonkey gives, and the script asked.
|
|
63
|
+
//
|
|
64
|
+
// `Cookie` is on the list but deliberately absent here — see
|
|
65
|
+
// `COOKIE_HEADER` below.
|
|
66
|
+
const FORBIDDEN_HEADERS = new Set([
|
|
67
|
+
'accept-charset',
|
|
68
|
+
'accept-encoding',
|
|
69
|
+
'access-control-request-headers',
|
|
70
|
+
'access-control-request-method',
|
|
71
|
+
'connection',
|
|
72
|
+
'content-length',
|
|
73
|
+
'cookie2',
|
|
74
|
+
'date',
|
|
75
|
+
'dnt',
|
|
76
|
+
'expect',
|
|
77
|
+
'host',
|
|
78
|
+
'keep-alive',
|
|
79
|
+
'origin',
|
|
80
|
+
'permissions-policy',
|
|
81
|
+
'referer',
|
|
82
|
+
'te',
|
|
83
|
+
'trailer',
|
|
84
|
+
'transfer-encoding',
|
|
85
|
+
'upgrade',
|
|
86
|
+
'user-agent',
|
|
87
|
+
'via',
|
|
88
|
+
// Forbidden only when the value names a forbidden method, but
|
|
89
|
+
// `fetch` won't carry them dependably either way, so they take the
|
|
90
|
+
// rule path unconditionally.
|
|
91
|
+
'x-http-method',
|
|
92
|
+
'x-http-method-override',
|
|
93
|
+
'x-method-override',
|
|
94
|
+
]);
|
|
95
|
+
// `Cookie` needs the same surgery but has an extra consequence: a
|
|
96
|
+
// script-supplied Cookie replaces what the browser would have sent, so
|
|
97
|
+
// setting it is also a way to *drop* the user's session. Kept separate
|
|
98
|
+
// so callers can reason about it on its own.
|
|
99
|
+
const COOKIE_HEADER = 'cookie';
|
|
100
|
+
// Whether `fetch()` would refuse to send this header, so it has to go
|
|
101
|
+
// through a DNR rule instead.
|
|
102
|
+
export function isForbiddenHeader(name) {
|
|
103
|
+
const lower = name.toLowerCase();
|
|
104
|
+
if (FORBIDDEN_HEADERS.has(lower) || lower === COOKIE_HEADER)
|
|
105
|
+
return true;
|
|
106
|
+
// Prefix rules. `Sec-` is the one that matters most in practice:
|
|
107
|
+
// `Sec-Fetch-Site` is how a server tells a page request from an
|
|
108
|
+
// extension one, and a service worker always sends `none` — a value
|
|
109
|
+
// no page can produce, which some WAFs (web application firewalls —
|
|
110
|
+
// the filtering layer in front of a site) reject outright.
|
|
111
|
+
return lower.startsWith('sec-') || lower.startsWith('proxy-');
|
|
112
|
+
}
|
|
113
|
+
// Header names are case-insensitive, so a caller passing both `Origin`
|
|
114
|
+
// and `origin` collapses to one surgical entry and the last one wins.
|
|
115
|
+
// Object key order is the caller's own, and either value is a thing
|
|
116
|
+
// they asked for, so there's nothing better to pick.
|
|
117
|
+
export function splitHeaders(headers) {
|
|
118
|
+
const passthrough = {};
|
|
119
|
+
const surgical = {};
|
|
120
|
+
for (const [name, value] of Object.entries(headers ?? {})) {
|
|
121
|
+
if (isForbiddenHeader(name))
|
|
122
|
+
surgical[name.toLowerCase()] = value;
|
|
123
|
+
else
|
|
124
|
+
passthrough[name] = value;
|
|
125
|
+
}
|
|
126
|
+
return { passthrough, surgical };
|
|
127
|
+
}
|
|
128
|
+
// The one header we change without being asked: `Origin`, which
|
|
129
|
+
// `fetch` from the service worker stamps as `chrome-extension://<id>`.
|
|
130
|
+
// That tells the target nothing it should act on and advertises that
|
|
131
|
+
// the user runs SourceMonkey, so it comes off.
|
|
132
|
+
//
|
|
133
|
+
// Only on methods that would carry one. Measured: a SW `fetch` sends
|
|
134
|
+
// `Origin` on POST and not on GET, so defaulting it for every request
|
|
135
|
+
// would install a rule per request to remove a header that isn't
|
|
136
|
+
// there — a declarativeNetRequest round trip each way, on the most
|
|
137
|
+
// common shape of request there is.
|
|
138
|
+
//
|
|
139
|
+
// Nothing else is defaulted. In particular `Sec-Fetch-Site` is left at
|
|
140
|
+
// the `none` the browser supplies: setting it to `same-origin` defeats
|
|
141
|
+
// a site's cross-origin check, and that is a thing a script should have
|
|
142
|
+
// to ask for rather than something every request quietly does. A script
|
|
143
|
+
// that needs it passes it in `headers` like any other.
|
|
144
|
+
const ORIGIN_LESS_METHODS = new Set(['GET', 'HEAD']);
|
|
145
|
+
function defaultRemovedHeaders(method) {
|
|
146
|
+
return ORIGIN_LESS_METHODS.has(method.toUpperCase()) ? [] : ['origin'];
|
|
147
|
+
}
|
|
148
|
+
// `urlFilter` is a pattern language, not a literal: `*` matches any
|
|
149
|
+
// run of characters and `^` matches any URL separator. A URL carrying
|
|
150
|
+
// either — legal in a query string — would silently widen the rule to
|
|
151
|
+
// other URLs, and the headers meant for one request would be applied
|
|
152
|
+
// to somebody else's. `|` is special only at the ends, where we put
|
|
153
|
+
// our own anchors, so an embedded one is already literal.
|
|
154
|
+
//
|
|
155
|
+
// URLs with a metacharacter go through `regexFilter` instead, where
|
|
156
|
+
// everything can be escaped exactly. Ordinary URLs keep the cheaper
|
|
157
|
+
// `urlFilter` path.
|
|
158
|
+
function hasUrlFilterMetacharacter(url) {
|
|
159
|
+
return url.includes('*') || url.includes('^');
|
|
160
|
+
}
|
|
161
|
+
// Escape for DNR's regexFilter (RE2). Every character that could be
|
|
162
|
+
// regex syntax is backslash-escaped, so the pattern matches the URL and
|
|
163
|
+
// nothing else. `/` is deliberately absent — it isn't syntax in RE2
|
|
164
|
+
// (there are no delimiters to close) and `\/` is what a stricter engine
|
|
165
|
+
// would reject.
|
|
166
|
+
function escapeForRegexFilter(url) {
|
|
167
|
+
return url.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
168
|
+
}
|
|
169
|
+
// Fragment tag that makes a request identifiable to its own rule. The
|
|
170
|
+
// rule id doubles as the tag: it is already unique among live rules,
|
|
171
|
+
// and tying the two together means a request and its rule can't
|
|
172
|
+
// disagree about which one it is. Prefixed so a tagged URL is
|
|
173
|
+
// recognizable in DevTools.
|
|
174
|
+
const XHR_URL_TAG_PREFIX = 'sm-xhr-';
|
|
175
|
+
// The URL as the browser will actually request it, which is what DNR
|
|
176
|
+
// matches against — not the string the script passed. `fetch`
|
|
177
|
+
// canonicalizes first: a space becomes `%20`, a bare origin gains its
|
|
178
|
+
// `/`, scheme and host lowercase, a default port drops, non-ASCII is
|
|
179
|
+
// percent-encoded or punycoded. Matching the raw string means the rule
|
|
180
|
+
// silently fails to apply, which for a `Cookie` override is worse than
|
|
181
|
+
// an error — the user's real session goes out in place of the one the
|
|
182
|
+
// script asked for.
|
|
183
|
+
//
|
|
184
|
+
// The fragment carries this request's tag, which is what makes the rule
|
|
185
|
+
// match one request rather than every request to that URL. Whatever the
|
|
186
|
+
// script put there is replaced: a fragment is never sent, and
|
|
187
|
+
// `Response.url` strips it again, so nothing the script can observe
|
|
188
|
+
// depends on it.
|
|
189
|
+
//
|
|
190
|
+
// A URL `new URL()` can't parse is handed back unchanged, untagged.
|
|
191
|
+
// Unreachable in practice — the `@connect` check rejects such a URL
|
|
192
|
+
// before `runXhr` is entered — and harmless if it were reached: the
|
|
193
|
+
// rule and the fetch still name the same string.
|
|
194
|
+
export function taggedRequestUrl(url, ruleId) {
|
|
195
|
+
try {
|
|
196
|
+
const parsed = new URL(url);
|
|
197
|
+
parsed.hash = `${XHR_URL_TAG_PREFIX}${ruleId}`;
|
|
198
|
+
return parsed.href;
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return url;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// This extension's id, which is the "domain" of a request it starts.
|
|
205
|
+
// Guarded because the rule builder is also exercised outside an
|
|
206
|
+
// extension (unit tests); without an id the rule keeps the broader
|
|
207
|
+
// `tabIds` condition alone rather than failing to build.
|
|
208
|
+
function selfInitiatorDomain() {
|
|
209
|
+
return typeof chrome !== 'undefined' ? chrome.runtime?.id : undefined;
|
|
210
|
+
}
|
|
211
|
+
// DNR session-rule ids. XHR rules start well above zero and climb,
|
|
212
|
+
// leaving the low ids free for any other subsystem that wants one.
|
|
213
|
+
//
|
|
214
|
+
// Everything from XHR_RULE_ID_MIN up is this module's band —
|
|
215
|
+
// `clearStaleHeaderRules` deletes any session rule in it at startup, so
|
|
216
|
+
// a future subsystem that wants its own session rule must stay below.
|
|
217
|
+
//
|
|
218
|
+
// Ids only have to be unique among *live* rules and each is released
|
|
219
|
+
// when its request settles, so the counter wraps. A collision needs
|
|
220
|
+
// ~999,000 requests in flight at once, or a stranded rule the sweep
|
|
221
|
+
// never reached; `installHeaderRule` clears the id before reusing it so
|
|
222
|
+
// neither turns into a failed install. That clear is not free of
|
|
223
|
+
// consequence, though: if the id really were still live, its request
|
|
224
|
+
// loses its overrides silently — so the band is sized to keep the wrap
|
|
225
|
+
// unreachable rather than to make the collision survivable.
|
|
226
|
+
const XHR_RULE_ID_MIN = 1000;
|
|
227
|
+
const XHR_RULE_ID_MAX = 1_000_000;
|
|
228
|
+
let nextRuleId = XHR_RULE_ID_MIN;
|
|
229
|
+
export function allocateRuleId() {
|
|
230
|
+
const id = nextRuleId;
|
|
231
|
+
nextRuleId = id >= XHR_RULE_ID_MAX ? XHR_RULE_ID_MIN : id + 1;
|
|
232
|
+
return id;
|
|
233
|
+
}
|
|
234
|
+
// Drop any XHR header rule left over from a previous service-worker
|
|
235
|
+
// life. Rules come off in `runXhr`'s `finally`, but a worker killed
|
|
236
|
+
// mid-request never reaches it, and a session rule outlives the worker
|
|
237
|
+
// — it would go on rewriting requests to that URL for the rest of the
|
|
238
|
+
// browser session. Called once at startup, when no XHR can be in
|
|
239
|
+
// flight yet.
|
|
240
|
+
let staleSweep = null;
|
|
241
|
+
// Resolves once the startup sweep has finished (immediately if it was
|
|
242
|
+
// never started). `installHeaderRule` waits on this so the sweep can't
|
|
243
|
+
// delete a rule added while it was reading the rule list.
|
|
244
|
+
export function whenStaleRulesCleared() {
|
|
245
|
+
return staleSweep ?? Promise.resolve();
|
|
246
|
+
}
|
|
247
|
+
export function clearStaleHeaderRules() {
|
|
248
|
+
staleSweep = runStaleSweep();
|
|
249
|
+
return staleSweep;
|
|
250
|
+
}
|
|
251
|
+
async function runStaleSweep() {
|
|
252
|
+
const dnr = chrome.declarativeNetRequest;
|
|
253
|
+
if (!dnr)
|
|
254
|
+
return;
|
|
255
|
+
try {
|
|
256
|
+
const rules = await dnr.getSessionRules();
|
|
257
|
+
const stale = rules.filter((r) => r.id >= XHR_RULE_ID_MIN).map((r) => r.id);
|
|
258
|
+
if (stale.length > 0)
|
|
259
|
+
await dnr.updateSessionRules({ removeRuleIds: stale });
|
|
260
|
+
}
|
|
261
|
+
catch (err) {
|
|
262
|
+
console.info('[SourceMonkey] could not clear stale GM_xmlhttpRequest header rules', err);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// Build the session rule that applies `surgical` to the request for
|
|
266
|
+
// `url`. Returns null when there's nothing to do — no overrides and
|
|
267
|
+
// nothing to remove — so the common request costs no rule at all.
|
|
268
|
+
export function buildHeaderRule(url, surgical, method = 'GET') {
|
|
269
|
+
const requestHeaders = [];
|
|
270
|
+
for (const [header, value] of Object.entries(surgical)) {
|
|
271
|
+
requestHeaders.push({
|
|
272
|
+
header,
|
|
273
|
+
value,
|
|
274
|
+
operation: 'set',
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
for (const header of defaultRemovedHeaders(method)) {
|
|
278
|
+
// A caller-supplied value wins over the default removal.
|
|
279
|
+
if (header in surgical)
|
|
280
|
+
continue;
|
|
281
|
+
requestHeaders.push({
|
|
282
|
+
header,
|
|
283
|
+
operation: 'remove',
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
if (requestHeaders.length === 0)
|
|
287
|
+
return null;
|
|
288
|
+
const initiator = selfInitiatorDomain();
|
|
289
|
+
const id = allocateRuleId();
|
|
290
|
+
// The id is also the URL tag, so the rule and the request it belongs
|
|
291
|
+
// to name each other. Everything else about the URL is canonicalized
|
|
292
|
+
// by `new URL()` on the way through, which is what `fetch` will do to
|
|
293
|
+
// it as well — a rule built from the raw string would match nothing.
|
|
294
|
+
const target = taggedRequestUrl(url, id);
|
|
295
|
+
return {
|
|
296
|
+
id,
|
|
297
|
+
target,
|
|
298
|
+
rule: {
|
|
299
|
+
id,
|
|
300
|
+
// Constant. Priority never has to arbitrate between our own
|
|
301
|
+
// rules: the URL tag means at most one of them matches any given
|
|
302
|
+
// request.
|
|
303
|
+
priority: 1,
|
|
304
|
+
action: {
|
|
305
|
+
type: 'modifyHeaders',
|
|
306
|
+
requestHeaders,
|
|
307
|
+
},
|
|
308
|
+
condition: {
|
|
309
|
+
// `|` anchors mean "the whole URL, exactly"; `^…$` is the
|
|
310
|
+
// regex equivalent for URLs that can't use urlFilter safely.
|
|
311
|
+
...(hasUrlFilterMetacharacter(target)
|
|
312
|
+
? { regexFilter: `^${escapeForRegexFilter(target)}$` }
|
|
313
|
+
: { urlFilter: `|${target}|` }),
|
|
314
|
+
isUrlFilterCaseSensitive: true,
|
|
315
|
+
// The tag alone already picks out one request, since nothing
|
|
316
|
+
// else asks for that URL. These narrow it anyway, so a rule
|
|
317
|
+
// can't reach beyond this extension even if a tag ever escaped:
|
|
318
|
+
// `tabIds: [-1]` is "not associated with a tab", which every
|
|
319
|
+
// extension request is and no page load is, and
|
|
320
|
+
// `initiatorDomains` pins it to requests this extension
|
|
321
|
+
// started. `tabIds` is allowed only on session rules, which is
|
|
322
|
+
// what these are.
|
|
323
|
+
//
|
|
324
|
+
// Verified rather than assumed: with a bogus initiator the
|
|
325
|
+
// header e2e tests fail, so the condition really is evaluated
|
|
326
|
+
// and really does match us.
|
|
327
|
+
tabIds: [-1],
|
|
328
|
+
...(initiator ? { initiatorDomains: [initiator] } : {}),
|
|
329
|
+
resourceTypes: ['xmlhttprequest'],
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|