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,141 @@
|
|
|
1
|
+
// SW-side `GM_notification` dispatcher.
|
|
2
|
+
//
|
|
3
|
+
// chrome.notifications wrapping. The page sends a details object
|
|
4
|
+
// describing the notification; we call chrome.notifications.create
|
|
5
|
+
// and remember the (chromeNotificationId ↔ originator + requestId)
|
|
6
|
+
// mapping so onClicked / onClosed can be routed back.
|
|
7
|
+
//
|
|
8
|
+
// chrome.notifications has its own assigned-id mechanism (we can
|
|
9
|
+
// pass an explicit notificationId or "" to have Chrome assign one).
|
|
10
|
+
// We always let Chrome assign — keeps our requestId space cleanly
|
|
11
|
+
// page-side and avoids collisions between userscripts.
|
|
12
|
+
import { dropEntriesForOriginator } from './gm-originator-map.js';
|
|
13
|
+
import { hasAllowedScheme, WEB_SCHEMES } from './gm-url-scheme.js';
|
|
14
|
+
const byChromeId = new Map();
|
|
15
|
+
// One string for the two guards that check the same thing: this one and
|
|
16
|
+
// `handleNotificationShow` in `background.ts`, which rejects before it
|
|
17
|
+
// ever calls in here. Names the browser permission the reader would go
|
|
18
|
+
// looking for rather than the API object we happened to test.
|
|
19
|
+
export const NOTIFICATIONS_UNAVAILABLE = 'this browser will not show notifications to SourceMonkey; check the extension\'s notification permission';
|
|
20
|
+
// Page-supplied notification image. Chrome fetches `iconUrl` from the
|
|
21
|
+
// privileged extension context, so we only honor http(s) / data: URLs.
|
|
22
|
+
// A `file://` or `chrome-extension://` image would let a script probe
|
|
23
|
+
// local files / packaged resources through the extension; reject those
|
|
24
|
+
// and fall back to the default icon.
|
|
25
|
+
function isAllowedIconUrl(image) {
|
|
26
|
+
return hasAllowedScheme(image, [...WEB_SCHEMES, 'data:']);
|
|
27
|
+
}
|
|
28
|
+
// Pick the icon URL chrome.notifications wants. The user-supplied
|
|
29
|
+
// image is preferred when it's an allowed scheme; otherwise we fall
|
|
30
|
+
// back to the extension's own icon so chrome.notifications doesn't
|
|
31
|
+
// reject the create call (iconUrl is required by the API).
|
|
32
|
+
function pickIconUrl(image) {
|
|
33
|
+
if (image && isAllowedIconUrl(image))
|
|
34
|
+
return image;
|
|
35
|
+
try {
|
|
36
|
+
return chrome.runtime.getURL('icons/icon-128.png');
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return '';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function showGmNotification(payload, originatorTabId) {
|
|
43
|
+
// Defense in depth — handleNotificationShow already guards this,
|
|
44
|
+
// but a future direct caller (test surface, retry path) shouldn't
|
|
45
|
+
// crash if the API is missing.
|
|
46
|
+
if (!chrome.notifications) {
|
|
47
|
+
return { ok: false, error: NOTIFICATIONS_UNAVAILABLE };
|
|
48
|
+
}
|
|
49
|
+
const d = payload.details;
|
|
50
|
+
const options = {
|
|
51
|
+
type: 'basic',
|
|
52
|
+
iconUrl: pickIconUrl(d.image),
|
|
53
|
+
title: d.title || 'SourceMonkey',
|
|
54
|
+
message: d.text,
|
|
55
|
+
silent: d.silent,
|
|
56
|
+
};
|
|
57
|
+
try {
|
|
58
|
+
// The installed @types/chrome declares the callback form only,
|
|
59
|
+
// but chrome.notifications.create is Promise-returning in MV3.
|
|
60
|
+
// Wrap to avoid the type mismatch and stay forward-compatible.
|
|
61
|
+
const chromeId = await new Promise((res, rej) => {
|
|
62
|
+
try {
|
|
63
|
+
chrome.notifications.create('', options, (id) => {
|
|
64
|
+
if (chrome.runtime.lastError) {
|
|
65
|
+
rej(new Error(chrome.runtime.lastError.message ?? 'notifications.create failed'));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
res(id);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
rej(e);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
const entry = {
|
|
76
|
+
originatorTabId,
|
|
77
|
+
requestId: payload.requestId,
|
|
78
|
+
scriptId: payload.scriptId,
|
|
79
|
+
timeoutTimer: null,
|
|
80
|
+
};
|
|
81
|
+
if (d.timeout > 0) {
|
|
82
|
+
entry.timeoutTimer = setTimeout(() => {
|
|
83
|
+
// chrome.notifications.clear fires its callback synchronously
|
|
84
|
+
// for our purposes; the onClosed listener will then dispatch
|
|
85
|
+
// the page-side ondone via the normal path.
|
|
86
|
+
try {
|
|
87
|
+
chrome.notifications.clear(chromeId);
|
|
88
|
+
}
|
|
89
|
+
catch { /* gone */ }
|
|
90
|
+
}, d.timeout);
|
|
91
|
+
}
|
|
92
|
+
byChromeId.set(chromeId, entry);
|
|
93
|
+
return { ok: true, notificationId: chromeId };
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
error: err instanceof Error ? err.message : String(err),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Called by the SW's chrome.notifications.onClicked listener.
|
|
103
|
+
export function takeForClicked(chromeId) {
|
|
104
|
+
const entry = byChromeId.get(chromeId);
|
|
105
|
+
if (!entry)
|
|
106
|
+
return null;
|
|
107
|
+
// Don't delete the entry on click — TM's `onclick` is NOT terminal.
|
|
108
|
+
// The user may click then the notification auto-closes (or stays
|
|
109
|
+
// open until they dismiss); we still want to deliver ondone later.
|
|
110
|
+
return {
|
|
111
|
+
originatorTabId: entry.originatorTabId,
|
|
112
|
+
requestId: entry.requestId,
|
|
113
|
+
scriptId: entry.scriptId,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
// Called by the SW's chrome.notifications.onClosed listener.
|
|
117
|
+
// Terminal — we free the entry.
|
|
118
|
+
export function takeForClosed(chromeId) {
|
|
119
|
+
const entry = byChromeId.get(chromeId);
|
|
120
|
+
if (!entry)
|
|
121
|
+
return null;
|
|
122
|
+
if (entry.timeoutTimer != null)
|
|
123
|
+
clearTimeout(entry.timeoutTimer);
|
|
124
|
+
byChromeId.delete(chromeId);
|
|
125
|
+
return {
|
|
126
|
+
originatorTabId: entry.originatorTabId,
|
|
127
|
+
requestId: entry.requestId,
|
|
128
|
+
scriptId: entry.scriptId,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
// Drop every entry whose originator tab just closed. Best-effort
|
|
132
|
+
// matches GM_openInTab's dropOriginator. The chrome notifications
|
|
133
|
+
// themselves stay on screen — chrome.notifications survive tab close
|
|
134
|
+
// (they're per-extension, not per-tab) — but with the originator
|
|
135
|
+
// gone we have nobody to route the eventual events to.
|
|
136
|
+
export function dropOriginator(originatorTabId) {
|
|
137
|
+
return dropEntriesForOriginator(byChromeId, originatorTabId, (entry) => {
|
|
138
|
+
if (entry.timeoutTimer != null)
|
|
139
|
+
clearTimeout(entry.timeoutTimer);
|
|
140
|
+
});
|
|
141
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Shared shed-on-tab-close helper for the per-request maps in
|
|
2
|
+
// `gm-tab.ts` (GM_openInTab), `gm-notification.ts` (GM_notification),
|
|
3
|
+
// and `gm-download.ts` (GM_download).
|
|
4
|
+
//
|
|
5
|
+
// Each keeps an in-memory `Map<key, { originatorTabId, … }>` and, when
|
|
6
|
+
// a tab that originated requests is itself closed, drops every entry
|
|
7
|
+
// that points at it (the opened tabs / on-screen notifications /
|
|
8
|
+
// running downloads stay; we just have nowhere left to route their
|
|
9
|
+
// eventual events). The iterate → match `originatorTabId` → per-entry
|
|
10
|
+
// cleanup → delete → collect-key loop is identical; only the per-entry
|
|
11
|
+
// cleanup differs (a secondary index vs. timers). Factored here so the
|
|
12
|
+
// dispatchers can't drift.
|
|
13
|
+
export function dropEntriesForOriginator(map, originatorTabId, cleanup) {
|
|
14
|
+
const dropped = [];
|
|
15
|
+
for (const [key, entry] of map) {
|
|
16
|
+
if (entry.originatorTabId !== originatorTabId)
|
|
17
|
+
continue;
|
|
18
|
+
cleanup(entry);
|
|
19
|
+
map.delete(key);
|
|
20
|
+
dropped.push(key);
|
|
21
|
+
}
|
|
22
|
+
return dropped;
|
|
23
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// SW-side store for `GM_getTab` / `GM_saveTab` / `GM_getTabs`: one
|
|
2
|
+
// plain object per (tab, script), kept while the tab is open.
|
|
3
|
+
//
|
|
4
|
+
// Behavior, as observed in TM:
|
|
5
|
+
// - `GM_getTab` gives this script's object for the sender's tab, `{}`
|
|
6
|
+
// when it has saved nothing there. The call itself counts as
|
|
7
|
+
// presence: the tab then shows in `GM_getTabs` with `{}`.
|
|
8
|
+
// - `GM_saveTab(obj)` replaces the object with a shallow copy. A null
|
|
9
|
+
// or undefined `obj` saves nothing.
|
|
10
|
+
// - `GM_getTabs` gives this script's object for every tab that has
|
|
11
|
+
// one, keyed by tab id.
|
|
12
|
+
// - Frames share their tab's object; the key is the tab, not the
|
|
13
|
+
// document.
|
|
14
|
+
// - Closing the tab drops its objects (`dropTabData`, from
|
|
15
|
+
// background.ts's `tabs.onRemoved`).
|
|
16
|
+
//
|
|
17
|
+
// Where it lives: in memory, mirrored to `chrome.storage.session` as one
|
|
18
|
+
// `gmtab:<tabId>` row per tab ({ [scriptId]: object }). The mirror is
|
|
19
|
+
// what keeps the data across the service worker being stopped while
|
|
20
|
+
// idle, which would otherwise lose it within a minute of quiet. Session
|
|
21
|
+
// storage also clears with the browser, the lifetime the API promises.
|
|
22
|
+
// The rows are first read on the first call after a worker start.
|
|
23
|
+
//
|
|
24
|
+
// Wire protocol (page → SW): `{ type: SM_TAB_DATA_MSG, scriptId,
|
|
25
|
+
// action: 'get' | 'save' | 'list', data? }` → `{ ok: true, value }` or
|
|
26
|
+
// `{ ok: false, error }`. Values cross as JSON, so they hold what JSON
|
|
27
|
+
// can.
|
|
28
|
+
export const SM_TAB_DATA_MSG = 'sm:gm-tab-data';
|
|
29
|
+
const KEY_PREFIX = 'gmtab:';
|
|
30
|
+
let tabs = null;
|
|
31
|
+
let loading = null;
|
|
32
|
+
function rowKey(tabId) {
|
|
33
|
+
return `${KEY_PREFIX}${tabId}`;
|
|
34
|
+
}
|
|
35
|
+
function load() {
|
|
36
|
+
if (tabs)
|
|
37
|
+
return Promise.resolve(tabs);
|
|
38
|
+
if (!loading) {
|
|
39
|
+
loading = (async () => {
|
|
40
|
+
const all = await chrome.storage.session.get(null);
|
|
41
|
+
const m = new Map();
|
|
42
|
+
for (const [key, value] of Object.entries(all)) {
|
|
43
|
+
if (!key.startsWith(KEY_PREFIX))
|
|
44
|
+
continue;
|
|
45
|
+
const tabId = Number(key.slice(KEY_PREFIX.length));
|
|
46
|
+
if (!Number.isInteger(tabId) || !value || typeof value !== 'object')
|
|
47
|
+
continue;
|
|
48
|
+
m.set(tabId, value);
|
|
49
|
+
}
|
|
50
|
+
tabs = m;
|
|
51
|
+
return m;
|
|
52
|
+
})().finally(() => { loading = null; });
|
|
53
|
+
}
|
|
54
|
+
return loading;
|
|
55
|
+
}
|
|
56
|
+
// Write one tab's row (or remove it once empty). On failure this
|
|
57
|
+
// script's entry is put back as it was, so memory and the row don't
|
|
58
|
+
// disagree; other scripts' entries, which may have changed since, stay.
|
|
59
|
+
async function persist(m, tabId, scriptId, prev) {
|
|
60
|
+
const next = m.get(tabId);
|
|
61
|
+
try {
|
|
62
|
+
if (next && Object.keys(next).length > 0) {
|
|
63
|
+
await chrome.storage.session.set({ [rowKey(tabId)]: next });
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
await chrome.storage.session.remove(rowKey(tabId));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
const { [scriptId]: _failed, ...rest } = m.get(tabId) ?? {};
|
|
71
|
+
const restored = prev ? { ...rest, [scriptId]: prev } : rest;
|
|
72
|
+
if (Object.keys(restored).length > 0)
|
|
73
|
+
m.set(tabId, restored);
|
|
74
|
+
else
|
|
75
|
+
m.delete(tabId);
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export async function getTabData(tabId, scriptId) {
|
|
80
|
+
const m = await load();
|
|
81
|
+
const prev = m.get(tabId);
|
|
82
|
+
const existing = prev?.[scriptId];
|
|
83
|
+
if (existing)
|
|
84
|
+
return existing;
|
|
85
|
+
m.set(tabId, { ...prev, [scriptId]: {} });
|
|
86
|
+
await persist(m, tabId, scriptId, undefined);
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
export async function saveTabData(tabId, scriptId, data) {
|
|
90
|
+
if (data == null)
|
|
91
|
+
return;
|
|
92
|
+
if (typeof data !== 'object')
|
|
93
|
+
throw new Error('The tab object must be an object');
|
|
94
|
+
const m = await load();
|
|
95
|
+
const byScript = m.get(tabId);
|
|
96
|
+
const prev = byScript?.[scriptId];
|
|
97
|
+
m.set(tabId, { ...byScript, [scriptId]: { ...data } });
|
|
98
|
+
await persist(m, tabId, scriptId, prev);
|
|
99
|
+
}
|
|
100
|
+
export async function listTabData(scriptId) {
|
|
101
|
+
const m = await load();
|
|
102
|
+
const out = {};
|
|
103
|
+
for (const [tabId, byScript] of m) {
|
|
104
|
+
const obj = byScript[scriptId];
|
|
105
|
+
if (obj)
|
|
106
|
+
out[tabId] = obj;
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
// The tab closed. Waits out a first load already under way, which may
|
|
111
|
+
// have read the row before it goes.
|
|
112
|
+
export async function dropTabData(tabId) {
|
|
113
|
+
const m = tabs ?? (loading ? await loading : null);
|
|
114
|
+
m?.delete(tabId);
|
|
115
|
+
await chrome.storage.session.remove(rowKey(tabId));
|
|
116
|
+
}
|
|
117
|
+
// For the test surface's `resetAllState`, which clears session storage:
|
|
118
|
+
// the memory copy came from it.
|
|
119
|
+
export function forgetTabDataForTests() {
|
|
120
|
+
tabs = null;
|
|
121
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// SW-side `GM_openInTab` dispatcher.
|
|
2
|
+
//
|
|
3
|
+
// Owns the (requestId ↔ chrome tab id ↔ originator tab id) mapping
|
|
4
|
+
// so we can route close events back to the page that opened the tab.
|
|
5
|
+
// Per-call state is small; we keep two maps for O(1) lookup either
|
|
6
|
+
// way:
|
|
7
|
+
//
|
|
8
|
+
// requestState: requestId -> { tabId?, originatorTabId, scriptId }
|
|
9
|
+
// tabIdToRequest: chromeTabId -> requestId
|
|
10
|
+
//
|
|
11
|
+
// The maps live in memory only. On SW restart the mapping is lost —
|
|
12
|
+
// userscripts that hold a control handle from before the restart
|
|
13
|
+
// stop receiving close notifications. That's the same boat as XHR
|
|
14
|
+
// requests in flight; acceptable for v1.
|
|
15
|
+
import { dropEntriesForOriginator } from './gm-originator-map.js';
|
|
16
|
+
import { hasAllowedScheme, WEB_SCHEMES } from './gm-url-scheme.js';
|
|
17
|
+
const requestState = new Map();
|
|
18
|
+
const tabIdToRequest = new Map();
|
|
19
|
+
// Only http(s) targets are allowed. The URL comes straight from
|
|
20
|
+
// page-supplied message payload, so without this gate a script could
|
|
21
|
+
// ask the privileged SW to open `file://`, `chrome://`, or
|
|
22
|
+
// `chrome-extension://` tabs (local-file disclosure, settings pages,
|
|
23
|
+
// other extensions' pages). TM/VM's GM_openInTab is web-oriented;
|
|
24
|
+
// restricting to http/https matches the realistic use and closes the
|
|
25
|
+
// scheme-smuggling hole.
|
|
26
|
+
export function isAllowedTabUrl(url) {
|
|
27
|
+
return hasAllowedScheme(url, WEB_SCHEMES);
|
|
28
|
+
}
|
|
29
|
+
// The refusal a script sees for a URL `isAllowedTabUrl` rejects.
|
|
30
|
+
export function tabUrlRefusedMessage(url) {
|
|
31
|
+
return `GM_openInTab: refusing non-http(s) URL ${url}`;
|
|
32
|
+
}
|
|
33
|
+
// Open a new tab on behalf of a script. Returns the assigned chrome
|
|
34
|
+
// tab id (or an error) and stores the originator mapping so close
|
|
35
|
+
// events can be routed back.
|
|
36
|
+
export async function openGmTab(payload, originatorTabId,
|
|
37
|
+
// Run on the new tab before it loads the URL: it opens blank, this
|
|
38
|
+
// runs, then the URL loads. For a tab sm-dev controls, whose claim
|
|
39
|
+
// has to be on the new tab before its scripts ask (sm-dev-link.ts).
|
|
40
|
+
beforeLoad) {
|
|
41
|
+
if (!isAllowedTabUrl(payload.url)) {
|
|
42
|
+
return { ok: false, error: tabUrlRefusedMessage(payload.url) };
|
|
43
|
+
}
|
|
44
|
+
// Stash the entry first so a close event arriving between
|
|
45
|
+
// chrome.tabs.create resolving and the page receiving the reply
|
|
46
|
+
// can still find the request. The originator can't have called
|
|
47
|
+
// close() yet because it doesn't have the tabId yet, but the user
|
|
48
|
+
// could theoretically close the new tab manually that fast.
|
|
49
|
+
requestState.set(payload.requestId, {
|
|
50
|
+
originatorTabId,
|
|
51
|
+
scriptId: payload.scriptId,
|
|
52
|
+
tabId: null,
|
|
53
|
+
closeRequested: false,
|
|
54
|
+
});
|
|
55
|
+
try {
|
|
56
|
+
const created = await chrome.tabs.create({
|
|
57
|
+
url: beforeLoad ? 'about:blank' : payload.url,
|
|
58
|
+
active: payload.active,
|
|
59
|
+
// Bias positioning: when `insert` is true, hint Chrome to place
|
|
60
|
+
// the new tab next to the originator.
|
|
61
|
+
...(payload.insert ? { openerTabId: originatorTabId } : {}),
|
|
62
|
+
});
|
|
63
|
+
if (created.id == null) {
|
|
64
|
+
requestState.delete(payload.requestId);
|
|
65
|
+
return { ok: false, error: 'internal error: the tab opened but Chrome reported no id for it' };
|
|
66
|
+
}
|
|
67
|
+
const entry = requestState.get(payload.requestId);
|
|
68
|
+
if (!entry) {
|
|
69
|
+
// Request was already aborted; close the tab we just opened so
|
|
70
|
+
// we don't leak it.
|
|
71
|
+
try {
|
|
72
|
+
await chrome.tabs.remove(created.id);
|
|
73
|
+
}
|
|
74
|
+
catch { /* gone */ }
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
error: 'internal error: the tab request was dropped before the tab opened',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
entry.tabId = created.id;
|
|
81
|
+
tabIdToRequest.set(created.id, payload.requestId);
|
|
82
|
+
if (beforeLoad) {
|
|
83
|
+
try {
|
|
84
|
+
await beforeLoad(created.id);
|
|
85
|
+
// Closed meanwhile, by the script or the person: nothing to load.
|
|
86
|
+
if (!entry.closeRequested)
|
|
87
|
+
await chrome.tabs.update(created.id, { url: payload.url });
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
// The tab opened, so the script is told so. A tab gone before
|
|
91
|
+
// its URL loaded is reported closed by the onRemoved listener.
|
|
92
|
+
console.info('[SourceMonkey] GM_openInTab: could not load the URL into the new tab:', err);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (entry.closeRequested) {
|
|
96
|
+
// The page called close() before create resolved. Close
|
|
97
|
+
// immediately — the onRemoved listener will then clear the
|
|
98
|
+
// mapping and route the close event back to the originator.
|
|
99
|
+
try {
|
|
100
|
+
await chrome.tabs.remove(created.id);
|
|
101
|
+
}
|
|
102
|
+
catch { /* gone */ }
|
|
103
|
+
}
|
|
104
|
+
return { ok: true, tabId: created.id };
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
requestState.delete(payload.requestId);
|
|
108
|
+
return {
|
|
109
|
+
ok: false,
|
|
110
|
+
error: err instanceof Error ? err.message : String(err),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Close a tab by requestId. Idempotent — close on a tab that's
|
|
115
|
+
// already gone is a no-op.
|
|
116
|
+
export async function closeGmTab(requestId) {
|
|
117
|
+
const entry = requestState.get(requestId);
|
|
118
|
+
if (!entry)
|
|
119
|
+
return;
|
|
120
|
+
if (entry.tabId == null) {
|
|
121
|
+
// chrome.tabs.create hasn't resolved yet. Flag the entry; the
|
|
122
|
+
// openGmTab path checks closeRequested after create resolves and
|
|
123
|
+
// closes the just-opened tab. Avoids a "open-then-leak" when
|
|
124
|
+
// close() fires faster than create.
|
|
125
|
+
entry.closeRequested = true;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const tabId = entry.tabId;
|
|
129
|
+
try {
|
|
130
|
+
await chrome.tabs.remove(tabId);
|
|
131
|
+
}
|
|
132
|
+
catch { /* tab already gone; the onRemoved listener handles cleanup */ }
|
|
133
|
+
}
|
|
134
|
+
// Called by the SW's chrome.tabs.onRemoved listener. Returns the
|
|
135
|
+
// originator that needs notifying (or null if this wasn't a
|
|
136
|
+
// GM_openInTab-created tab). The caller is responsible for the
|
|
137
|
+
// actual sendMessage so this module stays independent of the broader
|
|
138
|
+
// message-routing surface.
|
|
139
|
+
export function takeRequestForClosedTab(tabId) {
|
|
140
|
+
const requestId = tabIdToRequest.get(tabId);
|
|
141
|
+
if (!requestId)
|
|
142
|
+
return null;
|
|
143
|
+
tabIdToRequest.delete(tabId);
|
|
144
|
+
const entry = requestState.get(requestId);
|
|
145
|
+
if (!entry)
|
|
146
|
+
return null;
|
|
147
|
+
requestState.delete(requestId);
|
|
148
|
+
return { originatorTabId: entry.originatorTabId, requestId, scriptId: entry.scriptId };
|
|
149
|
+
}
|
|
150
|
+
// Drop every entry pointing at this originator tab — used when a tab
|
|
151
|
+
// containing a userscript that opened other tabs is itself closed.
|
|
152
|
+
// The opened tabs stay open (matches TM/VM), but we can't route close
|
|
153
|
+
// notifications anywhere useful, so we shed the mapping. Returns the
|
|
154
|
+
// dropped requestIds for logging.
|
|
155
|
+
export function dropOriginator(originatorTabId) {
|
|
156
|
+
return dropEntriesForOriginator(requestState, originatorTabId, (entry) => {
|
|
157
|
+
if (entry.tabId != null)
|
|
158
|
+
tabIdToRequest.delete(entry.tabId);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// SW-side driver for `@grant window.onurlchange`.
|
|
2
|
+
//
|
|
3
|
+
// Why the service worker is involved at all: a USER_SCRIPT-world
|
|
4
|
+
// script does NOT share `window.history` with the page. Patching
|
|
5
|
+
// `history.pushState` from the isolated world produces a patched
|
|
6
|
+
// function only isolated-world code ever calls — the page's own SPA
|
|
7
|
+
// router keeps calling the real one, unobserved. The isolated world
|
|
8
|
+
// does receive `popstate` / `hashchange`, but those cover only part of
|
|
9
|
+
// the surface and overlap the events below, so this module is the
|
|
10
|
+
// single source rather than one of two.
|
|
11
|
+
//
|
|
12
|
+
// `chrome.webNavigation` reports the whole surface:
|
|
13
|
+
//
|
|
14
|
+
// - `onHistoryStateUpdated` — pushState / replaceState and
|
|
15
|
+
// same-document traversals
|
|
16
|
+
// - `onReferenceFragmentUpdated` — fragment-only changes
|
|
17
|
+
//
|
|
18
|
+
// These two are *also* what Tampermonkey feeds its own `urlchange`
|
|
19
|
+
// from (verified in TM 5.5.0's `background.js`), which is why our
|
|
20
|
+
// firing rules match theirs for free: nothing on initial load,
|
|
21
|
+
// nothing on a cross-document navigation, one event per reported
|
|
22
|
+
// history update — including a same-URL `replaceState`, which TM does
|
|
23
|
+
// not suppress. The `webNavigation` permission is already in the
|
|
24
|
+
// manifest and `background.ts` already subscribes to
|
|
25
|
+
// `onHistoryStateUpdated` for tab status, so this costs no new
|
|
26
|
+
// privilege.
|
|
27
|
+
//
|
|
28
|
+
// The alternative — registering a MAIN-world helper that patches the
|
|
29
|
+
// page's `history` and postMessages across — was rejected: it
|
|
30
|
+
// pollutes every page's globals with a wrapper the page can see,
|
|
31
|
+
// unpatch, or use to forge events, for a signal Chrome already hands
|
|
32
|
+
// us for free.
|
|
33
|
+
//
|
|
34
|
+
// Wire protocol:
|
|
35
|
+
// SW → page: `{ type: 'sm:url-change', url }` via
|
|
36
|
+
// `chrome.tabs.sendMessage(tabId, msg, { documentId })`, falling
|
|
37
|
+
// back to `{ frameId }` (see `urlChangeTarget`). The URL is
|
|
38
|
+
// replayed verbatim to the userscript, so a burst of history
|
|
39
|
+
// updates produces one event each rather than a coalesced one.
|
|
40
|
+
//
|
|
41
|
+
// No gating: we notify on every reported update rather than tracking
|
|
42
|
+
// which tabs have a granting script. A stateful gate would have to
|
|
43
|
+
// survive SW restarts (chrome.userScripts registrations do; SW memory
|
|
44
|
+
// doesn't), and getting that wrong fails silently. The message is
|
|
45
|
+
// cheap — the SW is already awake handling this exact event for tab
|
|
46
|
+
// status — and a frame with no SourceMonkey runtime just rejects the
|
|
47
|
+
// send, which we swallow.
|
|
48
|
+
export const SM_URL_CHANGE_MSG = 'sm:url-change';
|
|
49
|
+
// Pure: should this history update be forwarded to a frame?
|
|
50
|
+
//
|
|
51
|
+
// Chrome reports events for pre-render / devtools targets with a
|
|
52
|
+
// negative `tabId`, which `chrome.tabs.sendMessage` can't address. A
|
|
53
|
+
// URL-less event has nothing worth reporting. Everything else is
|
|
54
|
+
// forwarded, including `file://` (SourceMonkey injects there too).
|
|
55
|
+
//
|
|
56
|
+
// Sub-frames pass the filter, but the targeting below is what
|
|
57
|
+
// actually matters for them: a sub-frame's navigation must never
|
|
58
|
+
// be delivered to the top frame's listeners as if the document had
|
|
59
|
+
// moved. A sub-frame with no script in it simply has no receiver.
|
|
60
|
+
export function shouldNotifyUrlChange(details) {
|
|
61
|
+
if (typeof details.tabId !== 'number' || details.tabId < 0)
|
|
62
|
+
return false;
|
|
63
|
+
if (typeof details.frameId !== 'number' || details.frameId < 0)
|
|
64
|
+
return false;
|
|
65
|
+
return typeof details.url === 'string' && details.url !== '';
|
|
66
|
+
}
|
|
67
|
+
// Which receiver the message is addressed to.
|
|
68
|
+
//
|
|
69
|
+
// Prefer `documentId` over `frameId`: a frame ID names a *slot*, not a
|
|
70
|
+
// document. The send is asynchronous relative to the navigation event,
|
|
71
|
+
// so if that frame commits a cross-document navigation in between, a
|
|
72
|
+
// frame-addressed message lands in a document that was never at this
|
|
73
|
+
// URL and its listeners report a URL the page isn't on. A document ID
|
|
74
|
+
// names the exact document, and the send simply finds no receiver once
|
|
75
|
+
// that document is gone — which is the outcome we want.
|
|
76
|
+
//
|
|
77
|
+
// Only one of the two is passed: Chrome treats them as alternatives,
|
|
78
|
+
// not as a pair to intersect.
|
|
79
|
+
export function urlChangeTarget(details) {
|
|
80
|
+
return typeof details.documentId === 'string' && details.documentId !== ''
|
|
81
|
+
? { documentId: details.documentId }
|
|
82
|
+
: { frameId: details.frameId };
|
|
83
|
+
}
|
|
84
|
+
// Fire-and-forget notification to the document that changed. Rejections
|
|
85
|
+
// are expected and swallowed: pages with no active SourceMonkey script
|
|
86
|
+
// have no GM runtime registered, so there is no receiving end.
|
|
87
|
+
export function notifyUrlChange(details) {
|
|
88
|
+
if (!shouldNotifyUrlChange(details))
|
|
89
|
+
return;
|
|
90
|
+
void chrome.tabs
|
|
91
|
+
.sendMessage(details.tabId, { type: SM_URL_CHANGE_MSG, url: details.url }, urlChangeTarget(details))
|
|
92
|
+
.catch(() => undefined);
|
|
93
|
+
}
|
|
94
|
+
// Subscribe to both same-document navigation events. Called once from
|
|
95
|
+
// `installNavigationListeners` in background.ts.
|
|
96
|
+
//
|
|
97
|
+
// This registers its own listeners rather than piggy-backing on the
|
|
98
|
+
// tab-status `onHistoryStateUpdated` listener already in background.ts:
|
|
99
|
+
// that one filters to the main frame, and a userscript inside an
|
|
100
|
+
// iframe wants its own frame's navigations too.
|
|
101
|
+
export function installUrlChangeWatch() {
|
|
102
|
+
const wn = chrome.webNavigation;
|
|
103
|
+
if (!wn?.onHistoryStateUpdated || !wn?.onReferenceFragmentUpdated) {
|
|
104
|
+
// Matches what TM logs in the same situation. `console.info`, not
|
|
105
|
+
// `.warn` — see the logging policy in CLAUDE.md.
|
|
106
|
+
console.info('[SourceMonkey] chrome.webNavigation same-document events unavailable; window.onurlchange will not fire');
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
wn.onHistoryStateUpdated.addListener((d) => notifyUrlChange(d));
|
|
110
|
+
wn.onReferenceFragmentUpdated.addListener((d) => notifyUrlChange(d));
|
|
111
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Shared URL-scheme gate for the SW-side `GM_*` dispatchers.
|
|
2
|
+
//
|
|
3
|
+
// Every dispatcher that hands a page-supplied URL to a privileged
|
|
4
|
+
// Chrome API (`chrome.tabs.create`, `chrome.notifications.create`,
|
|
5
|
+
// `chrome.downloads.download`) has to answer the same question first:
|
|
6
|
+
// is this a scheme we're willing to act on? Without the gate a
|
|
7
|
+
// userscript could ask the extension to open, fetch, or save
|
|
8
|
+
// `file:///…`, `chrome://…`, or `chrome-extension://<id>/…` — reaching
|
|
9
|
+
// past what the page itself could do.
|
|
10
|
+
//
|
|
11
|
+
// The allowed set differs per API (tabs and downloads take http(s);
|
|
12
|
+
// notification icons also take `data:`), so the caller passes it in.
|
|
13
|
+
// Only the parse-and-compare is shared — that's the part that was
|
|
14
|
+
// previously copied three times and could drift.
|
|
15
|
+
// True iff `url` parses and its scheme is one of `allowed`.
|
|
16
|
+
// `allowed` entries include the trailing colon (`'http:'`), matching
|
|
17
|
+
// `URL.protocol`. Anything unparseable is refused.
|
|
18
|
+
export function hasAllowedScheme(url, allowed) {
|
|
19
|
+
try {
|
|
20
|
+
return allowed.includes(new URL(url).protocol);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// The common case: plain web URLs.
|
|
27
|
+
export const WEB_SCHEMES = Object.freeze(['http:', 'https:']);
|