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,141 @@
1
+ // SW-side `GM_notification` dispatcher.
2
+ //
3
+ // chrome.notifications wrapping. The page sends a details object
4
+ // describing the notification; we call chrome.notifications.create
5
+ // and remember the (chromeNotificationId ↔ originator + requestId)
6
+ // mapping so onClicked / onClosed can be routed back.
7
+ //
8
+ // chrome.notifications has its own assigned-id mechanism (we can
9
+ // pass an explicit notificationId or "" to have Chrome assign one).
10
+ // We always let Chrome assign — keeps our requestId space cleanly
11
+ // page-side and avoids collisions between userscripts.
12
+ import { dropEntriesForOriginator } from './gm-originator-map.js';
13
+ import { hasAllowedScheme, WEB_SCHEMES } from './gm-url-scheme.js';
14
+ const byChromeId = new Map();
15
+ // One string for the two guards that check the same thing: this one and
16
+ // `handleNotificationShow` in `background.ts`, which rejects before it
17
+ // ever calls in here. Names the browser permission the reader would go
18
+ // looking for rather than the API object we happened to test.
19
+ export const NOTIFICATIONS_UNAVAILABLE = 'this browser will not show notifications to SourceMonkey; check the extension\'s notification permission';
20
+ // Page-supplied notification image. Chrome fetches `iconUrl` from the
21
+ // privileged extension context, so we only honor http(s) / data: URLs.
22
+ // A `file://` or `chrome-extension://` image would let a script probe
23
+ // local files / packaged resources through the extension; reject those
24
+ // and fall back to the default icon.
25
+ function isAllowedIconUrl(image) {
26
+ return hasAllowedScheme(image, [...WEB_SCHEMES, 'data:']);
27
+ }
28
+ // Pick the icon URL chrome.notifications wants. The user-supplied
29
+ // image is preferred when it's an allowed scheme; otherwise we fall
30
+ // back to the extension's own icon so chrome.notifications doesn't
31
+ // reject the create call (iconUrl is required by the API).
32
+ function pickIconUrl(image) {
33
+ if (image && isAllowedIconUrl(image))
34
+ return image;
35
+ try {
36
+ return chrome.runtime.getURL('icons/icon-128.png');
37
+ }
38
+ catch {
39
+ return '';
40
+ }
41
+ }
42
+ export async function showGmNotification(payload, originatorTabId) {
43
+ // Defense in depth — handleNotificationShow already guards this,
44
+ // but a future direct caller (test surface, retry path) shouldn't
45
+ // crash if the API is missing.
46
+ if (!chrome.notifications) {
47
+ return { ok: false, error: NOTIFICATIONS_UNAVAILABLE };
48
+ }
49
+ const d = payload.details;
50
+ const options = {
51
+ type: 'basic',
52
+ iconUrl: pickIconUrl(d.image),
53
+ title: d.title || 'SourceMonkey',
54
+ message: d.text,
55
+ silent: d.silent,
56
+ };
57
+ try {
58
+ // The installed @types/chrome declares the callback form only,
59
+ // but chrome.notifications.create is Promise-returning in MV3.
60
+ // Wrap to avoid the type mismatch and stay forward-compatible.
61
+ const chromeId = await new Promise((res, rej) => {
62
+ try {
63
+ chrome.notifications.create('', options, (id) => {
64
+ if (chrome.runtime.lastError) {
65
+ rej(new Error(chrome.runtime.lastError.message ?? 'notifications.create failed'));
66
+ return;
67
+ }
68
+ res(id);
69
+ });
70
+ }
71
+ catch (e) {
72
+ rej(e);
73
+ }
74
+ });
75
+ const entry = {
76
+ originatorTabId,
77
+ requestId: payload.requestId,
78
+ scriptId: payload.scriptId,
79
+ timeoutTimer: null,
80
+ };
81
+ if (d.timeout > 0) {
82
+ entry.timeoutTimer = setTimeout(() => {
83
+ // chrome.notifications.clear fires its callback synchronously
84
+ // for our purposes; the onClosed listener will then dispatch
85
+ // the page-side ondone via the normal path.
86
+ try {
87
+ chrome.notifications.clear(chromeId);
88
+ }
89
+ catch { /* gone */ }
90
+ }, d.timeout);
91
+ }
92
+ byChromeId.set(chromeId, entry);
93
+ return { ok: true, notificationId: chromeId };
94
+ }
95
+ catch (err) {
96
+ return {
97
+ ok: false,
98
+ error: err instanceof Error ? err.message : String(err),
99
+ };
100
+ }
101
+ }
102
+ // Called by the SW's chrome.notifications.onClicked listener.
103
+ export function takeForClicked(chromeId) {
104
+ const entry = byChromeId.get(chromeId);
105
+ if (!entry)
106
+ return null;
107
+ // Don't delete the entry on click — TM's `onclick` is NOT terminal.
108
+ // The user may click then the notification auto-closes (or stays
109
+ // open until they dismiss); we still want to deliver ondone later.
110
+ return {
111
+ originatorTabId: entry.originatorTabId,
112
+ requestId: entry.requestId,
113
+ scriptId: entry.scriptId,
114
+ };
115
+ }
116
+ // Called by the SW's chrome.notifications.onClosed listener.
117
+ // Terminal — we free the entry.
118
+ export function takeForClosed(chromeId) {
119
+ const entry = byChromeId.get(chromeId);
120
+ if (!entry)
121
+ return null;
122
+ if (entry.timeoutTimer != null)
123
+ clearTimeout(entry.timeoutTimer);
124
+ byChromeId.delete(chromeId);
125
+ return {
126
+ originatorTabId: entry.originatorTabId,
127
+ requestId: entry.requestId,
128
+ scriptId: entry.scriptId,
129
+ };
130
+ }
131
+ // Drop every entry whose originator tab just closed. Best-effort
132
+ // matches GM_openInTab's dropOriginator. The chrome notifications
133
+ // themselves stay on screen — chrome.notifications survive tab close
134
+ // (they're per-extension, not per-tab) — but with the originator
135
+ // gone we have nobody to route the eventual events to.
136
+ export function dropOriginator(originatorTabId) {
137
+ return dropEntriesForOriginator(byChromeId, originatorTabId, (entry) => {
138
+ if (entry.timeoutTimer != null)
139
+ clearTimeout(entry.timeoutTimer);
140
+ });
141
+ }
@@ -0,0 +1,23 @@
1
+ // Shared shed-on-tab-close helper for the per-request maps in
2
+ // `gm-tab.ts` (GM_openInTab), `gm-notification.ts` (GM_notification),
3
+ // and `gm-download.ts` (GM_download).
4
+ //
5
+ // Each keeps an in-memory `Map<key, { originatorTabId, … }>` and, when
6
+ // a tab that originated requests is itself closed, drops every entry
7
+ // that points at it (the opened tabs / on-screen notifications /
8
+ // running downloads stay; we just have nowhere left to route their
9
+ // eventual events). The iterate → match `originatorTabId` → per-entry
10
+ // cleanup → delete → collect-key loop is identical; only the per-entry
11
+ // cleanup differs (a secondary index vs. timers). Factored here so the
12
+ // dispatchers can't drift.
13
+ export function dropEntriesForOriginator(map, originatorTabId, cleanup) {
14
+ const dropped = [];
15
+ for (const [key, entry] of map) {
16
+ if (entry.originatorTabId !== originatorTabId)
17
+ continue;
18
+ cleanup(entry);
19
+ map.delete(key);
20
+ dropped.push(key);
21
+ }
22
+ return dropped;
23
+ }
@@ -0,0 +1,121 @@
1
+ // SW-side store for `GM_getTab` / `GM_saveTab` / `GM_getTabs`: one
2
+ // plain object per (tab, script), kept while the tab is open.
3
+ //
4
+ // Behavior, as observed in TM:
5
+ // - `GM_getTab` gives this script's object for the sender's tab, `{}`
6
+ // when it has saved nothing there. The call itself counts as
7
+ // presence: the tab then shows in `GM_getTabs` with `{}`.
8
+ // - `GM_saveTab(obj)` replaces the object with a shallow copy. A null
9
+ // or undefined `obj` saves nothing.
10
+ // - `GM_getTabs` gives this script's object for every tab that has
11
+ // one, keyed by tab id.
12
+ // - Frames share their tab's object; the key is the tab, not the
13
+ // document.
14
+ // - Closing the tab drops its objects (`dropTabData`, from
15
+ // background.ts's `tabs.onRemoved`).
16
+ //
17
+ // Where it lives: in memory, mirrored to `chrome.storage.session` as one
18
+ // `gmtab:<tabId>` row per tab ({ [scriptId]: object }). The mirror is
19
+ // what keeps the data across the service worker being stopped while
20
+ // idle, which would otherwise lose it within a minute of quiet. Session
21
+ // storage also clears with the browser, the lifetime the API promises.
22
+ // The rows are first read on the first call after a worker start.
23
+ //
24
+ // Wire protocol (page → SW): `{ type: SM_TAB_DATA_MSG, scriptId,
25
+ // action: 'get' | 'save' | 'list', data? }` → `{ ok: true, value }` or
26
+ // `{ ok: false, error }`. Values cross as JSON, so they hold what JSON
27
+ // can.
28
+ export const SM_TAB_DATA_MSG = 'sm:gm-tab-data';
29
+ const KEY_PREFIX = 'gmtab:';
30
+ let tabs = null;
31
+ let loading = null;
32
+ function rowKey(tabId) {
33
+ return `${KEY_PREFIX}${tabId}`;
34
+ }
35
+ function load() {
36
+ if (tabs)
37
+ return Promise.resolve(tabs);
38
+ if (!loading) {
39
+ loading = (async () => {
40
+ const all = await chrome.storage.session.get(null);
41
+ const m = new Map();
42
+ for (const [key, value] of Object.entries(all)) {
43
+ if (!key.startsWith(KEY_PREFIX))
44
+ continue;
45
+ const tabId = Number(key.slice(KEY_PREFIX.length));
46
+ if (!Number.isInteger(tabId) || !value || typeof value !== 'object')
47
+ continue;
48
+ m.set(tabId, value);
49
+ }
50
+ tabs = m;
51
+ return m;
52
+ })().finally(() => { loading = null; });
53
+ }
54
+ return loading;
55
+ }
56
+ // Write one tab's row (or remove it once empty). On failure this
57
+ // script's entry is put back as it was, so memory and the row don't
58
+ // disagree; other scripts' entries, which may have changed since, stay.
59
+ async function persist(m, tabId, scriptId, prev) {
60
+ const next = m.get(tabId);
61
+ try {
62
+ if (next && Object.keys(next).length > 0) {
63
+ await chrome.storage.session.set({ [rowKey(tabId)]: next });
64
+ }
65
+ else {
66
+ await chrome.storage.session.remove(rowKey(tabId));
67
+ }
68
+ }
69
+ catch (err) {
70
+ const { [scriptId]: _failed, ...rest } = m.get(tabId) ?? {};
71
+ const restored = prev ? { ...rest, [scriptId]: prev } : rest;
72
+ if (Object.keys(restored).length > 0)
73
+ m.set(tabId, restored);
74
+ else
75
+ m.delete(tabId);
76
+ throw err;
77
+ }
78
+ }
79
+ export async function getTabData(tabId, scriptId) {
80
+ const m = await load();
81
+ const prev = m.get(tabId);
82
+ const existing = prev?.[scriptId];
83
+ if (existing)
84
+ return existing;
85
+ m.set(tabId, { ...prev, [scriptId]: {} });
86
+ await persist(m, tabId, scriptId, undefined);
87
+ return {};
88
+ }
89
+ export async function saveTabData(tabId, scriptId, data) {
90
+ if (data == null)
91
+ return;
92
+ if (typeof data !== 'object')
93
+ throw new Error('The tab object must be an object');
94
+ const m = await load();
95
+ const byScript = m.get(tabId);
96
+ const prev = byScript?.[scriptId];
97
+ m.set(tabId, { ...byScript, [scriptId]: { ...data } });
98
+ await persist(m, tabId, scriptId, prev);
99
+ }
100
+ export async function listTabData(scriptId) {
101
+ const m = await load();
102
+ const out = {};
103
+ for (const [tabId, byScript] of m) {
104
+ const obj = byScript[scriptId];
105
+ if (obj)
106
+ out[tabId] = obj;
107
+ }
108
+ return out;
109
+ }
110
+ // The tab closed. Waits out a first load already under way, which may
111
+ // have read the row before it goes.
112
+ export async function dropTabData(tabId) {
113
+ const m = tabs ?? (loading ? await loading : null);
114
+ m?.delete(tabId);
115
+ await chrome.storage.session.remove(rowKey(tabId));
116
+ }
117
+ // For the test surface's `resetAllState`, which clears session storage:
118
+ // the memory copy came from it.
119
+ export function forgetTabDataForTests() {
120
+ tabs = null;
121
+ }
@@ -0,0 +1,160 @@
1
+ // SW-side `GM_openInTab` dispatcher.
2
+ //
3
+ // Owns the (requestId ↔ chrome tab id ↔ originator tab id) mapping
4
+ // so we can route close events back to the page that opened the tab.
5
+ // Per-call state is small; we keep two maps for O(1) lookup either
6
+ // way:
7
+ //
8
+ // requestState: requestId -> { tabId?, originatorTabId, scriptId }
9
+ // tabIdToRequest: chromeTabId -> requestId
10
+ //
11
+ // The maps live in memory only. On SW restart the mapping is lost —
12
+ // userscripts that hold a control handle from before the restart
13
+ // stop receiving close notifications. That's the same boat as XHR
14
+ // requests in flight; acceptable for v1.
15
+ import { dropEntriesForOriginator } from './gm-originator-map.js';
16
+ import { hasAllowedScheme, WEB_SCHEMES } from './gm-url-scheme.js';
17
+ const requestState = new Map();
18
+ const tabIdToRequest = new Map();
19
+ // Only http(s) targets are allowed. The URL comes straight from
20
+ // page-supplied message payload, so without this gate a script could
21
+ // ask the privileged SW to open `file://`, `chrome://`, or
22
+ // `chrome-extension://` tabs (local-file disclosure, settings pages,
23
+ // other extensions' pages). TM/VM's GM_openInTab is web-oriented;
24
+ // restricting to http/https matches the realistic use and closes the
25
+ // scheme-smuggling hole.
26
+ export function isAllowedTabUrl(url) {
27
+ return hasAllowedScheme(url, WEB_SCHEMES);
28
+ }
29
+ // The refusal a script sees for a URL `isAllowedTabUrl` rejects.
30
+ export function tabUrlRefusedMessage(url) {
31
+ return `GM_openInTab: refusing non-http(s) URL ${url}`;
32
+ }
33
+ // Open a new tab on behalf of a script. Returns the assigned chrome
34
+ // tab id (or an error) and stores the originator mapping so close
35
+ // events can be routed back.
36
+ export async function openGmTab(payload, originatorTabId,
37
+ // Run on the new tab before it loads the URL: it opens blank, this
38
+ // runs, then the URL loads. For a tab sm-dev controls, whose claim
39
+ // has to be on the new tab before its scripts ask (sm-dev-link.ts).
40
+ beforeLoad) {
41
+ if (!isAllowedTabUrl(payload.url)) {
42
+ return { ok: false, error: tabUrlRefusedMessage(payload.url) };
43
+ }
44
+ // Stash the entry first so a close event arriving between
45
+ // chrome.tabs.create resolving and the page receiving the reply
46
+ // can still find the request. The originator can't have called
47
+ // close() yet because it doesn't have the tabId yet, but the user
48
+ // could theoretically close the new tab manually that fast.
49
+ requestState.set(payload.requestId, {
50
+ originatorTabId,
51
+ scriptId: payload.scriptId,
52
+ tabId: null,
53
+ closeRequested: false,
54
+ });
55
+ try {
56
+ const created = await chrome.tabs.create({
57
+ url: beforeLoad ? 'about:blank' : payload.url,
58
+ active: payload.active,
59
+ // Bias positioning: when `insert` is true, hint Chrome to place
60
+ // the new tab next to the originator.
61
+ ...(payload.insert ? { openerTabId: originatorTabId } : {}),
62
+ });
63
+ if (created.id == null) {
64
+ requestState.delete(payload.requestId);
65
+ return { ok: false, error: 'internal error: the tab opened but Chrome reported no id for it' };
66
+ }
67
+ const entry = requestState.get(payload.requestId);
68
+ if (!entry) {
69
+ // Request was already aborted; close the tab we just opened so
70
+ // we don't leak it.
71
+ try {
72
+ await chrome.tabs.remove(created.id);
73
+ }
74
+ catch { /* gone */ }
75
+ return {
76
+ ok: false,
77
+ error: 'internal error: the tab request was dropped before the tab opened',
78
+ };
79
+ }
80
+ entry.tabId = created.id;
81
+ tabIdToRequest.set(created.id, payload.requestId);
82
+ if (beforeLoad) {
83
+ try {
84
+ await beforeLoad(created.id);
85
+ // Closed meanwhile, by the script or the person: nothing to load.
86
+ if (!entry.closeRequested)
87
+ await chrome.tabs.update(created.id, { url: payload.url });
88
+ }
89
+ catch (err) {
90
+ // The tab opened, so the script is told so. A tab gone before
91
+ // its URL loaded is reported closed by the onRemoved listener.
92
+ console.info('[SourceMonkey] GM_openInTab: could not load the URL into the new tab:', err);
93
+ }
94
+ }
95
+ if (entry.closeRequested) {
96
+ // The page called close() before create resolved. Close
97
+ // immediately — the onRemoved listener will then clear the
98
+ // mapping and route the close event back to the originator.
99
+ try {
100
+ await chrome.tabs.remove(created.id);
101
+ }
102
+ catch { /* gone */ }
103
+ }
104
+ return { ok: true, tabId: created.id };
105
+ }
106
+ catch (err) {
107
+ requestState.delete(payload.requestId);
108
+ return {
109
+ ok: false,
110
+ error: err instanceof Error ? err.message : String(err),
111
+ };
112
+ }
113
+ }
114
+ // Close a tab by requestId. Idempotent — close on a tab that's
115
+ // already gone is a no-op.
116
+ export async function closeGmTab(requestId) {
117
+ const entry = requestState.get(requestId);
118
+ if (!entry)
119
+ return;
120
+ if (entry.tabId == null) {
121
+ // chrome.tabs.create hasn't resolved yet. Flag the entry; the
122
+ // openGmTab path checks closeRequested after create resolves and
123
+ // closes the just-opened tab. Avoids a "open-then-leak" when
124
+ // close() fires faster than create.
125
+ entry.closeRequested = true;
126
+ return;
127
+ }
128
+ const tabId = entry.tabId;
129
+ try {
130
+ await chrome.tabs.remove(tabId);
131
+ }
132
+ catch { /* tab already gone; the onRemoved listener handles cleanup */ }
133
+ }
134
+ // Called by the SW's chrome.tabs.onRemoved listener. Returns the
135
+ // originator that needs notifying (or null if this wasn't a
136
+ // GM_openInTab-created tab). The caller is responsible for the
137
+ // actual sendMessage so this module stays independent of the broader
138
+ // message-routing surface.
139
+ export function takeRequestForClosedTab(tabId) {
140
+ const requestId = tabIdToRequest.get(tabId);
141
+ if (!requestId)
142
+ return null;
143
+ tabIdToRequest.delete(tabId);
144
+ const entry = requestState.get(requestId);
145
+ if (!entry)
146
+ return null;
147
+ requestState.delete(requestId);
148
+ return { originatorTabId: entry.originatorTabId, requestId, scriptId: entry.scriptId };
149
+ }
150
+ // Drop every entry pointing at this originator tab — used when a tab
151
+ // containing a userscript that opened other tabs is itself closed.
152
+ // The opened tabs stay open (matches TM/VM), but we can't route close
153
+ // notifications anywhere useful, so we shed the mapping. Returns the
154
+ // dropped requestIds for logging.
155
+ export function dropOriginator(originatorTabId) {
156
+ return dropEntriesForOriginator(requestState, originatorTabId, (entry) => {
157
+ if (entry.tabId != null)
158
+ tabIdToRequest.delete(entry.tabId);
159
+ });
160
+ }
@@ -0,0 +1,111 @@
1
+ // SW-side driver for `@grant window.onurlchange`.
2
+ //
3
+ // Why the service worker is involved at all: a USER_SCRIPT-world
4
+ // script does NOT share `window.history` with the page. Patching
5
+ // `history.pushState` from the isolated world produces a patched
6
+ // function only isolated-world code ever calls — the page's own SPA
7
+ // router keeps calling the real one, unobserved. The isolated world
8
+ // does receive `popstate` / `hashchange`, but those cover only part of
9
+ // the surface and overlap the events below, so this module is the
10
+ // single source rather than one of two.
11
+ //
12
+ // `chrome.webNavigation` reports the whole surface:
13
+ //
14
+ // - `onHistoryStateUpdated` — pushState / replaceState and
15
+ // same-document traversals
16
+ // - `onReferenceFragmentUpdated` — fragment-only changes
17
+ //
18
+ // These two are *also* what Tampermonkey feeds its own `urlchange`
19
+ // from (verified in TM 5.5.0's `background.js`), which is why our
20
+ // firing rules match theirs for free: nothing on initial load,
21
+ // nothing on a cross-document navigation, one event per reported
22
+ // history update — including a same-URL `replaceState`, which TM does
23
+ // not suppress. The `webNavigation` permission is already in the
24
+ // manifest and `background.ts` already subscribes to
25
+ // `onHistoryStateUpdated` for tab status, so this costs no new
26
+ // privilege.
27
+ //
28
+ // The alternative — registering a MAIN-world helper that patches the
29
+ // page's `history` and postMessages across — was rejected: it
30
+ // pollutes every page's globals with a wrapper the page can see,
31
+ // unpatch, or use to forge events, for a signal Chrome already hands
32
+ // us for free.
33
+ //
34
+ // Wire protocol:
35
+ // SW → page: `{ type: 'sm:url-change', url }` via
36
+ // `chrome.tabs.sendMessage(tabId, msg, { documentId })`, falling
37
+ // back to `{ frameId }` (see `urlChangeTarget`). The URL is
38
+ // replayed verbatim to the userscript, so a burst of history
39
+ // updates produces one event each rather than a coalesced one.
40
+ //
41
+ // No gating: we notify on every reported update rather than tracking
42
+ // which tabs have a granting script. A stateful gate would have to
43
+ // survive SW restarts (chrome.userScripts registrations do; SW memory
44
+ // doesn't), and getting that wrong fails silently. The message is
45
+ // cheap — the SW is already awake handling this exact event for tab
46
+ // status — and a frame with no SourceMonkey runtime just rejects the
47
+ // send, which we swallow.
48
+ export const SM_URL_CHANGE_MSG = 'sm:url-change';
49
+ // Pure: should this history update be forwarded to a frame?
50
+ //
51
+ // Chrome reports events for pre-render / devtools targets with a
52
+ // negative `tabId`, which `chrome.tabs.sendMessage` can't address. A
53
+ // URL-less event has nothing worth reporting. Everything else is
54
+ // forwarded, including `file://` (SourceMonkey injects there too).
55
+ //
56
+ // Sub-frames pass the filter, but the targeting below is what
57
+ // actually matters for them: a sub-frame's navigation must never
58
+ // be delivered to the top frame's listeners as if the document had
59
+ // moved. A sub-frame with no script in it simply has no receiver.
60
+ export function shouldNotifyUrlChange(details) {
61
+ if (typeof details.tabId !== 'number' || details.tabId < 0)
62
+ return false;
63
+ if (typeof details.frameId !== 'number' || details.frameId < 0)
64
+ return false;
65
+ return typeof details.url === 'string' && details.url !== '';
66
+ }
67
+ // Which receiver the message is addressed to.
68
+ //
69
+ // Prefer `documentId` over `frameId`: a frame ID names a *slot*, not a
70
+ // document. The send is asynchronous relative to the navigation event,
71
+ // so if that frame commits a cross-document navigation in between, a
72
+ // frame-addressed message lands in a document that was never at this
73
+ // URL and its listeners report a URL the page isn't on. A document ID
74
+ // names the exact document, and the send simply finds no receiver once
75
+ // that document is gone — which is the outcome we want.
76
+ //
77
+ // Only one of the two is passed: Chrome treats them as alternatives,
78
+ // not as a pair to intersect.
79
+ export function urlChangeTarget(details) {
80
+ return typeof details.documentId === 'string' && details.documentId !== ''
81
+ ? { documentId: details.documentId }
82
+ : { frameId: details.frameId };
83
+ }
84
+ // Fire-and-forget notification to the document that changed. Rejections
85
+ // are expected and swallowed: pages with no active SourceMonkey script
86
+ // have no GM runtime registered, so there is no receiving end.
87
+ export function notifyUrlChange(details) {
88
+ if (!shouldNotifyUrlChange(details))
89
+ return;
90
+ void chrome.tabs
91
+ .sendMessage(details.tabId, { type: SM_URL_CHANGE_MSG, url: details.url }, urlChangeTarget(details))
92
+ .catch(() => undefined);
93
+ }
94
+ // Subscribe to both same-document navigation events. Called once from
95
+ // `installNavigationListeners` in background.ts.
96
+ //
97
+ // This registers its own listeners rather than piggy-backing on the
98
+ // tab-status `onHistoryStateUpdated` listener already in background.ts:
99
+ // that one filters to the main frame, and a userscript inside an
100
+ // iframe wants its own frame's navigations too.
101
+ export function installUrlChangeWatch() {
102
+ const wn = chrome.webNavigation;
103
+ if (!wn?.onHistoryStateUpdated || !wn?.onReferenceFragmentUpdated) {
104
+ // Matches what TM logs in the same situation. `console.info`, not
105
+ // `.warn` — see the logging policy in CLAUDE.md.
106
+ console.info('[SourceMonkey] chrome.webNavigation same-document events unavailable; window.onurlchange will not fire');
107
+ return;
108
+ }
109
+ wn.onHistoryStateUpdated.addListener((d) => notifyUrlChange(d));
110
+ wn.onReferenceFragmentUpdated.addListener((d) => notifyUrlChange(d));
111
+ }
@@ -0,0 +1,27 @@
1
+ // Shared URL-scheme gate for the SW-side `GM_*` dispatchers.
2
+ //
3
+ // Every dispatcher that hands a page-supplied URL to a privileged
4
+ // Chrome API (`chrome.tabs.create`, `chrome.notifications.create`,
5
+ // `chrome.downloads.download`) has to answer the same question first:
6
+ // is this a scheme we're willing to act on? Without the gate a
7
+ // userscript could ask the extension to open, fetch, or save
8
+ // `file:///…`, `chrome://…`, or `chrome-extension://<id>/…` — reaching
9
+ // past what the page itself could do.
10
+ //
11
+ // The allowed set differs per API (tabs and downloads take http(s);
12
+ // notification icons also take `data:`), so the caller passes it in.
13
+ // Only the parse-and-compare is shared — that's the part that was
14
+ // previously copied three times and could drift.
15
+ // True iff `url` parses and its scheme is one of `allowed`.
16
+ // `allowed` entries include the trailing colon (`'http:'`), matching
17
+ // `URL.protocol`. Anything unparseable is refused.
18
+ export function hasAllowedScheme(url, allowed) {
19
+ try {
20
+ return allowed.includes(new URL(url).protocol);
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
26
+ // The common case: plain web URLs.
27
+ export const WEB_SCHEMES = Object.freeze(['http:', 'https:']);