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,277 @@
1
+ // Captures navigations to `*.user.js` URLs — http(s):// and file://
2
+ // alike — and routes them to the userscript viewer's preview mode
3
+ // instead.
4
+ //
5
+ // Why this is needed at all: Chrome never renders a `.user.js` URL as
6
+ // a page you can click "install" on. It is treated like any other
7
+ // resource, so a `text/javascript` response just renders as source
8
+ // text and a local file opens as text. The user never gets an install
9
+ // prompt, so a script manager has to intercept the navigation itself.
10
+ // Tampermonkey and Violentmonkey do the same thing.
11
+ //
12
+ // (Chrome ≤ 151 special-cased the suffix and forced the navigation
13
+ // into a download that was then handed to the MV2 extension
14
+ // installer. `minimum_chrome_version` in the manifest puts the floor
15
+ // at 152, where crrev 38b617ca removed that; nothing here works
16
+ // around it any more.)
17
+ //
18
+ // The capture itself is `webNavigation.onBeforeNavigate`, which fires
19
+ // before the request goes out: we rewrite the tab to
20
+ // `userscript.html?preview=…`.
21
+ //
22
+ // `file://` capture additionally depends on the user's per-extension
23
+ // "Allow access to file URLs" toggle:
24
+ // - `webNavigation` delivery is gated on host access, so with the
25
+ // toggle off the event is expected never to arrive.
26
+ // - If one somehow did, the preview couldn't read the file anyway
27
+ // and would show its fetch error.
28
+ // - Either way the feature is absent rather than broken, so nothing
29
+ // here tests for the toggle.
30
+ // - Not verified automatically: Chrome grants file access
31
+ // unconditionally to `--load-extension` builds, so the e2e suite
32
+ // only reaches the toggle-on case. See TODO.md.
33
+ //
34
+ // Redirecting via `chrome.tabs.update` (rather than a DNR `redirect`
35
+ // action) is deliberate: DNR redirects to an extension page require
36
+ // the page in `web_accessible_resources`, which hands our extension ID
37
+ // to every site for fingerprinting. An extension-initiated navigation
38
+ // has no such requirement.
39
+ //
40
+ // See [docs/install-capture.md].
41
+ import { dlog } from '../debug-log.js';
42
+ // Storage key for the user-facing "capture .user.js links" toggle
43
+ // (options page). Absent means enabled — capture is the default.
44
+ export const INSTALL_CAPTURE_KEY = 'smCaptureUserJsLinks';
45
+ // The viewer page, which renders a not-yet-installed script from
46
+ // `?preview=<url>` and offers Install / Update. Same page an installed
47
+ // script opens in — see [docs/script-viewer.md].
48
+ const PREVIEW_PAGE = 'userscript.html';
49
+ // The one spelling of the suffix every matcher here keys off.
50
+ const USER_JS_SUFFIX = '.user.js';
51
+ // Session-storage prefix for one-shot capture bypasses. The preview
52
+ // page writes one of these right before navigating to the raw URL so
53
+ // its own navigation isn't captured straight back into a preview.
54
+ // Session storage (not an in-memory set) because the writer is an
55
+ // extension page and the reader is the SW, which may restart in
56
+ // between.
57
+ const BYPASS_PREFIX = 'installBypass:';
58
+ // How long a bypass stays valid. Long enough to cover a slow
59
+ // navigation start, short enough that a later manual visit to the same
60
+ // URL is captured normally again.
61
+ const BYPASS_TTL_MS = 30_000;
62
+ // Schemes the capture acts on. `file:` is here so that opening a
63
+ // local `.user.js` offers the same install prompt a remote one does;
64
+ // without it a script on disk is the one way in with no prompt at
65
+ // all. The cost (a local file opened *to read* gets hijacked) is
66
+ // accepted and mitigated rather than avoided; see
67
+ // [docs/install-capture.md] "The cost, and what pays for it".
68
+ //
69
+ // Anything else (`chrome-extension:`, `ftp:`, `data:`, …) stays out:
70
+ // those aren't collection shapes `addCollection` can register.
71
+ const CAPTURED_PROTOCOLS = new Set(['http:', 'https:', 'file:']);
72
+ // Code-hosting sites that serve an HTML *page* — a file browser with
73
+ // syntax highlighting, line numbers, and navigation — at a URL whose
74
+ // path still ends in `.user.js`. Those pages are something the user
75
+ // asked to read and Chrome renders them correctly on its own, so the
76
+ // capture stays out of the way.
77
+ //
78
+ // Only the page-view path is listed. Each of these hosts serves the
79
+ // real bytes from a different path (noted per entry), and those stay
80
+ // captured — the exclusion is about the rendering, not the host.
81
+ //
82
+ // Not a completeness claim, and it doesn't need to be: an unlisted
83
+ // forge's blob view is still captured, but the preview then sees a
84
+ // markup response and continues to the page by itself. This list only
85
+ // saves the round trip for the hosts worth knowing in advance.
86
+ //
87
+ // Pasting any of these URLs into the collections page's Add field
88
+ // still installs the script — that path rewrites a blob URL to its raw
89
+ // form (`normalizeCollectionInput`).
90
+ const CODE_HOST_PAGE_VIEWS = [
91
+ // bytes at raw.githubusercontent.com, or /<o>/<r>/raw/…
92
+ { host: 'github.com', path: /^\/[^\/]+\/[^\/]+\/blob\// },
93
+ // bytes at /-/raw/…; the group segment count varies, so no anchor
94
+ { host: 'gitlab.com', path: /\/-\/blob\// },
95
+ // bytes at /<w>/<r>/raw/…
96
+ { host: 'bitbucket.org', path: /^\/[^\/]+\/[^\/]+\/src\// },
97
+ // Gitea: page at /src/branch/<b>/…, bytes at /raw/branch/<b>/…
98
+ { host: 'codeberg.org', path: /^\/[^\/]+\/[^\/]+\/src\// },
99
+ ];
100
+ function isCodeHostPageView(u) {
101
+ // `?raw=true` on a blob URL is GitHub's own "give me the bytes"
102
+ // switch — it 302s to raw.githubusercontent.com. Excluding it would
103
+ // lose the capture entirely, since `onBeforeNavigate` does not fire
104
+ // again for a server redirect.
105
+ if (u.searchParams.get('raw') === 'true' || u.searchParams.get('raw') === '1')
106
+ return false;
107
+ // `hostname` is already lowercased by the URL parser. `www.` is
108
+ // stripped so the same rule covers the redirecting alias.
109
+ const host = u.hostname.replace(/^www\./, '');
110
+ return CODE_HOST_PAGE_VIEWS.some((e) => e.host === host && e.path.test(u.pathname));
111
+ }
112
+ // True for URLs the capture should act on: an http(s) or file:// URL
113
+ // whose path ends in `.user.js`, excluding the forge page views above.
114
+ // Query strings and fragments are ignored (they live outside
115
+ // `pathname`), so `…/foo.user.js?v=3#top` matches.
116
+ //
117
+ // The suffix match is case-SENSITIVE, matching both the
118
+ // `webNavigation` pre-filter below and `isSingleFileInput` in
119
+ // directories.ts (which decides, post-install, whether the URL is a
120
+ // single script or a directory to scan). Chrome's own legacy check is
121
+ // case-insensitive, so the three ways SourceMonkey can see one URL
122
+ // have to agree or a `FOO.USER.JS` ends up captured but registered as
123
+ // a directory collection. Lowercase is the universal convention; a
124
+ // mixed-case suffix is simply not ours to handle.
125
+ export function isUserJsUrl(url) {
126
+ let u;
127
+ try {
128
+ u = new URL(url);
129
+ }
130
+ catch {
131
+ return false;
132
+ }
133
+ if (!CAPTURED_PROTOCOLS.has(u.protocol))
134
+ return false;
135
+ if (!u.pathname.endsWith(USER_JS_SUFFIX))
136
+ return false;
137
+ return !isCodeHostPageView(u);
138
+ }
139
+ // Marks a preview the capture opened, as opposed to one the user asked
140
+ // for from the collections page. Only the captured kind may bail out
141
+ // on its own (see the viewer's markup handling): a capture is us
142
+ // guessing that a URL was a script, so being wrong means getting out
143
+ // of the way. A preview the user explicitly asked for is a request we
144
+ // owe an answer to, and silently navigating them off SourceMonkey to
145
+ // some web page would be the wrong one.
146
+ //
147
+ // Nothing on the web can set this, and it has to stay that way: a page
148
+ // that could forge it could make the viewer navigate the tab and plant
149
+ // a capture bypass. `deep-link.ts` strips it for that reason. The
150
+ // whole flow — who adds it, who strips it, who reads it — is written
151
+ // up in [docs/install-capture.md], "The `captured=1` marker".
152
+ export const CAPTURED_PARAM = 'captured';
153
+ // Build the preview URL for a captured script URL. The script URL is
154
+ // carried as a normal query param — the viewer is the only consumer and
155
+ // it validates the value before fetching anything.
156
+ export function buildPreviewPageUrl(scriptUrl) {
157
+ const qs = new URLSearchParams({ preview: scriptUrl, [CAPTURED_PARAM]: '1' });
158
+ return `${chrome.runtime.getURL(PREVIEW_PAGE)}?${qs.toString()}`;
159
+ }
160
+ function bypassKey(url) {
161
+ return BYPASS_PREFIX + url;
162
+ }
163
+ // Record a one-shot "don't capture this URL" marker. Called from the
164
+ // preview page's View collection action.
165
+ export async function markInstallBypass(url) {
166
+ await chrome.storage.session.set({ [bypassKey(url)]: Date.now() + BYPASS_TTL_MS });
167
+ }
168
+ // Read-and-clear a bypass marker. Returns true when the caller should
169
+ // let the navigation through untouched.
170
+ export async function consumeInstallBypass(url) {
171
+ const key = bypassKey(url);
172
+ const got = await chrome.storage.session.get(key);
173
+ const expiresAt = got[key];
174
+ if (typeof expiresAt !== 'number')
175
+ return false;
176
+ await chrome.storage.session.remove(key);
177
+ return expiresAt > Date.now();
178
+ }
179
+ // In-SW mirror of the capture toggle. Read once at startup and kept
180
+ // current by the storage listener, so the navigation handler never has
181
+ // to await storage on the critical path — every millisecond there is a
182
+ // millisecond the response can use to win the race.
183
+ //
184
+ // It starts at the default (on), which is a lie for a user who turned
185
+ // capture off: an MV3 service worker is woken *by* the navigation
186
+ // event, so the first navigation after every wake can arrive before
187
+ // the initial read lands. `captureFlagReady` is awaited on the async
188
+ // side of the handler — off the sync fast path — so that navigation
189
+ // gets the real answer too.
190
+ let captureEnabled = true;
191
+ let captureFlagReady = Promise.resolve();
192
+ // Bumped by every write. The initial read applies its result only if
193
+ // nothing newer landed while it was in flight — otherwise a toggle made
194
+ // during startup would be clobbered by the stale value, leaving the
195
+ // mirror wrong until the next toggle.
196
+ let captureFlagGeneration = 0;
197
+ // The live mirror, not a settled answer: it reads the optimistic
198
+ // default (on) until the startup read lands, so only a poll for
199
+ // `false` means anything. Exposed for the e2e suite, where a test that
200
+ // turns capture off needs to know the SW has seen the change before it
201
+ // navigates — the storage write resolving is not the same as the
202
+ // `onChanged` listener having run.
203
+ export function isCaptureEnabled() {
204
+ return captureEnabled;
205
+ }
206
+ async function loadCaptureFlag() {
207
+ const generation = captureFlagGeneration;
208
+ try {
209
+ const got = await chrome.storage.local.get(INSTALL_CAPTURE_KEY);
210
+ if (generation !== captureFlagGeneration)
211
+ return;
212
+ captureEnabled = got[INSTALL_CAPTURE_KEY] !== false;
213
+ }
214
+ catch (err) {
215
+ // Leave the mirror at its default rather than taking the SW's
216
+ // startup down; the next toggle re-syncs everything.
217
+ console.info('[SourceMonkey] could not read the .user.js capture setting', err);
218
+ }
219
+ }
220
+ // Wire the capture. Safe to call when webNavigation is unavailable —
221
+ // the feature just stays off.
222
+ export function installUserJsCapture() {
223
+ const wn = chrome.webNavigation;
224
+ if (!wn) {
225
+ console.info('[SourceMonkey] chrome.webNavigation not available; .user.js capture disabled');
226
+ return;
227
+ }
228
+ captureFlagReady = loadCaptureFlag();
229
+ chrome.storage.onChanged.addListener((changes, areaName) => {
230
+ if (areaName !== 'local')
231
+ return;
232
+ const change = changes[INSTALL_CAPTURE_KEY];
233
+ if (!change)
234
+ return;
235
+ captureFlagGeneration++;
236
+ // The bump above makes an in-flight initial read discard its
237
+ // result, so the toggle can't be clobbered by a stale value. The
238
+ // flag itself lands synchronously; `captureFlagReady` is reset so
239
+ // that a LATER navigation isn't parked behind the superseded
240
+ // startup read.
241
+ captureEnabled = change.newValue !== false;
242
+ captureFlagReady = Promise.resolve();
243
+ });
244
+ wn.onBeforeNavigate.addListener((d) => {
245
+ if (d.frameId !== 0)
246
+ return;
247
+ if (!captureEnabled)
248
+ return;
249
+ if (!isUserJsUrl(d.url))
250
+ return;
251
+ void captureNavigation(d.tabId, d.url);
252
+ },
253
+ // Cheap pre-filter so the listener isn't woken for every
254
+ // navigation. `pathSuffix` matches the path only, so query strings
255
+ // don't defeat it, and it is case-sensitive — the same rule
256
+ // `isUserJsUrl` applies, so the two agree on `FOO.USER.JS` (not
257
+ // captured). No scheme condition: the filter is scheme-agnostic,
258
+ // so `file://` navigations arrive here too and `isUserJsUrl` is
259
+ // the only thing deciding which schemes are in scope.
260
+ { url: [{ pathSuffix: '.user.js' }] });
261
+ }
262
+ async function captureNavigation(tabId, url) {
263
+ // Re-check against the settled flag: the sync check upstream may have
264
+ // run against the startup default. Cheap — resolved for every
265
+ // navigation but the first after an SW wake.
266
+ await captureFlagReady;
267
+ if (!captureEnabled)
268
+ return;
269
+ if (await consumeInstallBypass(url)) {
270
+ dlog(`[SourceMonkey] .user.js capture bypassed for ${url}`);
271
+ return;
272
+ }
273
+ console.log(`[SourceMonkey] capturing .user.js navigation: ${url}`);
274
+ // The tab may have closed (or navigated on) between the event firing
275
+ // and this update landing; ignore the resulting rejection.
276
+ await chrome.tabs.update(tabId, { url: buildPreviewPageUrl(url) }).catch(() => { });
277
+ }
@@ -0,0 +1,233 @@
1
+ // Line diff for the compare page: which lines two texts share, which
2
+ // each has alone, and how to fold the shared runs away into hunks.
3
+ //
4
+ // Pure — no DOM, no store — so the compare page can be tested apart
5
+ // from the pages that show its output. A line is the unit: a changed
6
+ // character marks its whole line, and nothing here knows what a
7
+ // function or a header tag is. Grouping changes into hunks with a few
8
+ // lines of context on either side is what makes the output read as
9
+ // "just the parts that changed", and it is a separate pass over the
10
+ // line ops rather than anything the diff itself does.
11
+ // Split into lines without inventing a trailing empty one: a text that
12
+ // ends in a newline has as many lines as newlines, not one more.
13
+ export function splitLines(text) {
14
+ if (text === '')
15
+ return [];
16
+ const lines = text.split('\n');
17
+ if (lines[lines.length - 1] === '')
18
+ lines.pop();
19
+ return lines;
20
+ }
21
+ // Past this many edits the search is abandoned and the two texts are
22
+ // shown as a whole replacement. Myers' algorithm costs O((N+M)·D) time
23
+ // and its backtrack trace grows as D², so a pair of large, unrelated
24
+ // files (one minified library against a different one) would otherwise
25
+ // spend seconds and tens of megabytes to print a diff no one can read
26
+ // anyway. A few thousand edits is far past what anyone reads line by
27
+ // line.
28
+ const MAX_EDITS = 3000;
29
+ // Myers' O(ND) shortest-edit-script diff over two line arrays, the same
30
+ // algorithm `diff` and git use. The common prefix and suffix are peeled
31
+ // off first: they are the bulk of any real comparison, and they cost
32
+ // the search nothing that way.
33
+ export function diffLines(a, b) {
34
+ let prefix = 0;
35
+ while (prefix < a.length && prefix < b.length && a[prefix] === b[prefix])
36
+ prefix++;
37
+ let suffix = 0;
38
+ while (suffix < a.length - prefix && suffix < b.length - prefix
39
+ && a[a.length - 1 - suffix] === b[b.length - 1 - suffix])
40
+ suffix++;
41
+ const ops = [];
42
+ for (let i = 0; i < prefix; i++) {
43
+ ops.push({ kind: 'same', text: a[i], aLine: i + 1, bLine: i + 1 });
44
+ }
45
+ const midA = a.slice(prefix, a.length - suffix);
46
+ const midB = b.slice(prefix, b.length - suffix);
47
+ const middle = myers(midA, midB) ?? replaceAll(midA, midB);
48
+ for (const op of middle) {
49
+ if (op.aLine !== undefined)
50
+ op.aLine += prefix;
51
+ if (op.bLine !== undefined)
52
+ op.bLine += prefix;
53
+ ops.push(op);
54
+ }
55
+ for (let i = 0; i < suffix; i++) {
56
+ const ai = a.length - suffix + i;
57
+ const bi = b.length - suffix + i;
58
+ ops.push({ kind: 'same', text: a[ai], aLine: ai + 1, bLine: bi + 1 });
59
+ }
60
+ return ops;
61
+ }
62
+ // The fallback past `MAX_EDITS`: everything in A removed, everything in
63
+ // B added. Honest, if unhelpful — the counts are right, and the reader
64
+ // sees both texts in full.
65
+ function replaceAll(a, b) {
66
+ const ops = [];
67
+ a.forEach((text, i) => ops.push({ kind: 'del', text, aLine: i + 1 }));
68
+ b.forEach((text, i) => ops.push({ kind: 'add', text, bLine: i + 1 }));
69
+ return ops;
70
+ }
71
+ // The forward search from "An O(ND) Difference Algorithm and Its
72
+ // Variations" (Myers, 1986). Line numbers in the result are 1-based
73
+ // within the arrays given. Returns null when the edit count passes
74
+ // `MAX_EDITS`.
75
+ //
76
+ // `v[k]` is the furthest x reached on diagonal k (where x − y = k)
77
+ // with the current number of edits. Diagonals run from −d to d after d
78
+ // edits, so each step's array is sized to just that range (plus the
79
+ // neighbour on either side it reads) rather than to the worst case,
80
+ // and the trace kept for the backtrack costs D² rather than D·(N+M).
81
+ // The array for step d is indexed as `v[k + d + 1]`.
82
+ function myers(a, b) {
83
+ const n = a.length;
84
+ const m = b.length;
85
+ if (n === 0 && m === 0)
86
+ return [];
87
+ const max = Math.min(n + m, MAX_EDITS);
88
+ // Before step 0: k ∈ [−1, 1], and the seed on k = 1 is what makes
89
+ // the first move a step down onto k = 0 at x = 0.
90
+ let v = new Int32Array(3).fill(-1);
91
+ v[2] = 0;
92
+ const trace = [];
93
+ for (let d = 0; d <= max; d++) {
94
+ trace.push(v);
95
+ const offset = d + 1;
96
+ // Step d + 1 reads one diagonal further out on each side.
97
+ const next = new Int32Array(v.length + 2).fill(-1);
98
+ next.set(v, 1);
99
+ for (let k = -d; k <= d; k += 2) {
100
+ // Step down (an added line) or right (a removed line), whichever
101
+ // reached further — preferring right on a tie, the conventional
102
+ // choice that lists a deletion before the insertion beside it.
103
+ let x;
104
+ if (k === -d || (k !== d && v[offset + k - 1] < v[offset + k + 1])) {
105
+ x = v[offset + k + 1];
106
+ }
107
+ else {
108
+ x = v[offset + k - 1] + 1;
109
+ }
110
+ let y = x - k;
111
+ // Then along the diagonal for as long as the lines agree.
112
+ while (x < n && y < m && a[x] === b[y]) {
113
+ x++;
114
+ y++;
115
+ }
116
+ next[offset + 1 + k] = x;
117
+ if (x >= n && y >= m)
118
+ return backtrack(a, b, trace, d);
119
+ }
120
+ v = next;
121
+ }
122
+ return null;
123
+ }
124
+ // Walk the trace back from the end to recover the edit script, then
125
+ // reverse it into reading order. `trace[d]` is the state before step
126
+ // d, which is what decided where that step went.
127
+ function backtrack(a, b, trace, edits) {
128
+ const ops = [];
129
+ let x = a.length;
130
+ let y = b.length;
131
+ for (let d = edits; d > 0; d--) {
132
+ const v = trace[d];
133
+ const offset = d + 1;
134
+ const k = x - y;
135
+ const prevK = (k === -d || (k !== d && v[offset + k - 1] < v[offset + k + 1]))
136
+ ? k + 1
137
+ : k - 1;
138
+ const prevX = v[offset + prevK];
139
+ const prevY = prevX - prevK;
140
+ // The diagonal run that followed the edit, walked backwards.
141
+ while (x > prevX && y > prevY) {
142
+ x--;
143
+ y--;
144
+ ops.push({ kind: 'same', text: a[x], aLine: x + 1, bLine: y + 1 });
145
+ }
146
+ if (x === prevX) {
147
+ y--;
148
+ ops.push({ kind: 'add', text: b[y], bLine: y + 1 });
149
+ }
150
+ else {
151
+ x--;
152
+ ops.push({ kind: 'del', text: a[x], aLine: x + 1 });
153
+ }
154
+ }
155
+ // Whatever is left is the diagonal run before the first edit.
156
+ while (x > 0 && y > 0) {
157
+ x--;
158
+ y--;
159
+ ops.push({ kind: 'same', text: a[x], aLine: x + 1, bLine: y + 1 });
160
+ }
161
+ return ops.reverse();
162
+ }
163
+ // Group the changed lines into hunks, each padded with `context` lines
164
+ // of unchanged text on either side; two changes closer together than
165
+ // twice that share a hunk. An op list with no changes yields no hunks.
166
+ export function toHunks(ops, context = 3) {
167
+ const hunks = [];
168
+ let i = 0;
169
+ while (i < ops.length) {
170
+ if (ops[i].kind === 'same') {
171
+ i++;
172
+ continue;
173
+ }
174
+ // The hunk starts `context` lines before this change…
175
+ const start = Math.max(0, i - context);
176
+ // …and runs until a stretch of more than 2·context unchanged lines
177
+ // separates it from the next change, or the ops run out.
178
+ let end = i;
179
+ let sameRun = 0;
180
+ let j = i;
181
+ while (j < ops.length) {
182
+ if (ops[j].kind === 'same') {
183
+ sameRun++;
184
+ if (sameRun > 2 * context)
185
+ break;
186
+ }
187
+ else {
188
+ sameRun = 0;
189
+ end = j;
190
+ }
191
+ j++;
192
+ }
193
+ const stop = Math.min(ops.length, end + 1 + context);
194
+ hunks.push(makeHunk(ops.slice(start, stop)));
195
+ i = stop;
196
+ }
197
+ return hunks;
198
+ }
199
+ function makeHunk(ops) {
200
+ let aStart = 0;
201
+ let bStart = 0;
202
+ let aCount = 0;
203
+ let bCount = 0;
204
+ for (const op of ops) {
205
+ if (op.aLine !== undefined) {
206
+ if (!aStart)
207
+ aStart = op.aLine;
208
+ aCount++;
209
+ }
210
+ if (op.bLine !== undefined) {
211
+ if (!bStart)
212
+ bStart = op.bLine;
213
+ bCount++;
214
+ }
215
+ }
216
+ // A hunk with nothing on one side (a pure insertion at the top of an
217
+ // empty file) starts where that side's next line would be, which is
218
+ // what `@@ -0,0 +1,3 @@` means.
219
+ return { ops, aStart, aCount, bStart, bCount };
220
+ }
221
+ // How many lines each side has alone — the `−4 +9` a diff is summed
222
+ // up as.
223
+ export function countChanges(ops) {
224
+ let removed = 0;
225
+ let added = 0;
226
+ for (const op of ops) {
227
+ if (op.kind === 'del')
228
+ removed++;
229
+ else if (op.kind === 'add')
230
+ added++;
231
+ }
232
+ return { removed, added };
233
+ }