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,186 @@
1
+ // Bounded-concurrency fan-out.
2
+ //
3
+ // Every fan-out over a collection's scripts runs through here, because
4
+ // the unbounded form is a crash rather than a slowdown. One promise per
5
+ // scanned file means one simultaneous fetch per script, each holding a
6
+ // decoded body plus any prefetched `@require` / `@resource` payloads;
7
+ // across a few thousand scripts that peak is enough to take the whole
8
+ // extension process down with an out-of-memory abort. Chrome surfaces
9
+ // that only as "this extension crashed", with no JavaScript error
10
+ // anywhere, so it is not a failure the logs will ever explain.
11
+ //
12
+ // Bounds the in-flight working set, not the total: callers still retain
13
+ // every result, so a large enough collection is still a large enough
14
+ // peak.
15
+ // ---------- Refresh limits ----------
16
+ //
17
+ // These nest, so read them as a product. Refreshing one collection uses
18
+ // only the inner limit; "Refresh All" runs the outer limit's worth of
19
+ // collections, each with its own inner set:
20
+ //
21
+ // worst-case fetches in flight
22
+ // = FETCH_COLLECTIONS_CONCURRENCY × FETCH_SCRIPTS_PER_COLLECTION_CONCURRENCY
23
+ //
24
+ // The balance leans on the inner one because that's the limit the
25
+ // common cases actually hit: most profiles have one or a few
26
+ // collections, and a per-collection Refresh button never engages the
27
+ // outer limit at all. Making the outer limit the larger of the two
28
+ // would only speed up a profile with many collections while capping the
29
+ // single-collection case that everyone hits.
30
+ // How many scripts a single collection's refresh fetches at once.
31
+ //
32
+ // Chosen for memory, not throughput: each in-flight entry can hold a
33
+ // script body plus its dependency payloads, so the cap is what keeps
34
+ // peak usage proportional to the limit instead of to the collection
35
+ // size. Large enough to keep the network and the file reader busy. (For
36
+ // an HTTP/1.1 host the browser's own ~6-per-host connection limit is a
37
+ // second backstop in the same range, but that is not something to lean
38
+ // on: an HTTP/2 host multiplexes and imposes no such bound, and file://
39
+ // reads have no per-host limit at all.)
40
+ export const FETCH_SCRIPTS_PER_COLLECTION_CONCURRENCY = 16;
41
+ // How many collections a "Refresh All" pass works on at once.
42
+ //
43
+ // Deliberately the smaller of the pair — see the note above. It exists
44
+ // to stop the product from exploding on a many-collection profile, not
45
+ // to make that profile fast.
46
+ export const FETCH_COLLECTIONS_CONCURRENCY = 2;
47
+ // ---------- Directory-scan limits ----------
48
+ //
49
+ // A scan lists a directory, then lists each subdirectory it found. Those
50
+ // siblings are independent, but walking them with an `await` in the loop
51
+ // made the whole scan one round trip per directory — measured at 195ms
52
+ // for a local tree of 78 directories, and 1151ms for a GitHub repo whose
53
+ // listing needs four API calls. The depth cap means these nest at most
54
+ // twice, so read each as a product with itself.
55
+ // Sibling `file://` directories listed at once.
56
+ //
57
+ // The larger of the pair: a local listing is a cheap read with no
58
+ // per-host connection limit to respect, and deep trees are the common
59
+ // shape (a userscripts folder organised by site).
60
+ export const SCAN_DIRS_CONCURRENCY = 12;
61
+ // Sibling GitHub directories listed at once.
62
+ //
63
+ // Deliberately small, and NOT for memory: every listing is a request
64
+ // against api.github.com, which applies a secondary limit to bursts of
65
+ // concurrent requests from one client on top of the hourly quota. Being
66
+ // throttled costs far more than the serialization this saves, and the
67
+ // win here is mostly the first fan-out anyway — a repo needing four
68
+ // calls goes from four round trips to two.
69
+ export const SCAN_GITHUB_DIRS_CONCURRENCY = 4;
70
+ // ---------- Update-probe limits ----------
71
+ //
72
+ // Their own pair rather than a reuse of the refresh limits above: the
73
+ // scripts handed to one probe pass can span every collection, so neither
74
+ // refresh limit describes the shape. Same nesting rule, though — read
75
+ // them as a product.
76
+ // How many scripts are probed for updates at once.
77
+ //
78
+ // This is the one that binds — a script yields at most two probe URLs,
79
+ // so the inner limit never does.
80
+ //
81
+ // It was 3, on the reasoning that a probe is an annotation nothing
82
+ // waits on. That is true of the per-script caller, which fires and
83
+ // forgets on the navigation path, and false of the collection ones: a
84
+ // Refresh / Refresh All awaits the probe pass so the row is current
85
+ // when the spinner stops. The limit sized for the caller that doesn't
86
+ // wait was being paid by the two that do — measured at 1772ms for 30
87
+ // probed scripts, ten serialized rounds of a couple-KB header fetch.
88
+ //
89
+ // So it is sized for the awaiting caller now. Still bounded, because
90
+ // the pass can span every collection at once and the nesting rule below
91
+ // still applies; a probe is also cheap in the way that matters here —
92
+ // it holds one small response, not a script body plus its dependency
93
+ // payloads, which is what forces the refresh limits to stay low.
94
+ export const PROBE_SCRIPTS_CONCURRENCY = 10;
95
+ // How many of ONE script's probe URLs are fetched at once.
96
+ //
97
+ // A guard rather than a working limit: `probeTargets` yields at most two
98
+ // entries per script (`@updateURL` and `@downloadURL`, deduped by URL),
99
+ // so the list never reaches this cap. It is here so that adding a third
100
+ // probe target kind can't quietly turn the per-script step into an
101
+ // unbounded fan-out — the failure this whole module exists to prevent.
102
+ export const PROBE_URLS_PER_SCRIPT_CONCURRENCY = 4;
103
+ // ---------- Parse-check limit ----------
104
+ // How many rows the registration-time re-check compiles at once
105
+ // (`verifyParseChecks`).
106
+ //
107
+ // The list this bounds is normally empty — a row keeps its verdict
108
+ // until its content changes. The pass that isn't empty is the first
109
+ // sync after a SourceMonkey update: `parseCheckKey` carries the
110
+ // extension version (how we wrap the program decides what parses), so
111
+ // an upgrade makes every stored row stale at once, and every compile
112
+ // composes the body plus each `@require` in memory before shipping it
113
+ // to the sandbox. Registration waits on the whole pass, so this is a
114
+ // memory bound on a step that runs before any script is injected.
115
+ export const PARSE_RECHECK_CONCURRENCY = 4;
116
+ // Run `fn` over `items` with at most `limit` calls outstanding.
117
+ //
118
+ // Results come back in `items` order regardless of completion order, so
119
+ // this is a drop-in replacement for `Promise.all(items.map(fn))`.
120
+ //
121
+ // Rejection behavior also matches `Promise.all`, in both halves — and
122
+ // the second half is the subtle one:
123
+ //
124
+ // - The first rejection rejects the whole call immediately. The caller
125
+ // does not wait out the rest of the list to hear about a failure.
126
+ // - Every item still RUNS. No work is cancelled (there is nothing to
127
+ // cancel it with); the pool keeps draining in the background after
128
+ // the caller has been rejected, and those results are discarded.
129
+ //
130
+ // The second half is why a worker swallows its item's rejection instead
131
+ // of letting it break the loop. A throwing `fn` that killed its worker
132
+ // would be fine for a one-off failure — the other workers drain the
133
+ // list — but a SYSTEMIC failure (every item throws) would kill every
134
+ // worker in turn and silently skip the tail of the list. That is the
135
+ // case that matters here: an out-of-quota `chrome.storage.local` write
136
+ // fails for every collection, not just one.
137
+ //
138
+ // Callers that need per-item failures to be survivable should return a
139
+ // failure value rather than throwing, which is what the fetch paths do.
140
+ export async function mapWithConcurrency(items, limit, fn) {
141
+ if (items.length === 0)
142
+ return [];
143
+ // A limit at or above the item count has no work to do beyond
144
+ // Promise.all, and a non-positive limit would deadlock the workers
145
+ // below — treat both as "no bound".
146
+ if (limit >= items.length || limit <= 0) {
147
+ return await Promise.all(items.map((item, i) => fn(item, i)));
148
+ }
149
+ const out = new Array(items.length);
150
+ // Rejects the caller the moment the first item fails, while the pool
151
+ // below carries on draining. Never resolves — on the success path the
152
+ // drain is what settles the race.
153
+ let failNow;
154
+ const firstFailure = new Promise((_, reject) => { failNow = reject; });
155
+ let failed = false;
156
+ // Shared cursor: each worker repeatedly claims the next unstarted
157
+ // index. Self-balancing — a worker that draws a slow item doesn't hold
158
+ // up items a free worker could be running, which a fixed chunked split
159
+ // would.
160
+ let next = 0;
161
+ const worker = async () => {
162
+ for (;;) {
163
+ const i = next++;
164
+ if (i >= items.length)
165
+ return;
166
+ try {
167
+ out[i] = await fn(items[i], i);
168
+ }
169
+ catch (err) {
170
+ // Reported, not rethrown: a worker that died here would stop
171
+ // claiming items, and if every item fails that skips the tail of
172
+ // the list entirely. See the note on rejection above.
173
+ if (!failed) {
174
+ failed = true;
175
+ failNow(err);
176
+ }
177
+ }
178
+ }
179
+ };
180
+ const drained = Promise.all(Array.from({ length: limit }, () => worker()));
181
+ // Workers never reject, so `drained` can only resolve — the race
182
+ // settles on `firstFailure` for a failure and on `drained` otherwise,
183
+ // and neither branch leaves an unhandled rejection behind.
184
+ await Promise.race([drained, firstFailure]);
185
+ return out;
186
+ }
@@ -0,0 +1,109 @@
1
+ // What the user has told SourceMonkey about the hosts a script's
2
+ // `GM_xmlhttpRequest` may reach beyond its `@connect` rules: the
3
+ // answers to the connection prompt (`connect-prompt.ts`), read by the
4
+ // worker before it asks and by the script's page, which lists them
5
+ // under Network access and can take them back.
6
+ //
7
+ // - Allowed and denied hosts persist in one row per script
8
+ // (`chrome.storage.local`), across refreshes and updates of the
9
+ // script, until reset on the script's page. The row goes when the
10
+ // script is removed.
11
+ //
12
+ // Hosts are `URL.hostname`s, lowercased: one host each, no subdomains,
13
+ // any port (as `@connect` matches hosts).
14
+ import { assertWorkerContext } from '../run-context.js';
15
+ // The prompt window's page, and what it sends the worker: the answer,
16
+ // and while it waits, a message every so often so the worker isn't
17
+ // stopped for being idle (pending work alone doesn't count) and lose
18
+ // the requests waiting on it. Here rather than in `connect-prompt.ts`
19
+ // so the page needn't load the worker's side.
20
+ export const CONNECT_PROMPT_PAGE = 'connect-prompt.html';
21
+ export const CONNECT_ANSWER_MESSAGE = 'sm:connect-answer';
22
+ export const CONNECT_PROMPT_ALIVE_MESSAGE = 'sm:connect-prompt-alive';
23
+ // Each change reads the row and writes it back, so two at once (two
24
+ // prompts answered together) would lose one. Within one context they
25
+ // take turns; the worker and a script's page changing the same script
26
+ // in the same moment is left to chance.
27
+ let queue = Promise.resolve();
28
+ function serialized(fn) {
29
+ const run = queue.then(fn, fn);
30
+ queue = run.catch(() => undefined);
31
+ return run;
32
+ }
33
+ const GRANTS_PREFIX = 'connectGrants:';
34
+ export function connectGrantsKey(scriptId) {
35
+ return GRANTS_PREFIX + scriptId;
36
+ }
37
+ export async function getConnectGrants(scriptId) {
38
+ const key = connectGrantsKey(scriptId);
39
+ const row = (await chrome.storage.local.get(key))[key];
40
+ return {
41
+ hosts: Array.isArray(row?.hosts) ? row.hosts : [],
42
+ ...(row?.allHosts ? { allHosts: true } : {}),
43
+ ...(Array.isArray(row?.denied) && row.denied.length > 0 ? { denied: row.denied } : {}),
44
+ };
45
+ }
46
+ // The user's standing answer about `host` for this script, if any. A
47
+ // host is never both allowed and denied (each answer clears the other),
48
+ // and "all sites" clears all denials.
49
+ export async function connectAnswer(scriptId, host) {
50
+ const grants = await getConnectGrants(scriptId);
51
+ if (grants.denied?.includes(host))
52
+ return 'deny';
53
+ if (grants.allHosts || grants.hosts.includes(host))
54
+ return 'allow';
55
+ return null;
56
+ }
57
+ // Allow `host`, or with `host` null, all hosts. Clears a denial of it
58
+ // (of all of them, for all hosts).
59
+ export async function allowConnectHost(scriptId, host) {
60
+ return change(scriptId, (grants) => {
61
+ if (host === null) {
62
+ grants.allHosts = true;
63
+ delete grants.denied;
64
+ return;
65
+ }
66
+ if (!grants.hosts.includes(host))
67
+ grants.hosts.push(host);
68
+ grants.denied = grants.denied?.filter((h) => h !== host);
69
+ });
70
+ }
71
+ // Deny `host`. Clears an allowance of it.
72
+ export async function denyConnectHost(scriptId, host) {
73
+ return change(scriptId, (grants) => {
74
+ grants.hosts = grants.hosts.filter((h) => h !== host);
75
+ if (!grants.denied?.includes(host))
76
+ grants.denied = [...grants.denied ?? [], host];
77
+ });
78
+ }
79
+ // Take back an allowance: one host, or with `host` null, "all sites".
80
+ export async function revokeConnectHost(scriptId, host) {
81
+ return change(scriptId, (grants) => {
82
+ if (host === null)
83
+ delete grants.allHosts;
84
+ else
85
+ grants.hosts = grants.hosts.filter((h) => h !== host);
86
+ });
87
+ }
88
+ // Forget a denial, so the next request asks again.
89
+ export async function forgetDeniedHost(scriptId, host) {
90
+ return change(scriptId, (grants) => {
91
+ grants.denied = grants.denied?.filter((h) => h !== host);
92
+ });
93
+ }
94
+ // Read the row, apply `edit`, and write it back (or remove it once
95
+ // nothing is left in it).
96
+ function change(scriptId, edit) {
97
+ return serialized(async () => {
98
+ const grants = await getConnectGrants(scriptId);
99
+ edit(grants);
100
+ if (grants.denied?.length === 0)
101
+ delete grants.denied;
102
+ const key = connectGrantsKey(scriptId);
103
+ assertWorkerContext('write storage');
104
+ if (grants.hosts.length === 0 && !grants.allHosts && !grants.denied)
105
+ await chrome.storage.local.remove(key);
106
+ else
107
+ await chrome.storage.local.set({ [key]: grants });
108
+ });
109
+ }
@@ -0,0 +1,70 @@
1
+ // The connection prompt window (`connect-prompt.html`): asks whether a
2
+ // script may reach a host its `@connect` rules don't cover. The worker
3
+ // opens it with everything it shows in the query string
4
+ // (`connect-prompt.ts`), and the answer goes back by message. Closing
5
+ // the window instead cancels, which the worker sees for itself.
6
+ import { CONNECT_ANSWER_MESSAGE, CONNECT_PROMPT_ALIVE_MESSAGE, } from './connect-grants.js';
7
+ // Under the worker's 30-second idle limit.
8
+ const ALIVE_INTERVAL_MS = 20_000;
9
+ const params = new URLSearchParams(location.search);
10
+ const promptId = params.get('prompt') ?? '';
11
+ const scriptId = params.get('script') ?? '';
12
+ const host = params.get('host') ?? '';
13
+ function byId(id) {
14
+ return document.getElementById(id);
15
+ }
16
+ // What the request was, without its query string or fragment: those
17
+ // often hold keys or session tokens, and the host and path are what the
18
+ // question is about.
19
+ function withoutQuery(url) {
20
+ try {
21
+ const u = new URL(url);
22
+ return `${u.origin}${u.pathname}`;
23
+ }
24
+ catch {
25
+ return url;
26
+ }
27
+ }
28
+ const link = byId('script');
29
+ link.textContent = params.get('name') || 'This script';
30
+ link.href = chrome.runtime.getURL(`userscript.html?script=${encodeURIComponent(scriptId)}`);
31
+ // The site, linked at its root. The request itself is in the tooltip.
32
+ const site = byId('site');
33
+ const url = params.get('url') ?? '';
34
+ site.textContent = host;
35
+ try {
36
+ site.href = `${new URL(url).origin}/`;
37
+ }
38
+ catch {
39
+ site.removeAttribute('href');
40
+ }
41
+ site.title = `${params.get('method') ?? 'GET'} ${withoutQuery(url)}`;
42
+ // Each button's id is the choice it sends.
43
+ const BUTTONS = ['once', 'cancel', 'allow', 'deny', 'allow-all'];
44
+ async function answer(choice) {
45
+ for (const id of BUTTONS)
46
+ byId(id).disabled = true;
47
+ const message = { type: CONNECT_ANSWER_MESSAGE, promptId, scriptId, host, choice };
48
+ let ok = false;
49
+ try {
50
+ ok = await chrome.runtime.sendMessage(message) === true;
51
+ }
52
+ catch {
53
+ // Reported below.
54
+ }
55
+ if (ok) {
56
+ // The worker closes the window; this is for when it can't.
57
+ window.close();
58
+ return;
59
+ }
60
+ const error = byId('error');
61
+ error.textContent = 'SourceMonkey could not save this answer. Close this window and try the request again.';
62
+ error.hidden = false;
63
+ }
64
+ for (const id of BUTTONS)
65
+ byId(id).addEventListener('click', () => void answer(id));
66
+ // No button starts focused: the window takes focus as it opens, and a
67
+ // key pressed for the page underneath mustn't answer it.
68
+ setInterval(() => {
69
+ chrome.runtime.sendMessage({ type: CONNECT_PROMPT_ALIVE_MESSAGE }).catch(() => undefined);
70
+ }, ALIVE_INTERVAL_MS);
@@ -0,0 +1,266 @@
1
+ // The connection prompt, worker side: what happens when a script's
2
+ // `GM_xmlhttpRequest` goes to a host its `@connect` rules don't cover
3
+ // and the user hasn't answered for yet (`connect-grants.ts`).
4
+ //
5
+ // - A small popup window (`connect-prompt.html`) asks: allow this host,
6
+ // allow all sites, or deny. A window of the extension's own, rather
7
+ // than something drawn in the page, so the page can neither fake nor
8
+ // click it.
9
+ // - One window per (script, host). Requests that arrive while it is up
10
+ // wait on the same answer, rather than each opening another.
11
+ // - Each request waits `CONNECT_PROMPT_WAIT_MS` at most, then fails.
12
+ // The limit sits under the page's own 5-minute give-up
13
+ // (`XHR_PAGE_SIDE_TTL_MS`), so the request fails with a message that
14
+ // says why rather than as silence.
15
+ // - A prompt with no request left waiting on it (timed out, aborted, or
16
+ // its tab closed) closes. Otherwise it would stay up asking about
17
+ // nothing, and its pings would keep the worker running.
18
+ // - At most `MAX_PROMPTS_PER_SCRIPT` windows are open for one script; a
19
+ // request for another site fails at once. A script looping over many
20
+ // hosts would otherwise bury the user in windows.
21
+ // - Cancel, or closing the window, refuses just the requests waiting
22
+ // on it and keeps nothing, so the next request asks again.
23
+ // - The answer travels with the script id and host, so it is saved even
24
+ // when the worker restarted in between and lost track of the window.
25
+ //
26
+ // sm-dev sees each prompt (`connect-prompt` events) and can answer it
27
+ // (`answerConnectPrompt`), since an agent can't click the window.
28
+ import { allowConnectHost, connectAnswer, CONNECT_PROMPT_PAGE, denyConnectHost } from './connect-grants.js';
29
+ import { appendScriptLog } from './script-log.js';
30
+ import { emitSmDevEvent } from './sm-dev-link.js';
31
+ export const CONNECT_PROMPT_WAIT_MS = 4 * 60 * 1000;
32
+ export const MAX_PROMPTS_PER_SCRIPT = 3;
33
+ const pending = new Map();
34
+ let nextPromptId = 1;
35
+ function pendingKey(scriptId, host) {
36
+ return `${scriptId}\n${host}`;
37
+ }
38
+ // Whether the request may go: the user's standing answer, or the one
39
+ // they give the prompt this opens (or one already open for this host).
40
+ export async function askConnect(info) {
41
+ const standing = await connectAnswer(info.scriptId, info.host);
42
+ if (standing === 'deny')
43
+ logStandingDenial(info);
44
+ if (standing)
45
+ return standing;
46
+ if (info.signal?.aborted)
47
+ return 'aborted';
48
+ const key = pendingKey(info.scriptId, info.host);
49
+ let entry = pending.get(key);
50
+ if (!entry) {
51
+ const open = [...pending.values()].filter((p) => p.scriptId === info.scriptId).length;
52
+ if (open >= MAX_PROMPTS_PER_SCRIPT)
53
+ return 'too-many';
54
+ entry = {
55
+ promptId: `cp${nextPromptId++}-${Math.random().toString(36).slice(2, 8)}`,
56
+ scriptId: info.scriptId,
57
+ host: info.host,
58
+ tabId: info.tabId,
59
+ windowId: null,
60
+ waiters: new Set(),
61
+ };
62
+ pending.set(key, entry);
63
+ const opened = entry;
64
+ void openPromptWindow(opened, info).then((windowId) => {
65
+ // Answered (by sm-dev) before the window was even up.
66
+ if (pending.get(key) !== opened) {
67
+ if (windowId !== null)
68
+ void chrome.windows.remove(windowId).catch(() => undefined);
69
+ return;
70
+ }
71
+ opened.windowId = windowId;
72
+ });
73
+ emitSmDevEvent({
74
+ kind: 'connect-prompt', op: 'shown', promptId: entry.promptId, scriptId: info.scriptId,
75
+ tabId: info.tabId, host: info.host, url: info.url, method: info.method,
76
+ });
77
+ void appendScriptLog(info.scriptId, {
78
+ level: 'info',
79
+ source: 'gm-xhr',
80
+ tabId: info.tabId,
81
+ message: `Opened permission request window for connection to ${info.host} (${info.method} ${withoutQuery(info.url)})`,
82
+ // sm-dev prints its own line for the prompt, which also says how
83
+ // to answer it.
84
+ fromReport: true,
85
+ }).catch(() => undefined);
86
+ }
87
+ const waiting = entry;
88
+ return new Promise((resolve) => {
89
+ const done = (outcome) => {
90
+ clearTimeout(timer);
91
+ info.signal?.removeEventListener('abort', onAbort);
92
+ waiting.waiters.delete(done);
93
+ resolve(outcome);
94
+ // Given up on rather than answered: the last one out closes it.
95
+ if ((outcome === 'unanswered' || outcome === 'aborted') && waiting.waiters.size === 0)
96
+ dropPrompt(waiting);
97
+ };
98
+ const onAbort = () => done('aborted');
99
+ info.signal?.addEventListener('abort', onAbort);
100
+ const timer = setTimeout(() => done('unanswered'), CONNECT_PROMPT_WAIT_MS);
101
+ waiting.waiters.add(done);
102
+ });
103
+ }
104
+ // Script, document and host of each request a standing denial has
105
+ // refused and logged, so a script that retries logs once per site for
106
+ // each page it runs on. Kept for the worker's lifetime, and cleared when
107
+ // it grows past `LOGGED_DENIALS_MAX`, which at worst logs a repeat.
108
+ const loggedDenials = new Set();
109
+ const LOGGED_DENIALS_MAX = 1000;
110
+ function logStandingDenial(info) {
111
+ const key = `${info.scriptId}\n${info.documentId ?? `tab ${info.tabId}`}\n${info.host}`;
112
+ if (loggedDenials.has(key))
113
+ return;
114
+ if (loggedDenials.size >= LOGGED_DENIALS_MAX)
115
+ loggedDenials.clear();
116
+ loggedDenials.add(key);
117
+ void appendScriptLog(info.scriptId, {
118
+ level: 'warning',
119
+ source: 'gm-xhr',
120
+ tabId: info.tabId,
121
+ message: `Blocked a request to ${info.host}, which was denied for this script. `
122
+ + "Manage permissions on the script's page. Later requests to it from the same page are blocked without a log entry.",
123
+ }).catch(() => undefined);
124
+ }
125
+ // A URL without its query string or fragment, for the log: those often
126
+ // hold keys or session tokens.
127
+ function withoutQuery(url) {
128
+ try {
129
+ const u = new URL(url);
130
+ return `${u.origin}${u.pathname}`;
131
+ }
132
+ catch {
133
+ return url;
134
+ }
135
+ }
136
+ // Close a prompt nobody is waiting on any more, without an answer.
137
+ function dropPrompt(entry) {
138
+ const key = pendingKey(entry.scriptId, entry.host);
139
+ if (pending.get(key) !== entry)
140
+ return;
141
+ pending.delete(key);
142
+ if (entry.windowId !== null)
143
+ void chrome.windows.remove(entry.windowId).catch(() => undefined);
144
+ emitSmDevEvent({
145
+ kind: 'connect-prompt', op: 'closed', promptId: entry.promptId, scriptId: entry.scriptId,
146
+ tabId: entry.tabId, host: entry.host,
147
+ });
148
+ }
149
+ // For tests: close all prompts, and forget which denials were logged.
150
+ export function resetConnectPromptsForTests() {
151
+ for (const entry of [...pending.values()])
152
+ dropPrompt(entry);
153
+ loggedDenials.clear();
154
+ }
155
+ async function openPromptWindow(entry, info) {
156
+ const params = new URLSearchParams({
157
+ prompt: entry.promptId,
158
+ script: info.scriptId,
159
+ name: info.scriptName,
160
+ host: info.host,
161
+ url: info.url,
162
+ method: info.method,
163
+ });
164
+ const width = 520;
165
+ const height = 350;
166
+ // Over the window the request came from, when there is one to place
167
+ // it by.
168
+ let place = {};
169
+ try {
170
+ const tab = await chrome.tabs.get(info.tabId);
171
+ const win = await chrome.windows.get(tab.windowId);
172
+ if (win.left !== undefined && win.top !== undefined && win.width !== undefined) {
173
+ // Not clamped at 0: a screen left of the primary one has negative
174
+ // coordinates.
175
+ place = { left: win.left + Math.round((win.width - width) / 2), top: win.top + 80 };
176
+ }
177
+ }
178
+ catch {
179
+ // Placed where Chrome likes.
180
+ }
181
+ try {
182
+ const win = await chrome.windows.create({
183
+ url: chrome.runtime.getURL(`${CONNECT_PROMPT_PAGE}?${params}`),
184
+ type: 'popup',
185
+ width,
186
+ height,
187
+ focused: true,
188
+ ...place,
189
+ });
190
+ return win?.id ?? null;
191
+ }
192
+ catch (err) {
193
+ // No window means no answer can come: fail the requests waiting,
194
+ // and leave nothing behind for the next one to join.
195
+ dropPrompt(entry);
196
+ const failed = err instanceof Error ? err.message : String(err);
197
+ for (const waiter of [...entry.waiters])
198
+ waiter({ failed });
199
+ return null;
200
+ }
201
+ }
202
+ // Record the user's answer and let the waiting requests go (or fail).
203
+ // `promptId` names the prompt answered; the answer is saved by script
204
+ // and host whether or not the worker still knows that prompt.
205
+ export async function answerConnect(scriptId, host, choice, how) {
206
+ if (choice === 'deny')
207
+ await denyConnectHost(scriptId, host);
208
+ else if (choice === 'allow' || choice === 'allow-all')
209
+ await allowConnectHost(scriptId, choice === 'allow-all' ? null : host);
210
+ const key = pendingKey(scriptId, host);
211
+ const entry = pending.get(key);
212
+ const others = choice === 'allow-all'
213
+ ? [...pending.values()].filter((p) => p.scriptId === scriptId && p !== entry)
214
+ : [];
215
+ for (const settled of entry ? [entry, ...others] : others) {
216
+ pending.delete(pendingKey(settled.scriptId, settled.host));
217
+ const outcome = choice === 'deny' ? 'deny' : choice === 'cancel' ? 'cancelled' : 'allow';
218
+ // "All sites" settles this script's other prompts too; "once" only this one.
219
+ for (const waiter of [...settled.waiters])
220
+ waiter(outcome);
221
+ if (how !== 'closed' && settled.windowId !== null) {
222
+ void chrome.windows.remove(settled.windowId).catch(() => undefined);
223
+ }
224
+ emitSmDevEvent({
225
+ kind: 'connect-prompt', op: 'answered', promptId: settled.promptId, scriptId,
226
+ tabId: settled.tabId, host: settled.host, answer: choice,
227
+ });
228
+ }
229
+ const where = how === 'sm-dev' ? ' from sm-dev' : '';
230
+ const message = choice === 'allow-all' ? `Allowed this script to connect to all sites${where}`
231
+ : choice === 'allow' ? `Allowed this script to connect to ${host}${where}`
232
+ : choice === 'once' ? `Allowed this script to connect to ${host} once${where}. The requests waiting on the permission prompt were sent, and the next one asks again.`
233
+ : choice === 'deny' ? `Denied this script connecting to ${host}${where}. Manage permissions on the script's page.`
234
+ : how === 'closed' ? `The permission prompt about connecting to ${host} was closed, which cancels it. The requests waiting on it were blocked, and the next one asks again.`
235
+ : `Cancelled the permission prompt about connecting to ${host}${where}. The requests waiting on it were blocked, and the next one asks again.`;
236
+ await appendScriptLog(scriptId, {
237
+ level: choice === 'deny' || choice === 'cancel' ? 'warning' : 'info',
238
+ source: 'gm-xhr',
239
+ ...(entry ? { tabId: entry.tabId } : {}),
240
+ message,
241
+ }).catch(() => undefined);
242
+ }
243
+ // A prompt window closed. Without an answer first, that cancels it.
244
+ export function onConnectPromptWindowRemoved(windowId) {
245
+ for (const entry of pending.values()) {
246
+ if (entry.windowId === windowId) {
247
+ void answerConnect(entry.scriptId, entry.host, 'cancel', 'closed');
248
+ return;
249
+ }
250
+ }
251
+ }
252
+ // sm-dev's answer to a prompt it was told about. False when no such
253
+ // prompt is waiting.
254
+ export async function answerConnectPrompt(promptId, choice) {
255
+ for (const entry of pending.values()) {
256
+ if (entry.promptId === promptId) {
257
+ await answerConnect(entry.scriptId, entry.host, choice, 'sm-dev');
258
+ return true;
259
+ }
260
+ }
261
+ return false;
262
+ }
263
+ // The prompts waiting, for sm-dev to list.
264
+ export function pendingConnectPrompts() {
265
+ return [...pending.values()].map(({ promptId, scriptId, tabId, host }) => ({ promptId, scriptId, tabId, host }));
266
+ }