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