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,436 @@
|
|
|
1
|
+
// Resolution + loading of resources referenced from a userscript
|
|
2
|
+
// metadata block — e.g. `@icon` today, `@require` planned. Centralizes
|
|
3
|
+
// the scheme-dispatch + relative-path-against-the-script rule so
|
|
4
|
+
// each consumer doesn't reinvent it.
|
|
5
|
+
//
|
|
6
|
+
// Reference forms accepted by `resolveScriptResource`:
|
|
7
|
+
// - absolute http(s):// URL → kind 'http' (passed through verbatim;
|
|
8
|
+
// any origin is allowed — the script
|
|
9
|
+
// author explicitly pointed there)
|
|
10
|
+
// - `data:` URL → kind 'data' (caller handles inline;
|
|
11
|
+
// not fetched here)
|
|
12
|
+
// - relative path → resolved against `scriptFileUrl`.
|
|
13
|
+
// Against a *local* (file://) script the
|
|
14
|
+
// result must still live at or under the
|
|
15
|
+
// script's own directory (containment
|
|
16
|
+
// check on the resolved URL). Against an
|
|
17
|
+
// http(s) script it may climb — see
|
|
18
|
+
// "Containment is about local files"
|
|
19
|
+
// below.
|
|
20
|
+
//
|
|
21
|
+
// Notably absent: absolute `file://` URLs. The extension holds
|
|
22
|
+
// `file:///*` host permission so any caller code can read arbitrary
|
|
23
|
+
// local files via the byte fetcher; we don't want a malicious
|
|
24
|
+
// HTTP-sourced userscript pointing `@icon file:///etc/passwd` and
|
|
25
|
+
// having us inline a host file into the collections-page DOM. A file://
|
|
26
|
+
// result is only reachable as the *resolution* of a relative path
|
|
27
|
+
// against a file:// script, in which case the containment check
|
|
28
|
+
// confines it to the script's own directory tree.
|
|
29
|
+
//
|
|
30
|
+
// Containment is about local files, so it applies only to a local
|
|
31
|
+
// base. A `../lib/util.js` next to a published script is the ordinary
|
|
32
|
+
// layout of a userscript repo served over HTTP, and Violentmonkey
|
|
33
|
+
// resolves it (`vetUrl` in `src/background/utils/url.js`, applied by
|
|
34
|
+
// `buildPathMap`) with no containment at all. Refusing it bought
|
|
35
|
+
// nothing: an http(s)-loaded script may already name an absolute URL on
|
|
36
|
+
// any origin, so climbing out of its own directory grants it no reach
|
|
37
|
+
// it didn't have. Against a `file://` script the same move would reach
|
|
38
|
+
// out of the script's tree into the rest of the disk, which is exactly
|
|
39
|
+
// what the rule exists to stop — there, the sanctioned way to reach a
|
|
40
|
+
// sibling `lib/` is a search root the user named (see
|
|
41
|
+
// `local-file-overrides.ts`).
|
|
42
|
+
//
|
|
43
|
+
// Containment check vs. textual `..` filter: a textual filter on the
|
|
44
|
+
// raw reference can be bypassed by percent-encoding (`%2e%2e` is
|
|
45
|
+
// normalized into a `..` directory-up move by the URL parser before
|
|
46
|
+
// we ever see the resolved form). Comparing the resolved URL string
|
|
47
|
+
// against the parent-directory URL string catches every encoding —
|
|
48
|
+
// literal `..`, `%2e%2e`, leading `/`, etc. — in one rule.
|
|
49
|
+
//
|
|
50
|
+
// The host script's parent dir is derived from `scriptFileUrl`, which
|
|
51
|
+
// is the canonical fetchable URL we already store on every script
|
|
52
|
+
// record — works uniformly for file://, http(s)://, and entries
|
|
53
|
+
// reached via a manifest.
|
|
54
|
+
import { fsMtimeFromLastModified, isFileUrl, isHttpUrl } from './file-locations.js';
|
|
55
|
+
import { githubAuthHeaders } from './github-auth.js';
|
|
56
|
+
import { mustHopToWorker, readHttpViaWorker, replyBytes } from './http-read.js';
|
|
57
|
+
const ABSOLUTE_URL_RE = /^[a-z][a-z0-9+.-]*:/i;
|
|
58
|
+
// Yes/no form, for the callers that only need a URL or nothing —
|
|
59
|
+
// syntax highlighting's link detection, the icon resolver, the corpus
|
|
60
|
+
// validator. A thin wrapper rather than a second implementation: the
|
|
61
|
+
// two used to be one function and the rules must not drift.
|
|
62
|
+
export function resolveScriptResource(ref, scriptFileUrl) {
|
|
63
|
+
const result = resolveScriptResourceDetailed(ref, scriptFileUrl);
|
|
64
|
+
return 'failed' in result ? null : result;
|
|
65
|
+
}
|
|
66
|
+
// Same rules, but says which one rejected the ref. Used where the
|
|
67
|
+
// answer becomes a message a user reads.
|
|
68
|
+
export function resolveScriptResourceDetailed(ref, scriptFileUrl) {
|
|
69
|
+
const trimmed = ref.trim();
|
|
70
|
+
if (!trimmed)
|
|
71
|
+
return { failed: true, reason: 'empty' };
|
|
72
|
+
if (trimmed.startsWith('data:')) {
|
|
73
|
+
return { kind: 'data', url: trimmed };
|
|
74
|
+
}
|
|
75
|
+
// Absolute http(s):// URL: the script author wrote a complete URL,
|
|
76
|
+
// any origin is allowed. Note that absolute `file://` is *not*
|
|
77
|
+
// accepted here — see module header for why.
|
|
78
|
+
if (ABSOLUTE_URL_RE.test(trimmed)) {
|
|
79
|
+
let parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = new URL(trimmed);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return { failed: true, reason: 'malformed-url' };
|
|
85
|
+
}
|
|
86
|
+
if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
|
|
87
|
+
return { kind: 'http', url: parsed.toString() };
|
|
88
|
+
}
|
|
89
|
+
return { failed: true, reason: 'unsupported-scheme', scheme: parsed.protocol };
|
|
90
|
+
}
|
|
91
|
+
// Relative path. Resolve against the script's own URL. Against a
|
|
92
|
+
// local script the result must still be confined to the script's own
|
|
93
|
+
// directory tree; against a remote one it need not (see the module
|
|
94
|
+
// header). We compare URL strings rather than path strings so the
|
|
95
|
+
// check accounts for origin, scheme, host, and port atomically — the
|
|
96
|
+
// resolved URL must share the same prefix as `<parentBase>/`.
|
|
97
|
+
let resolved;
|
|
98
|
+
let parentBase;
|
|
99
|
+
try {
|
|
100
|
+
resolved = new URL(trimmed, scriptFileUrl);
|
|
101
|
+
parentBase = new URL('./', scriptFileUrl);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return { failed: true, reason: 'malformed-url' };
|
|
105
|
+
}
|
|
106
|
+
// `parentBase` carries the base's scheme, so it answers "is the host
|
|
107
|
+
// script remote?" without a third parse of the same string.
|
|
108
|
+
const remoteBase = parentBase.protocol === 'http:' || parentBase.protocol === 'https:';
|
|
109
|
+
const parentStr = parentBase.toString();
|
|
110
|
+
const resolvedStr = resolved.toString();
|
|
111
|
+
if (!remoteBase && !resolvedStr.startsWith(parentStr)) {
|
|
112
|
+
return { failed: true, reason: 'outside-script-folder' };
|
|
113
|
+
}
|
|
114
|
+
if (resolved.protocol === 'http:' || resolved.protocol === 'https:') {
|
|
115
|
+
return { kind: 'http', url: resolvedStr };
|
|
116
|
+
}
|
|
117
|
+
if (resolved.protocol === 'file:') {
|
|
118
|
+
return { kind: 'file', url: resolvedStr };
|
|
119
|
+
}
|
|
120
|
+
// A scratch script (`scratch:///name.user.js`, see directories.ts) is
|
|
121
|
+
// stored as text alone, so it has no folder a relative path could name.
|
|
122
|
+
if (resolved.protocol === 'scratch:')
|
|
123
|
+
return { failed: true, reason: 'relative-in-scratch' };
|
|
124
|
+
// A relative ref that lands on some other scheme — only reachable
|
|
125
|
+
// when the script's own URL has one, which the collection pipeline
|
|
126
|
+
// doesn't produce today.
|
|
127
|
+
return { failed: true, reason: 'unsupported-scheme', scheme: resolved.protocol };
|
|
128
|
+
}
|
|
129
|
+
// ---------- Bounded byte fetch ----------
|
|
130
|
+
//
|
|
131
|
+
// Scheme-aware bounded fetcher used by resource loaders. http(s) goes
|
|
132
|
+
// through `fetch()`; file:// goes through XMLHttpRequest in page
|
|
133
|
+
// contexts (where `fetch()` would return an opaque chrome-extension →
|
|
134
|
+
// file:// response) and falls back to `fetch()` in service-worker
|
|
135
|
+
// contexts (no XHR). Both legs apply the same size cap and timeout.
|
|
136
|
+
const DEFAULT_FETCH_TIMEOUT_MS = 5000;
|
|
137
|
+
export function isNotModified(v) {
|
|
138
|
+
return v.notModified === true;
|
|
139
|
+
}
|
|
140
|
+
const fetchFailed = (reason) => ({ failed: true, reason });
|
|
141
|
+
const httpError = (status) => ({ failed: true, reason: 'http-error', status });
|
|
142
|
+
export async function fetchResourceBytes(url, opts) {
|
|
143
|
+
if (isHttpUrl(url))
|
|
144
|
+
return fetchHttpBytes(url, opts);
|
|
145
|
+
if (isFileUrl(url))
|
|
146
|
+
return fetchFileBytes(url, opts);
|
|
147
|
+
return fetchFailed('unsupported-scheme');
|
|
148
|
+
}
|
|
149
|
+
// Page contexts hand the read to the service worker — a document
|
|
150
|
+
// fetch acts on the response's `Link: rel=preload` headers, which our
|
|
151
|
+
// pages can't have. See src/http-read.ts for the whole story.
|
|
152
|
+
async function fetchHttpBytes(url, opts) {
|
|
153
|
+
if (mustHopToWorker()) {
|
|
154
|
+
const viaSw = await fetchHttpBytesViaWorker(url, opts);
|
|
155
|
+
if (viaSw)
|
|
156
|
+
return viaSw;
|
|
157
|
+
}
|
|
158
|
+
return fetchHttpBytesHere(url, opts);
|
|
159
|
+
}
|
|
160
|
+
// Map the worker's report of what came back onto this module's own
|
|
161
|
+
// verdict about it — which statuses are failures, which content types
|
|
162
|
+
// are acceptable, what counts as too big. Returns null when the worker
|
|
163
|
+
// never answered, so the caller falls back to fetching here.
|
|
164
|
+
async function fetchHttpBytesViaWorker(url, opts) {
|
|
165
|
+
const reply = await readHttpViaWorker({
|
|
166
|
+
url,
|
|
167
|
+
as: 'bytes',
|
|
168
|
+
// The conditional headers have to cross the hop too. Page contexts
|
|
169
|
+
// are where the refresh pipeline actually runs — `collections.ts`
|
|
170
|
+
// imports it directly — so dropping them here would leave every
|
|
171
|
+
// user-facing refresh unconditional while the worker-side path
|
|
172
|
+
// (and every test driving it) looked correct.
|
|
173
|
+
headers: conditionalHeaders(opts.conditional),
|
|
174
|
+
maxBytes: opts.maxBytes,
|
|
175
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS,
|
|
176
|
+
});
|
|
177
|
+
if (!reply)
|
|
178
|
+
return null;
|
|
179
|
+
if (reply.kind === 'error')
|
|
180
|
+
return fetchFailed(reply.reason);
|
|
181
|
+
// Before the success-range check, for the same reason the local path
|
|
182
|
+
// tests it before `response.ok`: 304 sits outside 200-299 and would
|
|
183
|
+
// otherwise be reported as an HTTP error.
|
|
184
|
+
if (reply.status === 304)
|
|
185
|
+
return { notModified: true };
|
|
186
|
+
if (reply.status < 200 || reply.status > 299)
|
|
187
|
+
return httpError(reply.status);
|
|
188
|
+
const contentType = reply.contentType.toLowerCase();
|
|
189
|
+
if (opts.acceptContentType && contentType && !opts.acceptContentType(contentType)) {
|
|
190
|
+
return fetchFailed('wrong-content-type');
|
|
191
|
+
}
|
|
192
|
+
const body = replyBytes(reply);
|
|
193
|
+
// Re-checked here rather than trusted: these caps are what each
|
|
194
|
+
// caller's own limit is expressed in, and `body` is what they get.
|
|
195
|
+
if (body.byteLength === 0)
|
|
196
|
+
return fetchFailed('empty');
|
|
197
|
+
if (body.byteLength > opts.maxBytes)
|
|
198
|
+
return fetchFailed('oversize');
|
|
199
|
+
const out = { body, contentType, finalUrl: reply.finalUrl };
|
|
200
|
+
if (reply.etag)
|
|
201
|
+
out.etag = reply.etag;
|
|
202
|
+
if (reply.lastModified)
|
|
203
|
+
out.lastModified = reply.lastModified;
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
// The fetch itself. Runs in the service worker for page-initiated
|
|
207
|
+
// reads (see above) and directly in whatever context asked otherwise.
|
|
208
|
+
async function fetchHttpBytesHere(url, opts) {
|
|
209
|
+
const controller = new AbortController();
|
|
210
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS);
|
|
211
|
+
let response;
|
|
212
|
+
try {
|
|
213
|
+
response = await fetch(url, {
|
|
214
|
+
cache: 'no-store',
|
|
215
|
+
redirect: 'follow',
|
|
216
|
+
referrerPolicy: 'no-referrer',
|
|
217
|
+
signal: controller.signal,
|
|
218
|
+
// Attach GitHub auth header when the URL targets a GitHub host so
|
|
219
|
+
// `@require` / `@resource` / icon fetches against private raw
|
|
220
|
+
// URLs work. Host-gated inside `githubAuthHeaders` so non-GitHub
|
|
221
|
+
// hosts never see the token.
|
|
222
|
+
headers: { ...githubAuthHeaders(url), ...conditionalHeaders(opts.conditional) },
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
catch {
|
|
226
|
+
// Our own timer is the only thing that aborts this request, so an
|
|
227
|
+
// aborted signal identifies the timeout exactly rather than
|
|
228
|
+
// leaving it merged with a network failure.
|
|
229
|
+
return controller.signal.aborted ? fetchFailed('timeout') : fetchFailed('no-response');
|
|
230
|
+
}
|
|
231
|
+
finally {
|
|
232
|
+
clearTimeout(timer);
|
|
233
|
+
}
|
|
234
|
+
// Before the `ok` check: 304 is not in the 200-299 range, so testing
|
|
235
|
+
// `response.ok` first would report the server's confirmation that
|
|
236
|
+
// nothing changed as an HTTP error.
|
|
237
|
+
if (response.status === 304)
|
|
238
|
+
return { notModified: true };
|
|
239
|
+
if (!response.ok)
|
|
240
|
+
return httpError(response.status);
|
|
241
|
+
const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
|
|
242
|
+
if (opts.acceptContentType && contentType && !opts.acceptContentType(contentType)) {
|
|
243
|
+
return fetchFailed('wrong-content-type');
|
|
244
|
+
}
|
|
245
|
+
const cl = response.headers.get('content-length');
|
|
246
|
+
if (cl && Number(cl) > opts.maxBytes)
|
|
247
|
+
return fetchFailed('oversize');
|
|
248
|
+
let body;
|
|
249
|
+
try {
|
|
250
|
+
body = await response.arrayBuffer();
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return fetchFailed('no-response');
|
|
254
|
+
}
|
|
255
|
+
if (body.byteLength === 0)
|
|
256
|
+
return fetchFailed('empty');
|
|
257
|
+
if (body.byteLength > opts.maxBytes)
|
|
258
|
+
return fetchFailed('oversize');
|
|
259
|
+
const out = { body, contentType, finalUrl: response.url || url };
|
|
260
|
+
const etag = response.headers.get('etag');
|
|
261
|
+
const lastModified = response.headers.get('last-modified');
|
|
262
|
+
if (etag)
|
|
263
|
+
out.etag = etag;
|
|
264
|
+
if (lastModified)
|
|
265
|
+
out.lastModified = lastModified;
|
|
266
|
+
return out;
|
|
267
|
+
}
|
|
268
|
+
// `If-None-Match` wins where a server offers both: an ETag is exact,
|
|
269
|
+
// where `Last-Modified` has one-second resolution and cannot represent
|
|
270
|
+
// two edits inside the same second. Sending both would let the server
|
|
271
|
+
// pick the weaker one.
|
|
272
|
+
function conditionalHeaders(conditional) {
|
|
273
|
+
if (!conditional)
|
|
274
|
+
return {};
|
|
275
|
+
if (conditional.etag)
|
|
276
|
+
return { 'If-None-Match': conditional.etag };
|
|
277
|
+
if (conditional.lastModified)
|
|
278
|
+
return { 'If-Modified-Since': conditional.lastModified };
|
|
279
|
+
return {};
|
|
280
|
+
}
|
|
281
|
+
async function fetchFileBytes(url, opts) {
|
|
282
|
+
// Page contexts: XHR avoids the chrome-extension → file:// CORS
|
|
283
|
+
// opaque-response trap that bites `fetch()`. SW contexts: no XHR,
|
|
284
|
+
// but `fetch()` is permitted for file:// when host_permissions
|
|
285
|
+
// grants `file:///*`.
|
|
286
|
+
if (typeof XMLHttpRequest !== 'undefined') {
|
|
287
|
+
return fetchFileBytesXhr(url, opts);
|
|
288
|
+
}
|
|
289
|
+
return fetchFileBytesFetch(url, opts);
|
|
290
|
+
}
|
|
291
|
+
function fetchFileBytesXhr(url, opts) {
|
|
292
|
+
return new Promise((resolve) => {
|
|
293
|
+
const xhr = new XMLHttpRequest();
|
|
294
|
+
let settled = false;
|
|
295
|
+
const finish = (value) => {
|
|
296
|
+
if (settled)
|
|
297
|
+
return;
|
|
298
|
+
settled = true;
|
|
299
|
+
resolve(value);
|
|
300
|
+
};
|
|
301
|
+
// The one branch here that knows its cause with certainty: we are
|
|
302
|
+
// inside our own timer.
|
|
303
|
+
const timer = setTimeout(() => {
|
|
304
|
+
try {
|
|
305
|
+
xhr.abort();
|
|
306
|
+
}
|
|
307
|
+
catch { /* ignore */ }
|
|
308
|
+
finish(fetchFailed('timeout'));
|
|
309
|
+
}, opts.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS);
|
|
310
|
+
xhr.open('GET', url);
|
|
311
|
+
xhr.responseType = 'arraybuffer';
|
|
312
|
+
xhr.onload = () => {
|
|
313
|
+
clearTimeout(timer);
|
|
314
|
+
const body = xhr.response;
|
|
315
|
+
// Defensive: `onload` with no body shouldn't happen — a failed
|
|
316
|
+
// load fires `onerror`, and a 0-byte file yields an empty buffer,
|
|
317
|
+
// not null. Guarded so that if it ever does, it isn't mistaken
|
|
318
|
+
// for an empty file.
|
|
319
|
+
if (!body) {
|
|
320
|
+
finish(fetchFailed('unreadable-file'));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (body.byteLength === 0) {
|
|
324
|
+
finish(fetchFailed('empty'));
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
if (body.byteLength > opts.maxBytes) {
|
|
328
|
+
finish(fetchFailed('oversize'));
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
// file:// XHRs don't carry an HTTP layer, so Content-Type is
|
|
332
|
+
// whatever Chrome inferred from the extension (often '' or
|
|
333
|
+
// text/plain for images). Run the predicate only when we have
|
|
334
|
+
// something usable; otherwise let the caller infer from the
|
|
335
|
+
// filename / bytes.
|
|
336
|
+
const ct = (xhr.getResponseHeader('content-type') ?? '').toLowerCase();
|
|
337
|
+
if (opts.acceptContentType && ct && !opts.acceptContentType(ct)) {
|
|
338
|
+
finish(fetchFailed('wrong-content-type'));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
finish({
|
|
342
|
+
body, contentType: ct, finalUrl: url,
|
|
343
|
+
...fsMtimeField(xhr.getResponseHeader('last-modified')),
|
|
344
|
+
});
|
|
345
|
+
};
|
|
346
|
+
// A file:// XHR carries no HTTP layer, so a file that isn't there
|
|
347
|
+
// and one we're not allowed to read arrive identically — hence one
|
|
348
|
+
// reason covering both, rather than a guess between them.
|
|
349
|
+
xhr.onerror = () => {
|
|
350
|
+
clearTimeout(timer);
|
|
351
|
+
finish(fetchFailed('unreadable-file'));
|
|
352
|
+
};
|
|
353
|
+
try {
|
|
354
|
+
xhr.send();
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
clearTimeout(timer);
|
|
358
|
+
finish(fetchFailed('unreadable-file'));
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
async function fetchFileBytesFetch(url, opts) {
|
|
363
|
+
const controller = new AbortController();
|
|
364
|
+
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS);
|
|
365
|
+
let response;
|
|
366
|
+
try {
|
|
367
|
+
response = await fetch(url, { signal: controller.signal });
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
return controller.signal.aborted ? fetchFailed('timeout') : fetchFailed('unreadable-file');
|
|
371
|
+
}
|
|
372
|
+
finally {
|
|
373
|
+
clearTimeout(timer);
|
|
374
|
+
}
|
|
375
|
+
// `status === 0` is normal for a successful file:// read. A non-ok
|
|
376
|
+
// status is NOT reported as an HTTP error: a `file:` URL has no HTTP
|
|
377
|
+
// layer, and whatever status this leg's `fetch` synthesises means the
|
|
378
|
+
// same thing the XHR leg's `onerror` does — the file wouldn't read.
|
|
379
|
+
// Reporting it as `HTTP <n>` would make the two legs describe one
|
|
380
|
+
// situation differently, which is what the corpus validator's fetch
|
|
381
|
+
// shim exposed.
|
|
382
|
+
if (!response.ok && response.status !== 0)
|
|
383
|
+
return fetchFailed('unreadable-file');
|
|
384
|
+
// Answered, but we aren't allowed to look.
|
|
385
|
+
if (response.type === 'opaque')
|
|
386
|
+
return fetchFailed('unreadable-file');
|
|
387
|
+
const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
|
|
388
|
+
if (opts.acceptContentType && contentType && !opts.acceptContentType(contentType)) {
|
|
389
|
+
return fetchFailed('wrong-content-type');
|
|
390
|
+
}
|
|
391
|
+
let body;
|
|
392
|
+
try {
|
|
393
|
+
body = await response.arrayBuffer();
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
// Same leg, same file — matches the `unreadable-file` above rather
|
|
397
|
+
// than blaming the network for a local read.
|
|
398
|
+
return fetchFailed('unreadable-file');
|
|
399
|
+
}
|
|
400
|
+
if (body.byteLength === 0)
|
|
401
|
+
return fetchFailed('empty');
|
|
402
|
+
if (body.byteLength > opts.maxBytes)
|
|
403
|
+
return fetchFailed('oversize');
|
|
404
|
+
return {
|
|
405
|
+
body, contentType, finalUrl: response.url || url,
|
|
406
|
+
...fsMtimeField(response.headers.get('last-modified')),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function fsMtimeField(header) {
|
|
410
|
+
const fsMtime = fsMtimeFromLastModified(header);
|
|
411
|
+
return fsMtime !== undefined ? { fsMtime } : {};
|
|
412
|
+
}
|
|
413
|
+
// ---------- Text + suffix helpers ----------
|
|
414
|
+
// Convenience for text-shaped consumers. Wraps fetchResourceBytes and
|
|
415
|
+
// decodes UTF-8. Returns null on any failure the bytes-fetcher
|
|
416
|
+
// reports, plus null on a decode failure (rare — `fatal: false` rules
|
|
417
|
+
// out throws).
|
|
418
|
+
//
|
|
419
|
+
// Still yes/no: its one caller is the version probe, whose row renders
|
|
420
|
+
// in a definition-list cell with no space for a reason. Switch it to
|
|
421
|
+
// the failure shape if that row ever gets room — the same condition
|
|
422
|
+
// TODO.md attaches to the probe's own wording.
|
|
423
|
+
export async function fetchResourceText(url, opts) {
|
|
424
|
+
const res = await fetchResourceBytes(url, opts);
|
|
425
|
+
// A caller wanting text has nothing to do with a 304 — it holds no
|
|
426
|
+
// prior copy to pair the answer with. Treated as "no text", the same
|
|
427
|
+
// as a failure.
|
|
428
|
+
if ('failed' in res || isNotModified(res))
|
|
429
|
+
return null;
|
|
430
|
+
try {
|
|
431
|
+
return new TextDecoder('utf-8', { fatal: false }).decode(res.body);
|
|
432
|
+
}
|
|
433
|
+
catch {
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
}
|