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,186 @@
|
|
|
1
|
+
// Bounded-concurrency fan-out.
|
|
2
|
+
//
|
|
3
|
+
// Every fan-out over a collection's scripts runs through here, because
|
|
4
|
+
// the unbounded form is a crash rather than a slowdown. One promise per
|
|
5
|
+
// scanned file means one simultaneous fetch per script, each holding a
|
|
6
|
+
// decoded body plus any prefetched `@require` / `@resource` payloads;
|
|
7
|
+
// across a few thousand scripts that peak is enough to take the whole
|
|
8
|
+
// extension process down with an out-of-memory abort. Chrome surfaces
|
|
9
|
+
// that only as "this extension crashed", with no JavaScript error
|
|
10
|
+
// anywhere, so it is not a failure the logs will ever explain.
|
|
11
|
+
//
|
|
12
|
+
// Bounds the in-flight working set, not the total: callers still retain
|
|
13
|
+
// every result, so a large enough collection is still a large enough
|
|
14
|
+
// peak.
|
|
15
|
+
// ---------- Refresh limits ----------
|
|
16
|
+
//
|
|
17
|
+
// These nest, so read them as a product. Refreshing one collection uses
|
|
18
|
+
// only the inner limit; "Refresh All" runs the outer limit's worth of
|
|
19
|
+
// collections, each with its own inner set:
|
|
20
|
+
//
|
|
21
|
+
// worst-case fetches in flight
|
|
22
|
+
// = FETCH_COLLECTIONS_CONCURRENCY × FETCH_SCRIPTS_PER_COLLECTION_CONCURRENCY
|
|
23
|
+
//
|
|
24
|
+
// The balance leans on the inner one because that's the limit the
|
|
25
|
+
// common cases actually hit: most profiles have one or a few
|
|
26
|
+
// collections, and a per-collection Refresh button never engages the
|
|
27
|
+
// outer limit at all. Making the outer limit the larger of the two
|
|
28
|
+
// would only speed up a profile with many collections while capping the
|
|
29
|
+
// single-collection case that everyone hits.
|
|
30
|
+
// How many scripts a single collection's refresh fetches at once.
|
|
31
|
+
//
|
|
32
|
+
// Chosen for memory, not throughput: each in-flight entry can hold a
|
|
33
|
+
// script body plus its dependency payloads, so the cap is what keeps
|
|
34
|
+
// peak usage proportional to the limit instead of to the collection
|
|
35
|
+
// size. Large enough to keep the network and the file reader busy. (For
|
|
36
|
+
// an HTTP/1.1 host the browser's own ~6-per-host connection limit is a
|
|
37
|
+
// second backstop in the same range, but that is not something to lean
|
|
38
|
+
// on: an HTTP/2 host multiplexes and imposes no such bound, and file://
|
|
39
|
+
// reads have no per-host limit at all.)
|
|
40
|
+
export const FETCH_SCRIPTS_PER_COLLECTION_CONCURRENCY = 16;
|
|
41
|
+
// How many collections a "Refresh All" pass works on at once.
|
|
42
|
+
//
|
|
43
|
+
// Deliberately the smaller of the pair — see the note above. It exists
|
|
44
|
+
// to stop the product from exploding on a many-collection profile, not
|
|
45
|
+
// to make that profile fast.
|
|
46
|
+
export const FETCH_COLLECTIONS_CONCURRENCY = 2;
|
|
47
|
+
// ---------- Directory-scan limits ----------
|
|
48
|
+
//
|
|
49
|
+
// A scan lists a directory, then lists each subdirectory it found. Those
|
|
50
|
+
// siblings are independent, but walking them with an `await` in the loop
|
|
51
|
+
// made the whole scan one round trip per directory — measured at 195ms
|
|
52
|
+
// for a local tree of 78 directories, and 1151ms for a GitHub repo whose
|
|
53
|
+
// listing needs four API calls. The depth cap means these nest at most
|
|
54
|
+
// twice, so read each as a product with itself.
|
|
55
|
+
// Sibling `file://` directories listed at once.
|
|
56
|
+
//
|
|
57
|
+
// The larger of the pair: a local listing is a cheap read with no
|
|
58
|
+
// per-host connection limit to respect, and deep trees are the common
|
|
59
|
+
// shape (a userscripts folder organised by site).
|
|
60
|
+
export const SCAN_DIRS_CONCURRENCY = 12;
|
|
61
|
+
// Sibling GitHub directories listed at once.
|
|
62
|
+
//
|
|
63
|
+
// Deliberately small, and NOT for memory: every listing is a request
|
|
64
|
+
// against api.github.com, which applies a secondary limit to bursts of
|
|
65
|
+
// concurrent requests from one client on top of the hourly quota. Being
|
|
66
|
+
// throttled costs far more than the serialization this saves, and the
|
|
67
|
+
// win here is mostly the first fan-out anyway — a repo needing four
|
|
68
|
+
// calls goes from four round trips to two.
|
|
69
|
+
export const SCAN_GITHUB_DIRS_CONCURRENCY = 4;
|
|
70
|
+
// ---------- Update-probe limits ----------
|
|
71
|
+
//
|
|
72
|
+
// Their own pair rather than a reuse of the refresh limits above: the
|
|
73
|
+
// scripts handed to one probe pass can span every collection, so neither
|
|
74
|
+
// refresh limit describes the shape. Same nesting rule, though — read
|
|
75
|
+
// them as a product.
|
|
76
|
+
// How many scripts are probed for updates at once.
|
|
77
|
+
//
|
|
78
|
+
// This is the one that binds — a script yields at most two probe URLs,
|
|
79
|
+
// so the inner limit never does.
|
|
80
|
+
//
|
|
81
|
+
// It was 3, on the reasoning that a probe is an annotation nothing
|
|
82
|
+
// waits on. That is true of the per-script caller, which fires and
|
|
83
|
+
// forgets on the navigation path, and false of the collection ones: a
|
|
84
|
+
// Refresh / Refresh All awaits the probe pass so the row is current
|
|
85
|
+
// when the spinner stops. The limit sized for the caller that doesn't
|
|
86
|
+
// wait was being paid by the two that do — measured at 1772ms for 30
|
|
87
|
+
// probed scripts, ten serialized rounds of a couple-KB header fetch.
|
|
88
|
+
//
|
|
89
|
+
// So it is sized for the awaiting caller now. Still bounded, because
|
|
90
|
+
// the pass can span every collection at once and the nesting rule below
|
|
91
|
+
// still applies; a probe is also cheap in the way that matters here —
|
|
92
|
+
// it holds one small response, not a script body plus its dependency
|
|
93
|
+
// payloads, which is what forces the refresh limits to stay low.
|
|
94
|
+
export const PROBE_SCRIPTS_CONCURRENCY = 10;
|
|
95
|
+
// How many of ONE script's probe URLs are fetched at once.
|
|
96
|
+
//
|
|
97
|
+
// A guard rather than a working limit: `probeTargets` yields at most two
|
|
98
|
+
// entries per script (`@updateURL` and `@downloadURL`, deduped by URL),
|
|
99
|
+
// so the list never reaches this cap. It is here so that adding a third
|
|
100
|
+
// probe target kind can't quietly turn the per-script step into an
|
|
101
|
+
// unbounded fan-out — the failure this whole module exists to prevent.
|
|
102
|
+
export const PROBE_URLS_PER_SCRIPT_CONCURRENCY = 4;
|
|
103
|
+
// ---------- Parse-check limit ----------
|
|
104
|
+
// How many rows the registration-time re-check compiles at once
|
|
105
|
+
// (`verifyParseChecks`).
|
|
106
|
+
//
|
|
107
|
+
// The list this bounds is normally empty — a row keeps its verdict
|
|
108
|
+
// until its content changes. The pass that isn't empty is the first
|
|
109
|
+
// sync after a SourceMonkey update: `parseCheckKey` carries the
|
|
110
|
+
// extension version (how we wrap the program decides what parses), so
|
|
111
|
+
// an upgrade makes every stored row stale at once, and every compile
|
|
112
|
+
// composes the body plus each `@require` in memory before shipping it
|
|
113
|
+
// to the sandbox. Registration waits on the whole pass, so this is a
|
|
114
|
+
// memory bound on a step that runs before any script is injected.
|
|
115
|
+
export const PARSE_RECHECK_CONCURRENCY = 4;
|
|
116
|
+
// Run `fn` over `items` with at most `limit` calls outstanding.
|
|
117
|
+
//
|
|
118
|
+
// Results come back in `items` order regardless of completion order, so
|
|
119
|
+
// this is a drop-in replacement for `Promise.all(items.map(fn))`.
|
|
120
|
+
//
|
|
121
|
+
// Rejection behavior also matches `Promise.all`, in both halves — and
|
|
122
|
+
// the second half is the subtle one:
|
|
123
|
+
//
|
|
124
|
+
// - The first rejection rejects the whole call immediately. The caller
|
|
125
|
+
// does not wait out the rest of the list to hear about a failure.
|
|
126
|
+
// - Every item still RUNS. No work is cancelled (there is nothing to
|
|
127
|
+
// cancel it with); the pool keeps draining in the background after
|
|
128
|
+
// the caller has been rejected, and those results are discarded.
|
|
129
|
+
//
|
|
130
|
+
// The second half is why a worker swallows its item's rejection instead
|
|
131
|
+
// of letting it break the loop. A throwing `fn` that killed its worker
|
|
132
|
+
// would be fine for a one-off failure — the other workers drain the
|
|
133
|
+
// list — but a SYSTEMIC failure (every item throws) would kill every
|
|
134
|
+
// worker in turn and silently skip the tail of the list. That is the
|
|
135
|
+
// case that matters here: an out-of-quota `chrome.storage.local` write
|
|
136
|
+
// fails for every collection, not just one.
|
|
137
|
+
//
|
|
138
|
+
// Callers that need per-item failures to be survivable should return a
|
|
139
|
+
// failure value rather than throwing, which is what the fetch paths do.
|
|
140
|
+
export async function mapWithConcurrency(items, limit, fn) {
|
|
141
|
+
if (items.length === 0)
|
|
142
|
+
return [];
|
|
143
|
+
// A limit at or above the item count has no work to do beyond
|
|
144
|
+
// Promise.all, and a non-positive limit would deadlock the workers
|
|
145
|
+
// below — treat both as "no bound".
|
|
146
|
+
if (limit >= items.length || limit <= 0) {
|
|
147
|
+
return await Promise.all(items.map((item, i) => fn(item, i)));
|
|
148
|
+
}
|
|
149
|
+
const out = new Array(items.length);
|
|
150
|
+
// Rejects the caller the moment the first item fails, while the pool
|
|
151
|
+
// below carries on draining. Never resolves — on the success path the
|
|
152
|
+
// drain is what settles the race.
|
|
153
|
+
let failNow;
|
|
154
|
+
const firstFailure = new Promise((_, reject) => { failNow = reject; });
|
|
155
|
+
let failed = false;
|
|
156
|
+
// Shared cursor: each worker repeatedly claims the next unstarted
|
|
157
|
+
// index. Self-balancing — a worker that draws a slow item doesn't hold
|
|
158
|
+
// up items a free worker could be running, which a fixed chunked split
|
|
159
|
+
// would.
|
|
160
|
+
let next = 0;
|
|
161
|
+
const worker = async () => {
|
|
162
|
+
for (;;) {
|
|
163
|
+
const i = next++;
|
|
164
|
+
if (i >= items.length)
|
|
165
|
+
return;
|
|
166
|
+
try {
|
|
167
|
+
out[i] = await fn(items[i], i);
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
// Reported, not rethrown: a worker that died here would stop
|
|
171
|
+
// claiming items, and if every item fails that skips the tail of
|
|
172
|
+
// the list entirely. See the note on rejection above.
|
|
173
|
+
if (!failed) {
|
|
174
|
+
failed = true;
|
|
175
|
+
failNow(err);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
const drained = Promise.all(Array.from({ length: limit }, () => worker()));
|
|
181
|
+
// Workers never reject, so `drained` can only resolve — the race
|
|
182
|
+
// settles on `firstFailure` for a failure and on `drained` otherwise,
|
|
183
|
+
// and neither branch leaves an unhandled rejection behind.
|
|
184
|
+
await Promise.race([drained, firstFailure]);
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// What the user has told SourceMonkey about the hosts a script's
|
|
2
|
+
// `GM_xmlhttpRequest` may reach beyond its `@connect` rules: the
|
|
3
|
+
// answers to the connection prompt (`connect-prompt.ts`), read by the
|
|
4
|
+
// worker before it asks and by the script's page, which lists them
|
|
5
|
+
// under Network access and can take them back.
|
|
6
|
+
//
|
|
7
|
+
// - Allowed and denied hosts persist in one row per script
|
|
8
|
+
// (`chrome.storage.local`), across refreshes and updates of the
|
|
9
|
+
// script, until reset on the script's page. The row goes when the
|
|
10
|
+
// script is removed.
|
|
11
|
+
//
|
|
12
|
+
// Hosts are `URL.hostname`s, lowercased: one host each, no subdomains,
|
|
13
|
+
// any port (as `@connect` matches hosts).
|
|
14
|
+
import { assertWorkerContext } from '../run-context.js';
|
|
15
|
+
// The prompt window's page, and what it sends the worker: the answer,
|
|
16
|
+
// and while it waits, a message every so often so the worker isn't
|
|
17
|
+
// stopped for being idle (pending work alone doesn't count) and lose
|
|
18
|
+
// the requests waiting on it. Here rather than in `connect-prompt.ts`
|
|
19
|
+
// so the page needn't load the worker's side.
|
|
20
|
+
export const CONNECT_PROMPT_PAGE = 'connect-prompt.html';
|
|
21
|
+
export const CONNECT_ANSWER_MESSAGE = 'sm:connect-answer';
|
|
22
|
+
export const CONNECT_PROMPT_ALIVE_MESSAGE = 'sm:connect-prompt-alive';
|
|
23
|
+
// Each change reads the row and writes it back, so two at once (two
|
|
24
|
+
// prompts answered together) would lose one. Within one context they
|
|
25
|
+
// take turns; the worker and a script's page changing the same script
|
|
26
|
+
// in the same moment is left to chance.
|
|
27
|
+
let queue = Promise.resolve();
|
|
28
|
+
function serialized(fn) {
|
|
29
|
+
const run = queue.then(fn, fn);
|
|
30
|
+
queue = run.catch(() => undefined);
|
|
31
|
+
return run;
|
|
32
|
+
}
|
|
33
|
+
const GRANTS_PREFIX = 'connectGrants:';
|
|
34
|
+
export function connectGrantsKey(scriptId) {
|
|
35
|
+
return GRANTS_PREFIX + scriptId;
|
|
36
|
+
}
|
|
37
|
+
export async function getConnectGrants(scriptId) {
|
|
38
|
+
const key = connectGrantsKey(scriptId);
|
|
39
|
+
const row = (await chrome.storage.local.get(key))[key];
|
|
40
|
+
return {
|
|
41
|
+
hosts: Array.isArray(row?.hosts) ? row.hosts : [],
|
|
42
|
+
...(row?.allHosts ? { allHosts: true } : {}),
|
|
43
|
+
...(Array.isArray(row?.denied) && row.denied.length > 0 ? { denied: row.denied } : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
// The user's standing answer about `host` for this script, if any. A
|
|
47
|
+
// host is never both allowed and denied (each answer clears the other),
|
|
48
|
+
// and "all sites" clears all denials.
|
|
49
|
+
export async function connectAnswer(scriptId, host) {
|
|
50
|
+
const grants = await getConnectGrants(scriptId);
|
|
51
|
+
if (grants.denied?.includes(host))
|
|
52
|
+
return 'deny';
|
|
53
|
+
if (grants.allHosts || grants.hosts.includes(host))
|
|
54
|
+
return 'allow';
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
// Allow `host`, or with `host` null, all hosts. Clears a denial of it
|
|
58
|
+
// (of all of them, for all hosts).
|
|
59
|
+
export async function allowConnectHost(scriptId, host) {
|
|
60
|
+
return change(scriptId, (grants) => {
|
|
61
|
+
if (host === null) {
|
|
62
|
+
grants.allHosts = true;
|
|
63
|
+
delete grants.denied;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!grants.hosts.includes(host))
|
|
67
|
+
grants.hosts.push(host);
|
|
68
|
+
grants.denied = grants.denied?.filter((h) => h !== host);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
// Deny `host`. Clears an allowance of it.
|
|
72
|
+
export async function denyConnectHost(scriptId, host) {
|
|
73
|
+
return change(scriptId, (grants) => {
|
|
74
|
+
grants.hosts = grants.hosts.filter((h) => h !== host);
|
|
75
|
+
if (!grants.denied?.includes(host))
|
|
76
|
+
grants.denied = [...grants.denied ?? [], host];
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
// Take back an allowance: one host, or with `host` null, "all sites".
|
|
80
|
+
export async function revokeConnectHost(scriptId, host) {
|
|
81
|
+
return change(scriptId, (grants) => {
|
|
82
|
+
if (host === null)
|
|
83
|
+
delete grants.allHosts;
|
|
84
|
+
else
|
|
85
|
+
grants.hosts = grants.hosts.filter((h) => h !== host);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// Forget a denial, so the next request asks again.
|
|
89
|
+
export async function forgetDeniedHost(scriptId, host) {
|
|
90
|
+
return change(scriptId, (grants) => {
|
|
91
|
+
grants.denied = grants.denied?.filter((h) => h !== host);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
// Read the row, apply `edit`, and write it back (or remove it once
|
|
95
|
+
// nothing is left in it).
|
|
96
|
+
function change(scriptId, edit) {
|
|
97
|
+
return serialized(async () => {
|
|
98
|
+
const grants = await getConnectGrants(scriptId);
|
|
99
|
+
edit(grants);
|
|
100
|
+
if (grants.denied?.length === 0)
|
|
101
|
+
delete grants.denied;
|
|
102
|
+
const key = connectGrantsKey(scriptId);
|
|
103
|
+
assertWorkerContext('write storage');
|
|
104
|
+
if (grants.hosts.length === 0 && !grants.allHosts && !grants.denied)
|
|
105
|
+
await chrome.storage.local.remove(key);
|
|
106
|
+
else
|
|
107
|
+
await chrome.storage.local.set({ [key]: grants });
|
|
108
|
+
});
|
|
109
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// The connection prompt window (`connect-prompt.html`): asks whether a
|
|
2
|
+
// script may reach a host its `@connect` rules don't cover. The worker
|
|
3
|
+
// opens it with everything it shows in the query string
|
|
4
|
+
// (`connect-prompt.ts`), and the answer goes back by message. Closing
|
|
5
|
+
// the window instead cancels, which the worker sees for itself.
|
|
6
|
+
import { CONNECT_ANSWER_MESSAGE, CONNECT_PROMPT_ALIVE_MESSAGE, } from './connect-grants.js';
|
|
7
|
+
// Under the worker's 30-second idle limit.
|
|
8
|
+
const ALIVE_INTERVAL_MS = 20_000;
|
|
9
|
+
const params = new URLSearchParams(location.search);
|
|
10
|
+
const promptId = params.get('prompt') ?? '';
|
|
11
|
+
const scriptId = params.get('script') ?? '';
|
|
12
|
+
const host = params.get('host') ?? '';
|
|
13
|
+
function byId(id) {
|
|
14
|
+
return document.getElementById(id);
|
|
15
|
+
}
|
|
16
|
+
// What the request was, without its query string or fragment: those
|
|
17
|
+
// often hold keys or session tokens, and the host and path are what the
|
|
18
|
+
// question is about.
|
|
19
|
+
function withoutQuery(url) {
|
|
20
|
+
try {
|
|
21
|
+
const u = new URL(url);
|
|
22
|
+
return `${u.origin}${u.pathname}`;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return url;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const link = byId('script');
|
|
29
|
+
link.textContent = params.get('name') || 'This script';
|
|
30
|
+
link.href = chrome.runtime.getURL(`userscript.html?script=${encodeURIComponent(scriptId)}`);
|
|
31
|
+
// The site, linked at its root. The request itself is in the tooltip.
|
|
32
|
+
const site = byId('site');
|
|
33
|
+
const url = params.get('url') ?? '';
|
|
34
|
+
site.textContent = host;
|
|
35
|
+
try {
|
|
36
|
+
site.href = `${new URL(url).origin}/`;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
site.removeAttribute('href');
|
|
40
|
+
}
|
|
41
|
+
site.title = `${params.get('method') ?? 'GET'} ${withoutQuery(url)}`;
|
|
42
|
+
// Each button's id is the choice it sends.
|
|
43
|
+
const BUTTONS = ['once', 'cancel', 'allow', 'deny', 'allow-all'];
|
|
44
|
+
async function answer(choice) {
|
|
45
|
+
for (const id of BUTTONS)
|
|
46
|
+
byId(id).disabled = true;
|
|
47
|
+
const message = { type: CONNECT_ANSWER_MESSAGE, promptId, scriptId, host, choice };
|
|
48
|
+
let ok = false;
|
|
49
|
+
try {
|
|
50
|
+
ok = await chrome.runtime.sendMessage(message) === true;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Reported below.
|
|
54
|
+
}
|
|
55
|
+
if (ok) {
|
|
56
|
+
// The worker closes the window; this is for when it can't.
|
|
57
|
+
window.close();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const error = byId('error');
|
|
61
|
+
error.textContent = 'SourceMonkey could not save this answer. Close this window and try the request again.';
|
|
62
|
+
error.hidden = false;
|
|
63
|
+
}
|
|
64
|
+
for (const id of BUTTONS)
|
|
65
|
+
byId(id).addEventListener('click', () => void answer(id));
|
|
66
|
+
// No button starts focused: the window takes focus as it opens, and a
|
|
67
|
+
// key pressed for the page underneath mustn't answer it.
|
|
68
|
+
setInterval(() => {
|
|
69
|
+
chrome.runtime.sendMessage({ type: CONNECT_PROMPT_ALIVE_MESSAGE }).catch(() => undefined);
|
|
70
|
+
}, ALIVE_INTERVAL_MS);
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// The connection prompt, worker side: what happens when a script's
|
|
2
|
+
// `GM_xmlhttpRequest` goes to a host its `@connect` rules don't cover
|
|
3
|
+
// and the user hasn't answered for yet (`connect-grants.ts`).
|
|
4
|
+
//
|
|
5
|
+
// - A small popup window (`connect-prompt.html`) asks: allow this host,
|
|
6
|
+
// allow all sites, or deny. A window of the extension's own, rather
|
|
7
|
+
// than something drawn in the page, so the page can neither fake nor
|
|
8
|
+
// click it.
|
|
9
|
+
// - One window per (script, host). Requests that arrive while it is up
|
|
10
|
+
// wait on the same answer, rather than each opening another.
|
|
11
|
+
// - Each request waits `CONNECT_PROMPT_WAIT_MS` at most, then fails.
|
|
12
|
+
// The limit sits under the page's own 5-minute give-up
|
|
13
|
+
// (`XHR_PAGE_SIDE_TTL_MS`), so the request fails with a message that
|
|
14
|
+
// says why rather than as silence.
|
|
15
|
+
// - A prompt with no request left waiting on it (timed out, aborted, or
|
|
16
|
+
// its tab closed) closes. Otherwise it would stay up asking about
|
|
17
|
+
// nothing, and its pings would keep the worker running.
|
|
18
|
+
// - At most `MAX_PROMPTS_PER_SCRIPT` windows are open for one script; a
|
|
19
|
+
// request for another site fails at once. A script looping over many
|
|
20
|
+
// hosts would otherwise bury the user in windows.
|
|
21
|
+
// - Cancel, or closing the window, refuses just the requests waiting
|
|
22
|
+
// on it and keeps nothing, so the next request asks again.
|
|
23
|
+
// - The answer travels with the script id and host, so it is saved even
|
|
24
|
+
// when the worker restarted in between and lost track of the window.
|
|
25
|
+
//
|
|
26
|
+
// sm-dev sees each prompt (`connect-prompt` events) and can answer it
|
|
27
|
+
// (`answerConnectPrompt`), since an agent can't click the window.
|
|
28
|
+
import { allowConnectHost, connectAnswer, CONNECT_PROMPT_PAGE, denyConnectHost } from './connect-grants.js';
|
|
29
|
+
import { appendScriptLog } from './script-log.js';
|
|
30
|
+
import { emitSmDevEvent } from './sm-dev-link.js';
|
|
31
|
+
export const CONNECT_PROMPT_WAIT_MS = 4 * 60 * 1000;
|
|
32
|
+
export const MAX_PROMPTS_PER_SCRIPT = 3;
|
|
33
|
+
const pending = new Map();
|
|
34
|
+
let nextPromptId = 1;
|
|
35
|
+
function pendingKey(scriptId, host) {
|
|
36
|
+
return `${scriptId}\n${host}`;
|
|
37
|
+
}
|
|
38
|
+
// Whether the request may go: the user's standing answer, or the one
|
|
39
|
+
// they give the prompt this opens (or one already open for this host).
|
|
40
|
+
export async function askConnect(info) {
|
|
41
|
+
const standing = await connectAnswer(info.scriptId, info.host);
|
|
42
|
+
if (standing === 'deny')
|
|
43
|
+
logStandingDenial(info);
|
|
44
|
+
if (standing)
|
|
45
|
+
return standing;
|
|
46
|
+
if (info.signal?.aborted)
|
|
47
|
+
return 'aborted';
|
|
48
|
+
const key = pendingKey(info.scriptId, info.host);
|
|
49
|
+
let entry = pending.get(key);
|
|
50
|
+
if (!entry) {
|
|
51
|
+
const open = [...pending.values()].filter((p) => p.scriptId === info.scriptId).length;
|
|
52
|
+
if (open >= MAX_PROMPTS_PER_SCRIPT)
|
|
53
|
+
return 'too-many';
|
|
54
|
+
entry = {
|
|
55
|
+
promptId: `cp${nextPromptId++}-${Math.random().toString(36).slice(2, 8)}`,
|
|
56
|
+
scriptId: info.scriptId,
|
|
57
|
+
host: info.host,
|
|
58
|
+
tabId: info.tabId,
|
|
59
|
+
windowId: null,
|
|
60
|
+
waiters: new Set(),
|
|
61
|
+
};
|
|
62
|
+
pending.set(key, entry);
|
|
63
|
+
const opened = entry;
|
|
64
|
+
void openPromptWindow(opened, info).then((windowId) => {
|
|
65
|
+
// Answered (by sm-dev) before the window was even up.
|
|
66
|
+
if (pending.get(key) !== opened) {
|
|
67
|
+
if (windowId !== null)
|
|
68
|
+
void chrome.windows.remove(windowId).catch(() => undefined);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
opened.windowId = windowId;
|
|
72
|
+
});
|
|
73
|
+
emitSmDevEvent({
|
|
74
|
+
kind: 'connect-prompt', op: 'shown', promptId: entry.promptId, scriptId: info.scriptId,
|
|
75
|
+
tabId: info.tabId, host: info.host, url: info.url, method: info.method,
|
|
76
|
+
});
|
|
77
|
+
void appendScriptLog(info.scriptId, {
|
|
78
|
+
level: 'info',
|
|
79
|
+
source: 'gm-xhr',
|
|
80
|
+
tabId: info.tabId,
|
|
81
|
+
message: `Opened permission request window for connection to ${info.host} (${info.method} ${withoutQuery(info.url)})`,
|
|
82
|
+
// sm-dev prints its own line for the prompt, which also says how
|
|
83
|
+
// to answer it.
|
|
84
|
+
fromReport: true,
|
|
85
|
+
}).catch(() => undefined);
|
|
86
|
+
}
|
|
87
|
+
const waiting = entry;
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
const done = (outcome) => {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
info.signal?.removeEventListener('abort', onAbort);
|
|
92
|
+
waiting.waiters.delete(done);
|
|
93
|
+
resolve(outcome);
|
|
94
|
+
// Given up on rather than answered: the last one out closes it.
|
|
95
|
+
if ((outcome === 'unanswered' || outcome === 'aborted') && waiting.waiters.size === 0)
|
|
96
|
+
dropPrompt(waiting);
|
|
97
|
+
};
|
|
98
|
+
const onAbort = () => done('aborted');
|
|
99
|
+
info.signal?.addEventListener('abort', onAbort);
|
|
100
|
+
const timer = setTimeout(() => done('unanswered'), CONNECT_PROMPT_WAIT_MS);
|
|
101
|
+
waiting.waiters.add(done);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// Script, document and host of each request a standing denial has
|
|
105
|
+
// refused and logged, so a script that retries logs once per site for
|
|
106
|
+
// each page it runs on. Kept for the worker's lifetime, and cleared when
|
|
107
|
+
// it grows past `LOGGED_DENIALS_MAX`, which at worst logs a repeat.
|
|
108
|
+
const loggedDenials = new Set();
|
|
109
|
+
const LOGGED_DENIALS_MAX = 1000;
|
|
110
|
+
function logStandingDenial(info) {
|
|
111
|
+
const key = `${info.scriptId}\n${info.documentId ?? `tab ${info.tabId}`}\n${info.host}`;
|
|
112
|
+
if (loggedDenials.has(key))
|
|
113
|
+
return;
|
|
114
|
+
if (loggedDenials.size >= LOGGED_DENIALS_MAX)
|
|
115
|
+
loggedDenials.clear();
|
|
116
|
+
loggedDenials.add(key);
|
|
117
|
+
void appendScriptLog(info.scriptId, {
|
|
118
|
+
level: 'warning',
|
|
119
|
+
source: 'gm-xhr',
|
|
120
|
+
tabId: info.tabId,
|
|
121
|
+
message: `Blocked a request to ${info.host}, which was denied for this script. `
|
|
122
|
+
+ "Manage permissions on the script's page. Later requests to it from the same page are blocked without a log entry.",
|
|
123
|
+
}).catch(() => undefined);
|
|
124
|
+
}
|
|
125
|
+
// A URL without its query string or fragment, for the log: those often
|
|
126
|
+
// hold keys or session tokens.
|
|
127
|
+
function withoutQuery(url) {
|
|
128
|
+
try {
|
|
129
|
+
const u = new URL(url);
|
|
130
|
+
return `${u.origin}${u.pathname}`;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return url;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Close a prompt nobody is waiting on any more, without an answer.
|
|
137
|
+
function dropPrompt(entry) {
|
|
138
|
+
const key = pendingKey(entry.scriptId, entry.host);
|
|
139
|
+
if (pending.get(key) !== entry)
|
|
140
|
+
return;
|
|
141
|
+
pending.delete(key);
|
|
142
|
+
if (entry.windowId !== null)
|
|
143
|
+
void chrome.windows.remove(entry.windowId).catch(() => undefined);
|
|
144
|
+
emitSmDevEvent({
|
|
145
|
+
kind: 'connect-prompt', op: 'closed', promptId: entry.promptId, scriptId: entry.scriptId,
|
|
146
|
+
tabId: entry.tabId, host: entry.host,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// For tests: close all prompts, and forget which denials were logged.
|
|
150
|
+
export function resetConnectPromptsForTests() {
|
|
151
|
+
for (const entry of [...pending.values()])
|
|
152
|
+
dropPrompt(entry);
|
|
153
|
+
loggedDenials.clear();
|
|
154
|
+
}
|
|
155
|
+
async function openPromptWindow(entry, info) {
|
|
156
|
+
const params = new URLSearchParams({
|
|
157
|
+
prompt: entry.promptId,
|
|
158
|
+
script: info.scriptId,
|
|
159
|
+
name: info.scriptName,
|
|
160
|
+
host: info.host,
|
|
161
|
+
url: info.url,
|
|
162
|
+
method: info.method,
|
|
163
|
+
});
|
|
164
|
+
const width = 520;
|
|
165
|
+
const height = 350;
|
|
166
|
+
// Over the window the request came from, when there is one to place
|
|
167
|
+
// it by.
|
|
168
|
+
let place = {};
|
|
169
|
+
try {
|
|
170
|
+
const tab = await chrome.tabs.get(info.tabId);
|
|
171
|
+
const win = await chrome.windows.get(tab.windowId);
|
|
172
|
+
if (win.left !== undefined && win.top !== undefined && win.width !== undefined) {
|
|
173
|
+
// Not clamped at 0: a screen left of the primary one has negative
|
|
174
|
+
// coordinates.
|
|
175
|
+
place = { left: win.left + Math.round((win.width - width) / 2), top: win.top + 80 };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
// Placed where Chrome likes.
|
|
180
|
+
}
|
|
181
|
+
try {
|
|
182
|
+
const win = await chrome.windows.create({
|
|
183
|
+
url: chrome.runtime.getURL(`${CONNECT_PROMPT_PAGE}?${params}`),
|
|
184
|
+
type: 'popup',
|
|
185
|
+
width,
|
|
186
|
+
height,
|
|
187
|
+
focused: true,
|
|
188
|
+
...place,
|
|
189
|
+
});
|
|
190
|
+
return win?.id ?? null;
|
|
191
|
+
}
|
|
192
|
+
catch (err) {
|
|
193
|
+
// No window means no answer can come: fail the requests waiting,
|
|
194
|
+
// and leave nothing behind for the next one to join.
|
|
195
|
+
dropPrompt(entry);
|
|
196
|
+
const failed = err instanceof Error ? err.message : String(err);
|
|
197
|
+
for (const waiter of [...entry.waiters])
|
|
198
|
+
waiter({ failed });
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Record the user's answer and let the waiting requests go (or fail).
|
|
203
|
+
// `promptId` names the prompt answered; the answer is saved by script
|
|
204
|
+
// and host whether or not the worker still knows that prompt.
|
|
205
|
+
export async function answerConnect(scriptId, host, choice, how) {
|
|
206
|
+
if (choice === 'deny')
|
|
207
|
+
await denyConnectHost(scriptId, host);
|
|
208
|
+
else if (choice === 'allow' || choice === 'allow-all')
|
|
209
|
+
await allowConnectHost(scriptId, choice === 'allow-all' ? null : host);
|
|
210
|
+
const key = pendingKey(scriptId, host);
|
|
211
|
+
const entry = pending.get(key);
|
|
212
|
+
const others = choice === 'allow-all'
|
|
213
|
+
? [...pending.values()].filter((p) => p.scriptId === scriptId && p !== entry)
|
|
214
|
+
: [];
|
|
215
|
+
for (const settled of entry ? [entry, ...others] : others) {
|
|
216
|
+
pending.delete(pendingKey(settled.scriptId, settled.host));
|
|
217
|
+
const outcome = choice === 'deny' ? 'deny' : choice === 'cancel' ? 'cancelled' : 'allow';
|
|
218
|
+
// "All sites" settles this script's other prompts too; "once" only this one.
|
|
219
|
+
for (const waiter of [...settled.waiters])
|
|
220
|
+
waiter(outcome);
|
|
221
|
+
if (how !== 'closed' && settled.windowId !== null) {
|
|
222
|
+
void chrome.windows.remove(settled.windowId).catch(() => undefined);
|
|
223
|
+
}
|
|
224
|
+
emitSmDevEvent({
|
|
225
|
+
kind: 'connect-prompt', op: 'answered', promptId: settled.promptId, scriptId,
|
|
226
|
+
tabId: settled.tabId, host: settled.host, answer: choice,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const where = how === 'sm-dev' ? ' from sm-dev' : '';
|
|
230
|
+
const message = choice === 'allow-all' ? `Allowed this script to connect to all sites${where}`
|
|
231
|
+
: choice === 'allow' ? `Allowed this script to connect to ${host}${where}`
|
|
232
|
+
: choice === 'once' ? `Allowed this script to connect to ${host} once${where}. The requests waiting on the permission prompt were sent, and the next one asks again.`
|
|
233
|
+
: choice === 'deny' ? `Denied this script connecting to ${host}${where}. Manage permissions on the script's page.`
|
|
234
|
+
: how === 'closed' ? `The permission prompt about connecting to ${host} was closed, which cancels it. The requests waiting on it were blocked, and the next one asks again.`
|
|
235
|
+
: `Cancelled the permission prompt about connecting to ${host}${where}. The requests waiting on it were blocked, and the next one asks again.`;
|
|
236
|
+
await appendScriptLog(scriptId, {
|
|
237
|
+
level: choice === 'deny' || choice === 'cancel' ? 'warning' : 'info',
|
|
238
|
+
source: 'gm-xhr',
|
|
239
|
+
...(entry ? { tabId: entry.tabId } : {}),
|
|
240
|
+
message,
|
|
241
|
+
}).catch(() => undefined);
|
|
242
|
+
}
|
|
243
|
+
// A prompt window closed. Without an answer first, that cancels it.
|
|
244
|
+
export function onConnectPromptWindowRemoved(windowId) {
|
|
245
|
+
for (const entry of pending.values()) {
|
|
246
|
+
if (entry.windowId === windowId) {
|
|
247
|
+
void answerConnect(entry.scriptId, entry.host, 'cancel', 'closed');
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// sm-dev's answer to a prompt it was told about. False when no such
|
|
253
|
+
// prompt is waiting.
|
|
254
|
+
export async function answerConnectPrompt(promptId, choice) {
|
|
255
|
+
for (const entry of pending.values()) {
|
|
256
|
+
if (entry.promptId === promptId) {
|
|
257
|
+
await answerConnect(entry.scriptId, entry.host, choice, 'sm-dev');
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
// The prompts waiting, for sm-dev to list.
|
|
264
|
+
export function pendingConnectPrompts() {
|
|
265
|
+
return [...pending.values()].map(({ promptId, scriptId, tabId, host }) => ({ promptId, scriptId, tabId, host }));
|
|
266
|
+
}
|