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.
Files changed (367) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +87 -2
  3. package/dist/bytes.js +37 -0
  4. package/dist/channel-placeholder.js +5 -0
  5. package/dist/collection-scan.js +17 -0
  6. package/dist/collections.html +353 -0
  7. package/dist/compare.html +358 -0
  8. package/dist/composed-source.js +205 -0
  9. package/dist/connect-prompt.html +47 -0
  10. package/dist/debug/icons.html +211 -0
  11. package/dist/debug/storage.html +163 -0
  12. package/dist/debug-log.js +75 -0
  13. package/dist/diagnostic-location.js +64 -0
  14. package/dist/eval-hook.js +308 -0
  15. package/dist/extension/background.js +5263 -0
  16. package/dist/extension/channel.js +133 -0
  17. package/dist/extension/code-links.js +187 -0
  18. package/dist/extension/code-tabs.js +460 -0
  19. package/dist/extension/collection-dialog.js +619 -0
  20. package/dist/extension/collection-display.js +322 -0
  21. package/dist/extension/collections.js +2905 -0
  22. package/dist/extension/compare.js +867 -0
  23. package/dist/extension/concurrency.js +186 -0
  24. package/dist/extension/connect-grants.js +109 -0
  25. package/dist/extension/connect-prompt-page.js +70 -0
  26. package/dist/extension/connect-prompt.js +266 -0
  27. package/dist/extension/copy-button.js +63 -0
  28. package/dist/extension/copy-dialog.js +502 -0
  29. package/dist/extension/copy-preference.js +66 -0
  30. package/dist/extension/debug/icons.js +203 -0
  31. package/dist/extension/debug/storage.js +365 -0
  32. package/dist/extension/deep-link.js +77 -0
  33. package/dist/extension/delete-dialog.js +432 -0
  34. package/dist/extension/delete-icon.js +40 -0
  35. package/dist/extension/details-tooltips.js +66 -0
  36. package/dist/extension/directories.js +679 -0
  37. package/dist/extension/export-backup.js +407 -0
  38. package/dist/extension/export-dialog.js +142 -0
  39. package/dist/extension/file-access.js +74 -0
  40. package/dist/extension/file-changes.js +346 -0
  41. package/dist/extension/frame-info.js +85 -0
  42. package/dist/extension/github-provider.js +336 -0
  43. package/dist/extension/glyph.js +84 -0
  44. package/dist/extension/gm-bindings.js +601 -0
  45. package/dist/extension/gm-download.js +419 -0
  46. package/dist/extension/gm-menu.js +228 -0
  47. package/dist/extension/gm-notification.js +141 -0
  48. package/dist/extension/gm-originator-map.js +23 -0
  49. package/dist/extension/gm-tab-data.js +121 -0
  50. package/dist/extension/gm-tab.js +160 -0
  51. package/dist/extension/gm-url-change.js +111 -0
  52. package/dist/extension/gm-url-scheme.js +27 -0
  53. package/dist/extension/gm-values.js +176 -0
  54. package/dist/extension/gm-window-control.js +104 -0
  55. package/dist/extension/gm-xhr-headers.js +333 -0
  56. package/dist/extension/gm-xhr.js +584 -0
  57. package/dist/extension/injector.js +3145 -0
  58. package/dist/extension/install-capture.js +277 -0
  59. package/dist/extension/line-diff.js +233 -0
  60. package/dist/extension/local-copy.js +560 -0
  61. package/dist/extension/local-delete.js +276 -0
  62. package/dist/extension/location-kinds.js +108 -0
  63. package/dist/extension/log-format.js +158 -0
  64. package/dist/extension/managed-changes.js +361 -0
  65. package/dist/extension/managed-items.js +57 -0
  66. package/dist/extension/match-index.js +375 -0
  67. package/dist/extension/missing-scripts.js +66 -0
  68. package/dist/extension/more-menu.js +109 -0
  69. package/dist/extension/move-dialog.js +197 -0
  70. package/dist/extension/offscreen.js +37 -0
  71. package/dist/extension/options.js +504 -0
  72. package/dist/extension/page-heading.js +170 -0
  73. package/dist/extension/page-links.js +199 -0
  74. package/dist/extension/parse-check-client.js +70 -0
  75. package/dist/extension/parse-check.js +161 -0
  76. package/dist/extension/parse-sandbox-host.js +172 -0
  77. package/dist/extension/parse-sandbox.js +175 -0
  78. package/dist/extension/popup-commands-open.js +41 -0
  79. package/dist/extension/popup-view.js +387 -0
  80. package/dist/extension/popup.js +703 -0
  81. package/dist/extension/prism-userscript.js +53 -0
  82. package/dist/extension/refresh-error.js +136 -0
  83. package/dist/extension/refresh-targets.js +160 -0
  84. package/dist/extension/related-versions.js +1144 -0
  85. package/dist/extension/resize-observer-loop.js +19 -0
  86. package/dist/extension/row-cache.js +387 -0
  87. package/dist/extension/script-blobs.js +413 -0
  88. package/dist/extension/script-details.js +658 -0
  89. package/dist/extension/script-enablement.js +285 -0
  90. package/dist/extension/script-identity.js +310 -0
  91. package/dist/extension/script-keys.js +54 -0
  92. package/dist/extension/script-log-id.js +155 -0
  93. package/dist/extension/script-log.js +290 -0
  94. package/dist/extension/script-rejection.js +104 -0
  95. package/dist/extension/script-sites.js +158 -0
  96. package/dist/extension/script-store.js +3507 -0
  97. package/dist/extension/script-updater.js +991 -0
  98. package/dist/extension/script-url-hash.js +61 -0
  99. package/dist/extension/select-all.js +37 -0
  100. package/dist/extension/select-open-guard.js +90 -0
  101. package/dist/extension/site-icon.js +1261 -0
  102. package/dist/extension/sm-dev-link.js +302 -0
  103. package/dist/extension/solo-click.js +58 -0
  104. package/dist/extension/sticky-numbers.js +188 -0
  105. package/dist/extension/storage-breakdown.js +253 -0
  106. package/dist/extension/storage-scan.js +86 -0
  107. package/dist/extension/storage-usage.js +194 -0
  108. package/dist/extension/structural-keys.js +29 -0
  109. package/dist/extension/sw-teardown.js +36 -0
  110. package/dist/extension/tab-status.js +403 -0
  111. package/dist/extension/toolbar-summary.js +105 -0
  112. package/dist/extension/user-scripts-access.js +43 -0
  113. package/dist/extension/userscript.js +3672 -0
  114. package/dist/extension/value-store.js +204 -0
  115. package/dist/extension/version-probe-fetch.js +68 -0
  116. package/dist/extension/worker-client.js +109 -0
  117. package/dist/extension/worker-ops.js +171 -0
  118. package/dist/extension/zip-writer.js +24 -0
  119. package/dist/external-files.js +878 -0
  120. package/dist/file-loader.js +436 -0
  121. package/dist/file-locations.js +439 -0
  122. package/dist/generated-wrappers.js +5 -0
  123. package/dist/github-auth.js +148 -0
  124. package/dist/github-urls.js +46 -0
  125. package/dist/gm-api-catalog.js +217 -0
  126. package/dist/gm-connect.js +273 -0
  127. package/dist/gm-xhr-types.js +5 -0
  128. package/dist/http-read.js +142 -0
  129. package/dist/icons/icon-128.png +0 -0
  130. package/dist/icons/icon-16.png +0 -0
  131. package/dist/icons/icon-48.png +0 -0
  132. package/dist/local-file-overrides.js +558 -0
  133. package/dist/manifest.json +38 -0
  134. package/dist/markup-response.js +126 -0
  135. package/dist/match-targets.js +305 -0
  136. package/dist/menu-captions.js +22 -0
  137. package/dist/offscreen.html +39 -0
  138. package/dist/options.html +197 -0
  139. package/dist/parse-check-protocol.js +51 -0
  140. package/dist/parse-location.js +171 -0
  141. package/dist/parse-sandbox.html +56 -0
  142. package/dist/phase-timing.js +155 -0
  143. package/dist/popup.html +197 -0
  144. package/dist/public-suffixes.js +6957 -0
  145. package/dist/run-context.js +31 -0
  146. package/dist/runtime-messages.js +34 -0
  147. package/dist/sandboxed-page.js +35 -0
  148. package/dist/script-build.js +787 -0
  149. package/dist/script-display.js +383 -0
  150. package/dist/script-icon.js +43 -0
  151. package/dist/script-links.js +204 -0
  152. package/dist/script-source-url.js +214 -0
  153. package/dist/script-world.js +65 -0
  154. package/dist/sm-dev-protocol.js +84 -0
  155. package/dist/source-map.js +177 -0
  156. package/dist/stack-link.js +138 -0
  157. package/dist/styles.css +825 -0
  158. package/dist/unsafe-window-use.js +229 -0
  159. package/dist/url-match.js +734 -0
  160. package/dist/userscript-header.js +248 -0
  161. package/dist/userscript.html +1165 -0
  162. package/dist/vendor/fflate.LICENSE +21 -0
  163. package/dist/vendor/fflate.js +2692 -0
  164. package/dist/vendor/prism/prism-line-numbers.css +1 -0
  165. package/dist/vendor/prism/prism-line-numbers.js +1 -0
  166. package/dist/vendor/prism/prism.css +1 -0
  167. package/dist/vendor/prism/prism.js +1946 -0
  168. package/dist/version-compare.js +141 -0
  169. package/dist/version-probe.js +120 -0
  170. package/lib/build-info.json +1 -0
  171. package/lib/bytes.d.ts +3 -0
  172. package/lib/bytes.js +37 -0
  173. package/lib/channel-placeholder.d.ts +1 -0
  174. package/lib/channel-placeholder.js +5 -0
  175. package/lib/collection-scan.d.ts +8 -0
  176. package/lib/collection-scan.js +17 -0
  177. package/lib/composed-source.d.ts +28 -0
  178. package/lib/composed-source.js +205 -0
  179. package/lib/debug-log.d.ts +9 -0
  180. package/lib/debug-log.js +75 -0
  181. package/lib/diagnostic-location.d.ts +8 -0
  182. package/lib/diagnostic-location.js +64 -0
  183. package/lib/eval-hook.d.ts +34 -0
  184. package/lib/eval-hook.js +308 -0
  185. package/lib/external-files.d.ts +111 -0
  186. package/lib/external-files.js +878 -0
  187. package/lib/file-loader.d.ts +48 -0
  188. package/lib/file-loader.js +436 -0
  189. package/lib/file-locations.d.ts +44 -0
  190. package/lib/file-locations.js +439 -0
  191. package/lib/generated-wrappers.d.ts +11 -0
  192. package/lib/generated-wrappers.js +5 -0
  193. package/lib/github-auth.d.ts +12 -0
  194. package/lib/github-auth.js +148 -0
  195. package/lib/github-urls.d.ts +1 -0
  196. package/lib/github-urls.js +46 -0
  197. package/lib/gm-api-catalog.d.ts +24 -0
  198. package/lib/gm-api-catalog.js +217 -0
  199. package/lib/gm-connect.d.ts +34 -0
  200. package/lib/gm-connect.js +273 -0
  201. package/lib/gm-xhr-types.d.ts +68 -0
  202. package/lib/gm-xhr-types.js +5 -0
  203. package/lib/harness/browser-extensions.d.ts +73 -0
  204. package/lib/harness/browser-extensions.js +285 -0
  205. package/lib/harness/browser-lang.d.ts +20 -0
  206. package/lib/harness/browser-lang.js +86 -0
  207. package/lib/harness/browser-launch.d.ts +14 -0
  208. package/lib/harness/browser-launch.js +184 -0
  209. package/lib/harness/checks.d.ts +110 -0
  210. package/lib/harness/checks.js +321 -0
  211. package/lib/harness/cli.d.ts +2 -0
  212. package/lib/harness/cli.js +2259 -0
  213. package/lib/harness/command-args.d.ts +91 -0
  214. package/lib/harness/command-args.js +369 -0
  215. package/lib/harness/command-lines.d.ts +80 -0
  216. package/lib/harness/command-lines.js +180 -0
  217. package/lib/harness/command-list.d.ts +206 -0
  218. package/lib/harness/command-list.js +1475 -0
  219. package/lib/harness/console-commands.d.ts +463 -0
  220. package/lib/harness/console-commands.js +3216 -0
  221. package/lib/harness/download-watch.d.ts +21 -0
  222. package/lib/harness/download-watch.js +156 -0
  223. package/lib/harness/extension-backend.d.ts +41 -0
  224. package/lib/harness/extension-backend.js +100 -0
  225. package/lib/harness/extension-host.d.ts +112 -0
  226. package/lib/harness/extension-host.js +552 -0
  227. package/lib/harness/extension-launch.d.ts +67 -0
  228. package/lib/harness/extension-launch.js +285 -0
  229. package/lib/harness/extension-link.d.ts +315 -0
  230. package/lib/harness/extension-link.js +607 -0
  231. package/lib/harness/external-manager.d.ts +146 -0
  232. package/lib/harness/external-manager.js +414 -0
  233. package/lib/harness/fixture.d.ts +165 -0
  234. package/lib/harness/fixture.js +461 -0
  235. package/lib/harness/frame-sessions.d.ts +71 -0
  236. package/lib/harness/frame-sessions.js +248 -0
  237. package/lib/harness/frames.d.ts +68 -0
  238. package/lib/harness/frames.js +216 -0
  239. package/lib/harness/help-format.d.ts +17 -0
  240. package/lib/harness/help-format.js +127 -0
  241. package/lib/harness/host.d.ts +352 -0
  242. package/lib/harness/host.js +423 -0
  243. package/lib/harness/index.d.ts +8 -0
  244. package/lib/harness/index.js +10 -0
  245. package/lib/harness/input-file.d.ts +18 -0
  246. package/lib/harness/input-file.js +61 -0
  247. package/lib/harness/json-output.d.ts +565 -0
  248. package/lib/harness/json-output.js +71 -0
  249. package/lib/harness/manager-actions.d.ts +21 -0
  250. package/lib/harness/manager-actions.js +129 -0
  251. package/lib/harness/manager-commands.d.ts +142 -0
  252. package/lib/harness/manager-commands.js +1196 -0
  253. package/lib/harness/manager-fetch.d.ts +33 -0
  254. package/lib/harness/manager-fetch.js +239 -0
  255. package/lib/harness/managers.d.ts +24 -0
  256. package/lib/harness/managers.js +27 -0
  257. package/lib/harness/output-style.d.ts +125 -0
  258. package/lib/harness/output-style.js +322 -0
  259. package/lib/harness/page-commands.d.ts +134 -0
  260. package/lib/harness/page-commands.js +1141 -0
  261. package/lib/harness/page-output.d.ts +67 -0
  262. package/lib/harness/page-output.js +139 -0
  263. package/lib/harness/page-watch.d.ts +171 -0
  264. package/lib/harness/page-watch.js +607 -0
  265. package/lib/harness/parse-check-node.d.ts +3 -0
  266. package/lib/harness/parse-check-node.js +132 -0
  267. package/lib/harness/path-args.d.ts +22 -0
  268. package/lib/harness/path-args.js +148 -0
  269. package/lib/harness/playwright-chromium.d.ts +16 -0
  270. package/lib/harness/playwright-chromium.js +86 -0
  271. package/lib/harness/prepare.d.ts +168 -0
  272. package/lib/harness/prepare.js +364 -0
  273. package/lib/harness/prompt-editor.d.ts +17 -0
  274. package/lib/harness/prompt-editor.js +354 -0
  275. package/lib/harness/prompt-history.d.ts +13 -0
  276. package/lib/harness/prompt-history.js +89 -0
  277. package/lib/harness/rebuild-advice.d.ts +6 -0
  278. package/lib/harness/rebuild-advice.js +39 -0
  279. package/lib/harness/run-log.d.ts +47 -0
  280. package/lib/harness/run-log.js +140 -0
  281. package/lib/harness/run-logs.d.ts +132 -0
  282. package/lib/harness/run-logs.js +241 -0
  283. package/lib/harness/run.d.ts +194 -0
  284. package/lib/harness/run.js +1973 -0
  285. package/lib/harness/runs.d.ts +229 -0
  286. package/lib/harness/runs.js +726 -0
  287. package/lib/harness/save-html.d.ts +39 -0
  288. package/lib/harness/save-html.js +89 -0
  289. package/lib/harness/saved-pages.d.ts +120 -0
  290. package/lib/harness/saved-pages.js +275 -0
  291. package/lib/harness/script-mark.d.ts +17 -0
  292. package/lib/harness/script-mark.js +56 -0
  293. package/lib/harness/snapshot.d.ts +143 -0
  294. package/lib/harness/snapshot.js +474 -0
  295. package/lib/harness/stuck-tabs.d.ts +19 -0
  296. package/lib/harness/stuck-tabs.js +131 -0
  297. package/lib/harness/tab-refs.d.ts +20 -0
  298. package/lib/harness/tab-refs.js +80 -0
  299. package/lib/harness/tampermonkey.d.ts +71 -0
  300. package/lib/harness/tampermonkey.js +356 -0
  301. package/lib/harness/tm-ids.d.ts +18 -0
  302. package/lib/harness/tm-ids.js +88 -0
  303. package/lib/harness/violentmonkey.d.ts +34 -0
  304. package/lib/harness/violentmonkey.js +162 -0
  305. package/lib/harness/wait.d.ts +77 -0
  306. package/lib/harness/wait.js +458 -0
  307. package/lib/http-read.d.ts +30 -0
  308. package/lib/http-read.js +142 -0
  309. package/lib/local-file-overrides.d.ts +35 -0
  310. package/lib/local-file-overrides.js +558 -0
  311. package/lib/manifest.json +38 -0
  312. package/lib/markup-response.d.ts +11 -0
  313. package/lib/markup-response.js +126 -0
  314. package/lib/match-targets.d.ts +18 -0
  315. package/lib/match-targets.js +305 -0
  316. package/lib/menu-captions.d.ts +1 -0
  317. package/lib/menu-captions.js +22 -0
  318. package/lib/parse-check-protocol.d.ts +27 -0
  319. package/lib/parse-check-protocol.js +51 -0
  320. package/lib/parse-location.d.ts +12 -0
  321. package/lib/parse-location.js +171 -0
  322. package/lib/phase-timing.d.ts +12 -0
  323. package/lib/phase-timing.js +155 -0
  324. package/lib/public-suffixes.d.ts +1 -0
  325. package/lib/public-suffixes.js +6957 -0
  326. package/lib/run-context.d.ts +2 -0
  327. package/lib/run-context.js +31 -0
  328. package/lib/runtime-messages.d.ts +10 -0
  329. package/lib/runtime-messages.js +34 -0
  330. package/lib/sandboxed-page.d.ts +5 -0
  331. package/lib/sandboxed-page.js +35 -0
  332. package/lib/script-build.d.ts +111 -0
  333. package/lib/script-build.js +787 -0
  334. package/lib/script-display.d.ts +51 -0
  335. package/lib/script-display.js +383 -0
  336. package/lib/script-icon.d.ts +16 -0
  337. package/lib/script-icon.js +43 -0
  338. package/lib/script-links.d.ts +24 -0
  339. package/lib/script-links.js +204 -0
  340. package/lib/script-source-url.d.ts +8 -0
  341. package/lib/script-source-url.js +214 -0
  342. package/lib/script-world.d.ts +6 -0
  343. package/lib/script-world.js +65 -0
  344. package/lib/sm-dev-protocol.d.ts +251 -0
  345. package/lib/sm-dev-protocol.js +84 -0
  346. package/lib/source-map.d.ts +32 -0
  347. package/lib/source-map.js +177 -0
  348. package/lib/stack-link.d.ts +15 -0
  349. package/lib/stack-link.js +138 -0
  350. package/lib/unsafe-window-use.d.ts +1 -0
  351. package/lib/unsafe-window-use.js +229 -0
  352. package/lib/url-match.d.ts +40 -0
  353. package/lib/url-match.js +734 -0
  354. package/lib/userscript-header.d.ts +33 -0
  355. package/lib/userscript-header.js +248 -0
  356. package/lib/version-compare.d.ts +6 -0
  357. package/lib/version-compare.js +141 -0
  358. package/lib/version-probe.d.ts +17 -0
  359. package/lib/version-probe.js +120 -0
  360. package/package.json +55 -3
  361. package/skills/SourceMonkey-dev/SKILL.md +868 -0
  362. package/skills/SourceMonkey-dev/references/cdp.md +293 -0
  363. package/skills/SourceMonkey-dev/references/commands.md +297 -0
  364. package/skills/SourceMonkey-dev/references/other-managers.md +63 -0
  365. package/skills/SourceMonkey-dev/references/specs.md +46 -0
  366. package/skills/SourceMonkey-friction/SKILL.md +103 -0
  367. package/skills/SourceMonkey-friction/references/template.md +59 -0
@@ -0,0 +1,584 @@
1
+ // SW-side `GM_xmlhttpRequest` handler. Drives `fetch` against the
2
+ // page's request, sends events back to the originating tab via
3
+ // `chrome.tabs.sendMessage`. Supports text and binary request bodies
4
+ // and response types (text/json/blob/arraybuffer/document/stream),
5
+ // and `@connect` URL gating.
6
+ // Requests are credentialed (the user's cookies go out) unless the
7
+ // caller passes `anonymous: true` — see `runXhr`.
8
+ // Headers `fetch` refuses to send (Origin / Cookie / Referer /
9
+ // Sec-Fetch-*) travel as a declarativeNetRequest rule instead — see
10
+ // `gm-xhr-headers.ts`.
11
+ //
12
+ // Wire protocol:
13
+ //
14
+ // (The payload types are in gm-xhr-types.ts.)
15
+ //
16
+ // Page → SW: `sm:gm-xhr-start`
17
+ // { type, id, scriptId, request: GmXhrRequestPayload }
18
+ //
19
+ // Page → SW: `sm:gm-xhr-abort`
20
+ // { type, id }
21
+ //
22
+ // SW → page: `sm:gm-xhr-event`
23
+ // { type, id, event: GmXhrStreamEvent | GmXhrEvent }
24
+ // The stream events (headers, then the body's pieces) first, then
25
+ // one terminal event.
26
+ //
27
+ // `id` is a per-request UUID-ish string the page generates. The SW
28
+ // just round-trips it.
29
+ //
30
+ // The response body streams to the page in `progress` pieces ahead of
31
+ // the terminal `load` (see `streamBody`), so the worker never holds the
32
+ // whole of it and no size cap is needed.
33
+ //
34
+ // One safety limit lives here:
35
+ // - `@connect` redirect re-validation: `fetch` follows redirects by
36
+ // default, so an allowlisted host could 302 to a disallowed one.
37
+ // The caller passes `checkFinalUrl`; if the response came back
38
+ // from a redirected URL the policy refuses (the user can be asked
39
+ // about it, as for the first URL), we drop the body and return the
40
+ // policy's error instead of handing it to the script.
41
+ // This blocks *reading* responses from non-allowlisted hosts; it
42
+ // can't prevent the request reaching the redirect target (fetch
43
+ // gives no per-hop hook and `redirect:'manual'` yields an opaque
44
+ // response with no readable Location), so the outbound hop still
45
+ // happens — but no disallowed-host data flows back to the script.
46
+ // That hop carries the user's cookies, so a script can provoke a
47
+ // credentialed request to an off-allowlist host it can't read the
48
+ // answer from. Blind, but a real side effect.
49
+ //
50
+ // AbortController per request. Stored in a Map keyed by id so an
51
+ // aborted request can be cancelled mid-fetch.
52
+ import { base64ToBytes, bytesToBase64 } from '../bytes.js';
53
+ import { headerRuleFailedMessage, headerStallMessage, redirectRefusedMessage, unreachableMessage, } from '../gm-connect.js';
54
+ import { buildHeaderRule, splitHeaders, whenStaleRulesCleared, } from './gm-xhr-headers.js';
55
+ const activeXhrs = new Map();
56
+ // How long a request may go without *response headers* before the
57
+ // worker gives up on it. Deliberately not a request timeout:
58
+ //
59
+ // - `GM_xmlhttpRequest` has no default `timeout`, matching TM/VM, and
60
+ // adding one would kill slow but healthy transfers — a 50 MB body
61
+ // on a bad connection can legitimately run for minutes.
62
+ // - What has to be bounded is the *header* phase, because that is
63
+ // exactly what the request's header rule waits on: the rule is
64
+ // removed the moment `fetch` resolves, so a slow body holds
65
+ // nothing, while a server that accepts the connection and never
66
+ // answers would keep a rule (and its id) alive indefinitely.
67
+ // - So the timer covers time-to-first-byte only, and is cleared as
68
+ // soon as headers arrive. A download that takes an hour after that
69
+ // is left alone.
70
+ //
71
+ // Two minutes is far past any healthy time-to-first-byte and well under
72
+ // the page-side `XHR_PAGE_SIDE_TTL_MS` (5 minutes, in
73
+ // `wrapper-gm-runtime.js`), so the worker reports a wedged request
74
+ // itself rather than racing the page's own giving-up. Keep it that way:
75
+ // if it ever exceeds the page TTL, both would fire and a script would
76
+ // see whichever won.
77
+ const XHR_HEADER_STALL_MS = 2 * 60 * 1000;
78
+ // How much of the body goes in one `progress` message, at most. Big
79
+ // enough that a large download isn't thousands of messages, small
80
+ // enough that the worker never holds much of it and a message stays far
81
+ // under Chrome's 64 MiB limit (base64 adds a third).
82
+ const MAX_PIECE_BYTES = 1024 * 1024;
83
+ // One run of the body's pieces into a single array.
84
+ function joinPieces(pieces, total) {
85
+ if (pieces.length === 1)
86
+ return pieces[0];
87
+ const out = new Uint8Array(total);
88
+ let offset = 0;
89
+ for (const p of pieces) {
90
+ out.set(p, offset);
91
+ offset += p.byteLength;
92
+ }
93
+ return out;
94
+ }
95
+ // The body's length when the response says it, for `lengthComputable`.
96
+ // Not with a Content-Encoding: Content-Length then counts the encoded
97
+ // bytes, and the reader hands over decoded ones.
98
+ function bodyLength(headers) {
99
+ const encoding = headers.get('content-encoding');
100
+ if (encoding && encoding.trim().toLowerCase() !== 'identity')
101
+ return null;
102
+ const length = Number(headers.get('content-length'));
103
+ return headers.has('content-length') && Number.isFinite(length) && length >= 0 ? length : null;
104
+ }
105
+ // Render the response headers as a single newline-joined string —
106
+ // TM/VM convention; userscripts call `getResponseHeader(name)`-style
107
+ // helpers on this. Each header is `Name: value\r\n`; we use `\r\n`
108
+ // per spec but `\n` would also work.
109
+ function serializeHeaders(headers) {
110
+ const lines = [];
111
+ headers.forEach((value, name) => {
112
+ lines.push(`${name}: ${value}`);
113
+ });
114
+ // Trailing CRLF after the last header — TM/VM behavior. Userscripts
115
+ // that split on \r\n and iterate get the expected N entries (rather
116
+ // than N-1 trailing-token-eaten).
117
+ return lines.length === 0 ? '' : lines.join('\r\n') + '\r\n';
118
+ }
119
+ // A stream event that couldn't be delivered: the page that asked is
120
+ // gone (closed, or navigated away). Chrome's own error says so in its
121
+ // terms; this says it in ours.
122
+ class PageGoneError extends Error {
123
+ constructor() {
124
+ super('The page that made the request is gone, so the request was stopped');
125
+ this.name = 'PageGoneError';
126
+ }
127
+ }
128
+ // A request that got no response at all, said in our words.
129
+ class UnreachableError extends Error {
130
+ constructor(url) {
131
+ super(unreachableMessage(URL.parse(url)?.host || url));
132
+ this.name = 'UnreachableError';
133
+ }
134
+ }
135
+ // Run a fetch on behalf of the page. Always resolves to the terminal
136
+ // event to send back, including for aborts and timeouts — the page-side
137
+ // runtime fires its own `onabort` immediately as well, so a script sees
138
+ // that one first.
139
+ //
140
+ // Before that, `push` delivers the stream events: the headers, then the
141
+ // body in pieces as it arrives (`streamBody`). Each push is awaited, so
142
+ // they arrive in order and a slow page holds back the reading rather
143
+ // than letting the worker buffer the body. A push that throws (the tab
144
+ // is gone) aborts the request.
145
+ //
146
+ // `checkFinalUrl`, when supplied, re-checks the response's URL against
147
+ // the script's `@connect` policy after redirects, resolving to null when
148
+ // allowed or else the error to report. A response that landed on a
149
+ // refused host (via a redirect the initial check couldn't see) is
150
+ // dropped with that error rather than returned.
151
+ //
152
+ // Request headers the browser's `fetch` refuses to send (`Origin`,
153
+ // `Cookie`, `Sec-Fetch-*`) go out by a short-lived declarativeNetRequest
154
+ // rule around the request (docs/xhr-header-surgery.md).
155
+ export async function runXhr(id, request, push, checkFinalUrl, tabId = null) {
156
+ // Taken over from `reserveXhr` when the caller made one, so an abort
157
+ // that came while the request waited on the policy counts.
158
+ const controller = activeXhrs.get(id)?.controller ?? new AbortController();
159
+ activeXhrs.set(id, { controller, tabId });
160
+ if (controller.signal.aborted) {
161
+ activeXhrs.delete(id);
162
+ return { kind: 'abort' };
163
+ }
164
+ // Timeout: setTimeout that aborts the controller. We track the
165
+ // timer so we can clear it on completion.
166
+ let timeoutFired = false;
167
+ let timeoutTimer = null;
168
+ let headerStallFired = false;
169
+ let headerStallTimer = null;
170
+ // Every path out of an aborted controller decides the terminal event
171
+ // here. The script's `timeout`, the header-stall backstop and a plain
172
+ // `abort()` all abort the *same* controller, so the shadow flags are
173
+ // the only thing separating them.
174
+ //
175
+ // The script's own timeout outranks the backstop: if both fired, the
176
+ // one it asked for is the one it should hear about.
177
+ const terminalAbortEvent = () => {
178
+ if (timeoutFired)
179
+ return { kind: 'timeout' };
180
+ if (headerStallFired) {
181
+ return { kind: 'error', message: headerStallMessage(XHR_HEADER_STALL_MS) };
182
+ }
183
+ return { kind: 'abort' };
184
+ };
185
+ if (request.timeout && request.timeout > 0) {
186
+ timeoutTimer = setTimeout(() => {
187
+ timeoutFired = true;
188
+ controller.abort();
189
+ }, request.timeout);
190
+ }
191
+ const clearHeaderStall = () => {
192
+ if (headerStallTimer != null) {
193
+ clearTimeout(headerStallTimer);
194
+ headerStallTimer = null;
195
+ }
196
+ };
197
+ // Header-phase backstop. Armed just before the rule install rather
198
+ // than just before the fetch: the install is the only other await
199
+ // that could wedge, and covering it costs nothing since it normally
200
+ // takes a millisecond.
201
+ const startHeaderStall = () => {
202
+ headerStallTimer = setTimeout(() => {
203
+ headerStallFired = true;
204
+ controller.abort();
205
+ }, XHR_HEADER_STALL_MS);
206
+ };
207
+ // Headers `fetch` would drop travel as a DNR rule instead; the rest
208
+ // ride along on the request. `installedRuleId` is held so the rule
209
+ // comes off however the request ends.
210
+ const { passthrough, surgical } = splitHeaders(request.headers);
211
+ let installedRuleId = null;
212
+ try {
213
+ const method = (request.method ?? 'GET').toUpperCase();
214
+ // Built once: `buildHeaderRule` allocates an id, so asking it twice
215
+ // would burn one and answer a different question the second time.
216
+ // Where there is a rule, `spec.target` is the URL to fetch — the
217
+ // same tagged string the rule matches on, which is what makes the
218
+ // rule apply to this request and no other.
219
+ const spec = buildHeaderRule(request.url, surgical, method);
220
+ startHeaderStall();
221
+ if (spec) {
222
+ installedRuleId = await installHeaderRule(spec);
223
+ // Refuse rather than send something the script didn't ask for.
224
+ // The headers it wanted are the ones the browser won't let it
225
+ // set, so a silent fallback isn't "the request minus a header": a
226
+ // `Cookie` override that doesn't apply sends the user's real
227
+ // session instead of the script's — the opposite of the ask.
228
+ // A rule that exists only to strip our own `Origin` is not the
229
+ // script's request, so that one falls back quietly.
230
+ if (installedRuleId == null && Object.keys(surgical).length > 0) {
231
+ return { kind: 'error', message: headerRuleFailedMessage() };
232
+ }
233
+ }
234
+ const init = {
235
+ method,
236
+ headers: Object.keys(passthrough).length > 0 ? passthrough : undefined,
237
+ // Credentialed by default, matching Tampermonkey/Violentmonkey —
238
+ // the point of GM_xmlhttpRequest is usually to read another site
239
+ // *as the signed-in user*, and `anonymous: true` opts out.
240
+ //
241
+ // 'include' is stated rather than left to the default because the
242
+ // default's behavior here isn't ours to rely on: Chrome already
243
+ // attaches cookies to an extension fetch aimed at a host the
244
+ // extension has permission for (we hold `<all_urls>`), so
245
+ // 'same-origin' happens to send them today. That is a property of
246
+ // the host-permission grant, not of the request we wrote. Saying
247
+ // 'include' pins the intent and gives `anonymous` a symmetric
248
+ // counterpart.
249
+ //
250
+ // The `@connect` allowlist is what gates which hosts a script may
251
+ // act as the user on.
252
+ credentials: request.anonymous ? 'omit' : 'include',
253
+ signal: controller.signal,
254
+ };
255
+ // GET / HEAD with no body — fetch() rejects "body not allowed".
256
+ if (init.method !== 'GET' && init.method !== 'HEAD') {
257
+ if (request.body)
258
+ init.body = requestBody(request.body);
259
+ else if (request.data != null)
260
+ init.body = typeof request.data === 'string' ? request.data : String(request.data);
261
+ }
262
+ if (request.nocache)
263
+ init.cache = 'no-store';
264
+ else if (request.revalidate)
265
+ init.cache = 'no-cache';
266
+ // 'manual' keeps the redirect from being followed, and the response
267
+ // it gives says it was one.
268
+ if (request.redirect === 'error')
269
+ init.redirect = 'manual';
270
+ // The tagged URL where there's a rule: the tag is what the rule
271
+ // matches on, and it never leaves the browser (a fragment isn't
272
+ // transmitted, and `res.url` comes back without it).
273
+ //
274
+ // Chrome's message for a network failure is exactly "Failed to
275
+ // fetch"; other `TypeError`s (a header name `fetch` won't take)
276
+ // name their own problem and pass through.
277
+ const res = await fetch(spec?.target ?? request.url, init).catch((err) => {
278
+ if (!controller.signal.aborted && err instanceof TypeError && err.message === 'Failed to fetch') {
279
+ throw new UnreachableError(request.url);
280
+ }
281
+ throw err;
282
+ });
283
+ if (res.type === 'opaqueredirect') {
284
+ return { kind: 'error', message: redirectRefusedMessage() };
285
+ }
286
+ // Headers are in, so the stall backstop has nothing left to guard —
287
+ // whatever the body does from here is the download's business.
288
+ clearHeaderStall();
289
+ // The rule has done its job the moment the request is on the wire,
290
+ // and `fetch` resolving means the response headers are already
291
+ // back. Dropping it here rather than in `finally` keeps a slow
292
+ // download from holding a rule (and its id) for the whole transfer.
293
+ if (installedRuleId != null) {
294
+ const ruleId = installedRuleId;
295
+ installedRuleId = null;
296
+ await removeHeaderRule(ruleId);
297
+ }
298
+ // @connect redirect re-validation: if fetch followed a redirect to
299
+ // a host the policy doesn't allow, refuse to read or return the
300
+ // body. (See the file header for why the outbound hop can't be
301
+ // prevented here.) We only check on actual redirects — an
302
+ // un-redirected response is on the host the initial check already
303
+ // approved.
304
+ const refusal = res.redirected && checkFinalUrl ? await checkFinalUrl(res.url, controller.signal) : null;
305
+ // Aborted (or timed out) while the check asked the user.
306
+ if (controller.signal.aborted)
307
+ return terminalAbortEvent();
308
+ if (refusal !== null)
309
+ return { kind: 'error', message: refusal };
310
+ const total = bodyLength(res.headers);
311
+ const responseHeaders = serializeHeaders(res.headers);
312
+ // A push that fails means the page is gone; said in our words.
313
+ const pushOrStop = (event) => push(event).catch(() => {
314
+ throw new PageGoneError();
315
+ });
316
+ await pushOrStop({
317
+ kind: 'headers', status: res.status, statusText: res.statusText, finalUrl: res.url, responseHeaders,
318
+ mimeType: res.headers.get('content-type') ?? '', total,
319
+ });
320
+ await streamBody(res, total, pushOrStop, controller);
321
+ // The body went ahead in `progress` events; the page-side runtime
322
+ // builds `responseText` and `response` from them.
323
+ return {
324
+ kind: 'load',
325
+ response: {
326
+ status: res.status,
327
+ statusText: res.statusText,
328
+ responseURL: res.url,
329
+ responseText: '',
330
+ response: null,
331
+ responseMimeType: res.headers.get('content-type') ?? '',
332
+ responseHeaders,
333
+ finalUrl: res.url,
334
+ },
335
+ };
336
+ }
337
+ catch (err) {
338
+ // Distinguish abort vs. timeout vs. error. AbortController-driven
339
+ // aborts surface as DOMException name='AbortError'; we use a
340
+ // shadow flag set by the timeout handler to know which one.
341
+ const isAbortError = err instanceof Error && (err.name === 'AbortError' || /aborted/i.test(err.message));
342
+ // The script's own timeout outranks everything, then the aborts.
343
+ if (timeoutFired)
344
+ return terminalAbortEvent();
345
+ if (err instanceof PageGoneError || err instanceof UnreachableError) {
346
+ return { kind: 'error', message: err.message };
347
+ }
348
+ if (headerStallFired || isAbortError)
349
+ return terminalAbortEvent();
350
+ const message = err instanceof Error ? err.message : String(err);
351
+ return { kind: 'error', message };
352
+ }
353
+ finally {
354
+ if (timeoutTimer != null)
355
+ clearTimeout(timeoutTimer);
356
+ clearHeaderStall();
357
+ activeXhrs.delete(id);
358
+ // Ends a body nobody read (a redirect refused, a page gone before
359
+ // the headers went out), which would otherwise hold its connection.
360
+ // Nothing once the request is done.
361
+ controller.abort();
362
+ // Backstop for the abort, timeout and error paths — the success
363
+ // path already dropped it. `removeHeaderRule` swallows its own
364
+ // failures, so this can't reject out of the `finally`.
365
+ if (installedRuleId != null)
366
+ await removeHeaderRule(installedRuleId);
367
+ }
368
+ }
369
+ // A non-text request body as `fetch` takes it. `fetch` sets the
370
+ // Content-Type from it (a Blob's type, the multipart boundary, or
371
+ // form-urlencoded) unless the script's headers set one.
372
+ function requestBody(body) {
373
+ switch (body.kind) {
374
+ case 'bytes':
375
+ return new Blob([base64ToBytes(body.data)], { type: body.type });
376
+ case 'params':
377
+ return new URLSearchParams(body.data);
378
+ case 'form': {
379
+ const form = new FormData();
380
+ for (const entry of body.entries) {
381
+ if ('value' in entry)
382
+ form.append(entry.name, entry.value);
383
+ else
384
+ form.append(entry.name, new Blob([base64ToBytes(entry.data)], { type: entry.type }), entry.filename);
385
+ }
386
+ return form;
387
+ }
388
+ }
389
+ }
390
+ // Read the body and push it in pieces. A piece goes out as soon as the
391
+ // previous push is done, holding whatever arrived meanwhile (up to
392
+ // MAX_PIECE_BYTES), so a trickle of small reads still reaches the page
393
+ // promptly while a fast download goes out in large pieces. Reading
394
+ // waits only when a full piece is already waiting behind a push, so the
395
+ // worker holds at most about two pieces.
396
+ //
397
+ // A push resolves once the page's listeners have run (none of them
398
+ // answers with `sendResponse`), which is what makes it a brake: an
399
+ // isolated-world page takes each piece as it comes. A page-world
400
+ // script's pieces go through the bridge, which only re-posts them, so
401
+ // there the brake is the bridge, not the script.
402
+ //
403
+ // A failed push aborts `controller` at once, so a read still waiting on
404
+ // the network ends now rather than with the next chunk.
405
+ async function streamBody(res, total, push, controller) {
406
+ const reader = res.body?.getReader();
407
+ // No body at all (a 204, a HEAD).
408
+ if (!reader)
409
+ return;
410
+ // In an object, as the push's callbacks change it.
411
+ const state = {
412
+ waiting: [],
413
+ waitingBytes: 0,
414
+ loaded: 0,
415
+ sending: null,
416
+ failed: null,
417
+ };
418
+ const send = () => {
419
+ const piece = joinPieces(state.waiting, state.waitingBytes);
420
+ state.waiting = [];
421
+ state.waitingBytes = 0;
422
+ state.loaded += piece.byteLength;
423
+ state.sending = push({ kind: 'progress', data: bytesToBase64(piece), loaded: state.loaded, total })
424
+ .then(() => {
425
+ state.sending = null;
426
+ // What arrived meanwhile goes now, not with the next read: on
427
+ // a trickle that could be a long way off.
428
+ if (state.waitingBytes > 0)
429
+ send();
430
+ }, (err) => {
431
+ state.sending = null;
432
+ state.failed = { err };
433
+ controller.abort();
434
+ });
435
+ };
436
+ // The pushes in flight done, and a failed one thrown.
437
+ const settle = async () => {
438
+ while (state.sending)
439
+ await state.sending;
440
+ if (state.failed)
441
+ throw state.failed.err;
442
+ };
443
+ try {
444
+ for (;;) {
445
+ let result;
446
+ try {
447
+ result = await reader.read();
448
+ }
449
+ catch (err) {
450
+ // Our own abort, for a failed push: that failure is the cause.
451
+ if (state.failed)
452
+ throw state.failed.err;
453
+ throw err;
454
+ }
455
+ const { done, value } = result;
456
+ if (done)
457
+ break;
458
+ if (state.failed)
459
+ throw state.failed.err;
460
+ if (!value)
461
+ continue;
462
+ // A read bigger than a piece is split, so no message outgrows one.
463
+ for (let at = 0; at < value.byteLength; at += MAX_PIECE_BYTES) {
464
+ const part = value.subarray(at, at + MAX_PIECE_BYTES);
465
+ // A full piece waiting: it goes next, once the pushes ahead of
466
+ // it are done.
467
+ if (state.waitingBytes > 0 && state.waitingBytes + part.byteLength > MAX_PIECE_BYTES) {
468
+ await settle();
469
+ if (state.waitingBytes > 0)
470
+ send();
471
+ }
472
+ state.waiting.push(part);
473
+ state.waitingBytes += part.byteLength;
474
+ if (!state.sending)
475
+ send();
476
+ }
477
+ }
478
+ await settle();
479
+ if (state.waitingBytes > 0) {
480
+ send();
481
+ await settle();
482
+ }
483
+ }
484
+ catch (err) {
485
+ try {
486
+ await reader.cancel();
487
+ }
488
+ catch { /* already torn down */ }
489
+ throw err;
490
+ }
491
+ }
492
+ // Install one request's header rule. Returns the id to remove
493
+ // afterwards, or null if it couldn't be installed — the caller decides
494
+ // whether that is fatal (it is, when the script asked for the headers).
495
+ async function installHeaderRule(spec) {
496
+ const dnr = chrome.declarativeNetRequest;
497
+ if (!dnr)
498
+ return null;
499
+ // The startup sweep reads every session rule and deletes the ones in
500
+ // our band. A rule added while that read is in flight is in its
501
+ // snapshot, so installing first would let the sweep delete a live
502
+ // rule. Wait it out — it runs once per worker life.
503
+ await whenStaleRulesCleared();
504
+ try {
505
+ // Clear the id first: `addRules` rejects a duplicate id outright,
506
+ // and an id can still be occupied if a rule ever escaped its
507
+ // `finally` (the counter wraps). Dropping a stale rule of ours is
508
+ // right anyway — its request is long gone.
509
+ await dnr.updateSessionRules({ removeRuleIds: [spec.id], addRules: [spec.rule] });
510
+ return spec.id;
511
+ }
512
+ catch (err) {
513
+ console.info('[SourceMonkey] could not apply GM_xmlhttpRequest header rule', err);
514
+ return null;
515
+ }
516
+ }
517
+ // Take one request's rule back off. A rule that survives its request
518
+ // matches nothing *this* worker will request — the counter has moved
519
+ // past its tag — but the tag comes back when the next worker restarts
520
+ // the counter, so it is worth a second attempt before giving up and
521
+ // saying so. What catches the leftover is the startup sweep and the
522
+ // id pre-clear in `installHeaderRule`.
523
+ async function removeHeaderRule(ruleId) {
524
+ for (let attempt = 0; attempt < 2; attempt += 1) {
525
+ try {
526
+ await chrome.declarativeNetRequest.updateSessionRules({ removeRuleIds: [ruleId] });
527
+ return;
528
+ }
529
+ catch (err) {
530
+ if (attempt === 1) {
531
+ console.info('[SourceMonkey] could not remove a GM_xmlhttpRequest header rule; it stays live until the next startup sweep', err);
532
+ }
533
+ }
534
+ }
535
+ }
536
+ // Make a request abortable before `runXhr` starts it: while it waits
537
+ // on the user's answer to the connection prompt, the script may call
538
+ // `abort()` or its tab may close. `runXhr` takes the entry over; a
539
+ // caller that ends up not running the request calls `releaseXhr`.
540
+ export function reserveXhr(id, tabId) {
541
+ const controller = new AbortController();
542
+ activeXhrs.set(id, { controller, tabId });
543
+ if (earlyAborts.delete(id))
544
+ controller.abort();
545
+ return controller.signal;
546
+ }
547
+ // Ids aborted before the worker took their request: an `abort()` right
548
+ // after the call can overtake the start message, which first waits on
549
+ // checks (whose bridge it came through) before `reserveXhr`. Kept a
550
+ // minute, far longer than that wait; without them the request would
551
+ // still go out.
552
+ const earlyAborts = new Set();
553
+ const EARLY_ABORT_TTL_MS = 60 * 1000;
554
+ export function releaseXhr(id) {
555
+ activeXhrs.delete(id);
556
+ }
557
+ // Cancel an in-flight request by id. Idempotent — calling abort on a
558
+ // completed request is a no-op (the entry is gone). An id not seen yet
559
+ // is remembered, for a start message still on its way.
560
+ export function abortXhr(id) {
561
+ const entry = activeXhrs.get(id);
562
+ if (!entry) {
563
+ earlyAborts.add(id);
564
+ setTimeout(() => earlyAborts.delete(id), EARLY_ABORT_TTL_MS);
565
+ return false;
566
+ }
567
+ entry.controller.abort();
568
+ return true;
569
+ }
570
+ // Cancel everything a closed tab left running. Its page is gone, so no
571
+ // terminal event can be delivered and nobody is waiting for the body —
572
+ // but without this the fetch runs to completion in the worker, holding
573
+ // its header rule live for as long as the server keeps the connection
574
+ // open.
575
+ //
576
+ // Safe to iterate while aborting: the entry is removed in `runXhr`'s
577
+ // `finally`, which runs in a later microtask, and a Map iterator
578
+ // tolerates deletion regardless.
579
+ export function abortXhrsForTab(tabId) {
580
+ for (const entry of activeXhrs.values()) {
581
+ if (entry.tabId === tabId)
582
+ entry.controller.abort();
583
+ }
584
+ }