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,679 @@
1
+ // Collection I/O: fetch + scan for every collection kind.
2
+ //
3
+ // Owns: content fetch and the scanner that turns a collection path/URL
4
+ // into a list of FoundScript entries. Path helpers, the `file://`
5
+ // reader and listing parser are in `src/file-locations.ts`, which this
6
+ // re-exports. All persistence lives in `src/extension/script-store.ts`; this
7
+ // module is pure I/O.
8
+ //
9
+ // The following collection kinds all flow through the same downstream
10
+ // pipeline:
11
+ // - local directory (an absolute filesystem path; recursive scan)
12
+ // - local single file (an absolute path ending in .user.js)
13
+ // - http single file (an http(s) URL whose pathname ends in .user.js)
14
+ // - manifest (anything whose pathname's basename is
15
+ // `script_manifest.json` — an absolute local
16
+ // path, a file:// URL, or an http(s) URL —
17
+ // returning a JSON list of script URLs that
18
+ // resolve against the manifest URL. The exact
19
+ // filename is the only manifest shape we accept.)
20
+ //
21
+ // HTTP requirements:
22
+ // - manifest: "<all_urls>" (or a narrower http(s) pattern) in
23
+ // host_permissions so the SW/collections page can fetch arbitrary
24
+ // origins without CORS blocking the response body.
25
+ //
26
+ // File requirements:
27
+ // - manifest: "file:///*" in host_permissions
28
+ // - user: enabled "Allow access to file URLs" on chrome://extensions
29
+ // The toggle is persistent across browser sessions, so unlike the File
30
+ // System Access API there is no per-session re-grant.
31
+ import { MAX_SCAN_DEPTH, MANIFEST_FILENAME } from '../collection-scan.js';
32
+ import { mapWithConcurrency, SCAN_DIRS_CONCURRENCY } from './concurrency.js';
33
+ // Page-only API under the hood, but the wrapper answers "allowed" from
34
+ // any context that can't ask — and only `classifyLocation`, which only
35
+ // the collection dialog calls, uses it.
36
+ import { isFileSchemeAccessAllowed } from './file-access.js';
37
+ export { MAX_SCAN_DEPTH } from '../collection-scan.js';
38
+ // A single script typed where a collection was expected. Two spellings
39
+ // of one message: the bare sentence for a surface that offers the
40
+ // install route itself (the collection dialog puts a button under it),
41
+ // and the sentence plus the fix for surfaces that can only refuse.
42
+ export const SINGLE_SCRIPT_NOT_A_COLLECTION = 'This is a single script, not a collection';
43
+ export const SINGLE_SCRIPT_USE_ADD_FIELD = `${SINGLE_SCRIPT_NOT_A_COLLECTION}. To install it, paste it in the Add field instead.`;
44
+ // A readable local file of a shape no collection can be built from.
45
+ // Says what would work rather than what didn't, per the "name the fix"
46
+ // rule.
47
+ export const UNSUPPORTED_LOCATION_FILE = `File must be a script (.user.js) or a manifest (${MANIFEST_FILENAME})`;
48
+ // An http(s) URL that names no collection we recognise. GitHub is the
49
+ // only folder we can scan over http — a plain web directory has no
50
+ // listing to read — so naming it is not an example but the whole rule.
51
+ // Deliberately shaped like `UNSUPPORTED_LOCATION_FILE`: one sentence
52
+ // saying what would work.
53
+ export const HTTP_URL_NOT_A_COLLECTION = `URL must be a GitHub folder or a manifest (${MANIFEST_FILENAME})`;
54
+ // A local path nothing could be read from. A file:// read fails
55
+ // identically for "not there" and "not allowed", so the caller passes
56
+ // in the answer to the second (`isFileSchemeAccessAllowed`) and the
57
+ // message names one cause rather than listing both.
58
+ //
59
+ // The off case opens on the same words as the file-access banner
60
+ // (`renderFileAccessBanner`) so the two read as one story, and points
61
+ // at the settings without repeating the banner's link: the banner is
62
+ // already on the page behind this dialog.
63
+ export function locationNotReadableMessage(location, fileAccess) {
64
+ // `Could not read …` matches the wording every other read failure in
65
+ // this module uses, so one session can't show two names for one
66
+ // failure.
67
+ //
68
+ // Ends with a period, unlike the single-sentence messages above: the
69
+ // no-trailing-period rule is there so a message can take a `(in
70
+ // <where>)` suffix, which these never do — they render alone in the
71
+ // dialog's error slot — and a run of sentences whose last one is the
72
+ // only bare one reads as a typo. Same shape as the other multi-
73
+ // sentence messages here (`SINGLE_SCRIPT_USE_ADD_FIELD`).
74
+ return fileAccess
75
+ ? `Could not read ${location}. Check that the path exists.`
76
+ : `Could not read ${location}. File access is disabled for this extension. Enable in extension settings.`;
77
+ }
78
+ // The shapes a location may be typed in: an http(s) URL, a file:// URL,
79
+ // or an absolute POSIX path that isn't the filesystem root. Shared by
80
+ // every surface that accepts one, so the rule and the message
81
+ // (`LOCATION_SHAPES`) can't drift apart.
82
+ export function isValidLocationShape(input) {
83
+ if (isHttpUrl(input))
84
+ return true;
85
+ if (isFileUrl(input))
86
+ return !isFilesystemRoot(locationFsPath(input));
87
+ return input.startsWith('/') && !isFilesystemRoot(input);
88
+ }
89
+ // Whether a POSIX path names the filesystem root, however it is
90
+ // spelled. Scanning `/` would walk the whole disk, so every spelling
91
+ // has to be refused — not just the bare one: `//`, `/.` and `/../..`
92
+ // all land in the same place, and a length test (what this used to be)
93
+ // lets each of them through. A path built only from separators and
94
+ // dots can't name anything else.
95
+ function isFilesystemRoot(path) {
96
+ return /^[/.]*$/.test(path.trim());
97
+ }
98
+ // Decide what a location IS, reading it where we can rather than
99
+ // guessing from its name. Local paths get probed — a directory listing
100
+ // first, then a plain file read — so a path that doesn't exist says so
101
+ // instead of being filed as a directory and failing later in a scan.
102
+ //
103
+ // http(s) URLs are classified by shape alone: there is no remote
104
+ // listing, and a keystroke-rate classifier must not fire network reads
105
+ // at arbitrary origins. Their readability is checked when the
106
+ // collection is created.
107
+ //
108
+ // Called from the collection dialog, a page. The reads would work from
109
+ // the worker too (docs/worker-writes.md#checked-what-the-worker-can-do),
110
+ // but its file-access check (`isFileSchemeAccessAllowed`) can only ask
111
+ // from a page.
112
+ export async function classifyLocation(input) {
113
+ const value = input.trim();
114
+ if (!isValidLocationShape(value)) {
115
+ return { kind: 'invalid', message: `Location ${LOCATION_SHAPES}` };
116
+ }
117
+ if (isHttpUrl(value)) {
118
+ if (isSingleFileInput(value))
119
+ return { kind: 'script' };
120
+ if (isManifestCollection(value))
121
+ return { kind: 'manifest' };
122
+ if (isGithubDirInput(value))
123
+ return { kind: 'directory' };
124
+ return { kind: 'invalid', message: HTTP_URL_NOT_A_COLLECTION };
125
+ }
126
+ // Local. Probe through the canonical URL rather than a filesystem
127
+ // path: `locationFsPath` mangles the Windows shapes `pathToFileUrl`
128
+ // can't rebuild, and a URL the user typed is already in the form the
129
+ // read wants.
130
+ // Safe to strip: the shape check above refused everything that
131
+ // collapses to `/`, so this can't eat `file://`'s own slashes.
132
+ const url = collectionCanonicalUrl(value).replace(/\/+$/, '');
133
+ // Directory first: Chrome serves a listing for a directory URL with
134
+ // OR without the trailing slash, so a plain read can't tell the two
135
+ // apart — but only a directory answers the slashed form at all.
136
+ if (await canReadFileUrl(url + '/'))
137
+ return { kind: 'directory' };
138
+ if (await canReadFileUrl(url)) {
139
+ if (isSingleFileInput(value))
140
+ return { kind: 'script' };
141
+ if (isManifestCollection(value))
142
+ return { kind: 'manifest' };
143
+ return { kind: 'invalid', message: UNSUPPORTED_LOCATION_FILE };
144
+ }
145
+ // Which of the two reasons it is IS knowable, so the message says
146
+ // which rather than listing both: with file access off, every local
147
+ // path reads as missing, and "check that the path exists" would send
148
+ // the user hunting for a file that is sitting right there.
149
+ const fileAccess = await isFileSchemeAccessAllowed();
150
+ return {
151
+ kind: 'invalid',
152
+ message: locationNotReadableMessage(canonicalCollectionInput(value), fileAccess),
153
+ };
154
+ }
155
+ // GitHub-specific collection handling lives in `github-provider.ts`. We
156
+ // re-export only the symbols `script-store.ts` and `options.ts` use
157
+ // directly. Callers always go through `normalizeCollectionInput`; today
158
+ // that's just `normalizeGithubInput` under its public name, but
159
+ // keeping the public name generic lets future providers slot in
160
+ // without changing the callers.
161
+ export { isGithubDirCollection } from './github-provider.js';
162
+ export { normalizeGithubInput as normalizeCollectionInput } from './github-provider.js';
163
+ import { githubDirScriptFileUrl, githubDirScriptDisplayUrl, isGithubDirCollection, isGithubDirInput, scanGithubDir, } from './github-provider.js';
164
+ import { githubRawToBlobUrl } from '../github-urls.js';
165
+ export { githubRawToBlobUrl } from '../github-urls.js';
166
+ import { githubAuthHeaders } from '../github-auth.js';
167
+ import { decodeForDisplay, leafName } from '../script-display.js';
168
+ import { mustHopToWorker, readHttpViaWorker } from '../http-read.js';
169
+ import { MarkupResponseError, isMarkupContentType, looksLikeMarkup, markupResponseMessage, } from '../markup-response.js';
170
+ import { baseName, canonicalCollectionInput, collectionCanonicalUrl, isContainerCollection, isFileUrl, isHttpUrl, isManifestCollection, isSingleFileInput, joinPath, listDirectory, LOCATION_SHAPES, locationFsPath, NOT_A_DIRECTORY_LISTING, parentDir, pathToFileUrl, readFileUrlText, readFileUrlWithStat, statFromListing, urlPathname, } from '../file-locations.js';
171
+ // Location and listing helpers, re-exported for the callers that reach
172
+ // them through here.
173
+ export * from '../file-locations.js';
174
+ // Scratch-script and sm-dev collection naming, likewise.
175
+ export * from './location-kinds.js';
176
+ import { isScratchUrl, scratchTextKey } from './location-kinds.js';
177
+ // Leaf name (last path segment) of an http(s) URL, percent-decoded —
178
+ // used as the FoundScript.name for HTTP manifest entries so the
179
+ // collections page shows "foo.user.js" rather than the whole URL.
180
+ // Returns '' for anything that doesn't parse as a URL, which callers
181
+ // use as their "fall back to the raw entry" signal.
182
+ export function urlBaseName(input) {
183
+ return decodeForDisplay(leafName(urlPathname(input)));
184
+ }
185
+ async function fetchScratchText(url) {
186
+ const got = await chrome.storage.local.get(scratchTextKey(url));
187
+ const text = got[scratchTextKey(url)];
188
+ if (typeof text !== 'string')
189
+ throw new Error(`Could not read ${url}: The scratch script's text was cleared. Push it again.`);
190
+ return text;
191
+ }
192
+ // ---------- File / directory fetch ----------
193
+ export function fetchFileUrlText(url) {
194
+ if (isScratchUrl(url))
195
+ return fetchScratchText(url);
196
+ return readFileUrlText(url);
197
+ }
198
+ // `fetchFileUrlText` for text that is about to be treated as a script.
199
+ //
200
+ // The markup guard can't live in `fetchFileUrlText` itself: among its
201
+ // other callers is the one that reads Chrome's `file://` directory
202
+ // listing, which is HTML *by design* (`parseListing`). So the two
203
+ // script-body call sites — the refresh pipeline and the preview —
204
+ // share this wrapper instead.
205
+ //
206
+ // What it catches locally: an `.user.js` path that is really a
207
+ // directory (Chrome answers with a generated listing), and an HTML
208
+ // file that got the suffix by mistake. The `file://` legs often report
209
+ // no Content-Type at all, so the body test is the only one available.
210
+ export async function fetchScriptFileUrlText(url) {
211
+ return refuseMarkup(url, await fetchFileUrlText(url));
212
+ }
213
+ // `fetchScriptFileUrlText` for a `file://` script, with the size and
214
+ // modified time a listing of its folder would give. See
215
+ // `readFileUrlWithStat`.
216
+ export async function fetchScriptFileWithStat(url) {
217
+ const read = await readFileUrlWithStat(url);
218
+ refuseMarkup(url, read.text);
219
+ return read;
220
+ }
221
+ function refuseMarkup(url, text) {
222
+ if (looksLikeMarkup(text)) {
223
+ // No `Could not read ${url}: ` prefix, unlike its siblings here:
224
+ // the local wording already names the file, and the prefix would
225
+ // print the same URL twice in one sentence.
226
+ throw new MarkupResponseError(markupResponseMessage({ localUrl: url }));
227
+ }
228
+ return text;
229
+ }
230
+ // Best-effort "can this file:// URL be read" probe. Returns false on
231
+ // any error (including non-existence and permission failures) —
232
+ // callers should only rely on a true result.
233
+ async function canReadFileUrl(url) {
234
+ try {
235
+ await fetchFileUrlText(url);
236
+ return true;
237
+ }
238
+ catch {
239
+ return false;
240
+ }
241
+ }
242
+ // Best-effort "does the path point to a readable file" probe. Used to
243
+ // turn the otherwise-cryptic "Could not read …/" message into a
244
+ // clearer one when the user types a path to an existing non-userscript
245
+ // file.
246
+ async function isReadableFile(absolutePath) {
247
+ return await canReadFileUrl(pathToFileUrl(absolutePath));
248
+ }
249
+ // Whether an input names one script file, checked against the raw
250
+ // input before any canonicalization: an http(s) or file:// URL, or an
251
+ // absolute local path — the same shapes the collections page's Add
252
+ // field accepts for a single script. Checked before canonicalizing
253
+ // because `collectionCanonicalUrl` turns anything else into a file://
254
+ // URL, and an `ftp://…` would otherwise fail with a baffling
255
+ // `file:///ftp%3A//…` error instead of being named.
256
+ export function isSingleFileLocation(input) {
257
+ return isHttpUrl(input) || isFileUrl(input) || input.startsWith('/');
258
+ }
259
+ // The text at a single-file location — the two shapes one can take once
260
+ // canonicalized, http(s) or file://. What the install preview fetches,
261
+ // and what the compare page fetches to stand a download against an
262
+ // installed copy.
263
+ export async function fetchSingleFileText(url) {
264
+ if (isFileUrl(url))
265
+ return fetchScriptFileUrlText(url);
266
+ const result = await fetchHttpText(url);
267
+ // `unchanged` needs a conditional request to happen, and we send
268
+ // none — but the type includes it, so fail loudly rather than
269
+ // rendering an empty page if that ever changes.
270
+ if (result.kind !== 'ok')
271
+ throw new Error('No content returned');
272
+ return result.text;
273
+ }
274
+ // Fetch an http(s) URL, optionally with conditional headers. A 304
275
+ // response surfaces as `{kind: 'unchanged'}` so callers can preserve
276
+ // the prior content without re-parsing. We use `cache: 'no-store'`
277
+ // because we maintain our own validators (etag / lastModified) on each
278
+ // script row — the browser HTTP cache would otherwise serve a stale
279
+ // 200 without ever asking the server.
280
+ export async function fetchHttpText(url, opts = {}) {
281
+ const headers = {};
282
+ if (opts.etag)
283
+ headers['If-None-Match'] = opts.etag;
284
+ if (opts.lastModified)
285
+ headers['If-Modified-Since'] = opts.lastModified;
286
+ // Page contexts hand the read to the service worker: a document that
287
+ // fetches for itself acts on the response's `Link: rel=preload`
288
+ // headers, and a login page — the case the markup check below exists
289
+ // for — is exactly the kind of response that sends them. See
290
+ // src/http-read.ts.
291
+ if (mustHopToWorker()) {
292
+ const reply = await readHttpViaWorker({ url, as: 'text', headers });
293
+ if (reply) {
294
+ if (reply.kind === 'error') {
295
+ throw new Error(`Could not read ${url}: ${reply.message ?? reply.reason}`);
296
+ }
297
+ return interpretHttpText(url, opts, {
298
+ status: reply.status,
299
+ contentType: reply.contentType,
300
+ etag: reply.etag,
301
+ lastModified: reply.lastModified,
302
+ text: reply.text ?? '',
303
+ });
304
+ }
305
+ // No answer at all (worker evicted mid-message, extension
306
+ // reloading). Falling through reads it here, which beats failing a
307
+ // read the user asked for.
308
+ }
309
+ // Attach the configured GitHub token when the URL targets a GitHub
310
+ // host. No-op for any other URL. Lets private-repo raw URLs and
311
+ // private repo manifests fetch under auth without each call site
312
+ // having to opt in. Header host-gating is enforced inside
313
+ // githubAuthHeaders so a token can never be leaked to a non-GitHub
314
+ // host. (Over the hop the worker attaches the same headers from its
315
+ // own copy of the token, so it never rides on a message.)
316
+ Object.assign(headers, githubAuthHeaders(url));
317
+ let response;
318
+ try {
319
+ response = await fetch(url, { cache: 'no-store', headers });
320
+ }
321
+ catch (err) {
322
+ throw new Error(`Could not read ${url}: ${err instanceof Error ? err.message : String(err)}`);
323
+ }
324
+ if (response.status === 304)
325
+ return { kind: 'unchanged' };
326
+ return interpretHttpText(url, opts, {
327
+ status: response.status,
328
+ contentType: response.headers.get('content-type') ?? '',
329
+ etag: response.headers.get('ETag'),
330
+ lastModified: response.headers.get('Last-Modified'),
331
+ text: await response.text(),
332
+ });
333
+ }
334
+ // What the response MEANS, applied identically to a response we read
335
+ // here and to one the worker read for us — so the two paths can't come
336
+ // to disagree about which statuses fail or which bodies are markup.
337
+ //
338
+ // A 304 never reaches here from the local path (it has no body to
339
+ // read); over the hop it arrives as an ordinary status and is answered
340
+ // the same way.
341
+ function interpretHttpText(url, opts, res) {
342
+ if (res.status === 304)
343
+ return { kind: 'unchanged' };
344
+ if (res.status < 200 || res.status > 299) {
345
+ throw new Error(`Could not read ${url}: HTTP ${res.status}`);
346
+ }
347
+ // Every caller of this treats the result as JavaScript (a script
348
+ // body) or JSON (a manifest); neither can be markup. Rejecting here
349
+ // rather than letting it through is what keeps a refresh that lands
350
+ // on a login page from replacing a working script with a page of
351
+ // HTML — a fetch failure preserves the prior copy, a "successful"
352
+ // fetch of garbage does not.
353
+ //
354
+ // The header is quoted verbatim, not lowercased: it is evidence, and
355
+ // evidence should read as what the server actually sent.
356
+ const markupHeader = isMarkupContentType(res.contentType);
357
+ if (markupHeader || looksLikeMarkup(res.text)) {
358
+ throw new MarkupResponseError(`Could not read ${url}: ${markupResponseMessage({
359
+ ...(markupHeader ? { contentType: res.contentType } : {}),
360
+ expected: opts.expects ?? 'JavaScript',
361
+ })}`);
362
+ }
363
+ const result = { kind: 'ok', text: res.text };
364
+ if (res.etag)
365
+ result.etag = res.etag;
366
+ if (res.lastModified)
367
+ result.lastModified = res.lastModified;
368
+ return result;
369
+ }
370
+ // ---------- Scanning ----------
371
+ // Parse a manifest body. Accepts either a bare JSON array or
372
+ // `{scripts: [...]}`. Anything else is rejected with a message that
373
+ // points at the supported shapes. Empty strings inside the list are
374
+ // dropped so a trailing newline / typo doesn't fail the whole source.
375
+ //
376
+ // List elements are either a URL string or an object with a `path`
377
+ // holding that string. The object form lets a manifest carry extra
378
+ // per-script bookkeeping (author's notes, tags, …) alongside the URL;
379
+ // every field other than `path` is ignored here, so unknown keys are
380
+ // forward-compatible rather than an error.
381
+ export function parseManifest(body) {
382
+ let parsed;
383
+ try {
384
+ parsed = JSON.parse(body);
385
+ }
386
+ catch (err) {
387
+ throw new Error(`Manifest is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
388
+ }
389
+ let arr;
390
+ if (Array.isArray(parsed)) {
391
+ arr = parsed;
392
+ }
393
+ else if (parsed && typeof parsed === 'object' && Array.isArray(parsed.scripts)) {
394
+ arr = parsed.scripts;
395
+ }
396
+ else {
397
+ throw new Error('Manifest must be a JSON array of entries, or `{"scripts": [...]}`');
398
+ }
399
+ const entries = [];
400
+ for (const item of arr) {
401
+ let value;
402
+ if (typeof item === 'string') {
403
+ value = item;
404
+ }
405
+ else if (item && typeof item === 'object' && !Array.isArray(item)) {
406
+ // An array is also `typeof 'object'`, hence the isArray guard —
407
+ // otherwise `[["a.user.js"]]` would look for a `path` field on it.
408
+ const entryPath = item.path;
409
+ if (typeof entryPath !== 'string') {
410
+ throw new Error('Manifest entry objects must have a "path" string');
411
+ }
412
+ value = entryPath;
413
+ }
414
+ else {
415
+ throw new Error('Manifest entries must all be URL strings or objects with a "path"');
416
+ }
417
+ const trimmed = value.trim();
418
+ if (trimmed)
419
+ entries.push(trimmed);
420
+ }
421
+ return entries;
422
+ }
423
+ export async function scanForUserScripts(absolutePath, depth = 0, prefix = '',
424
+ // Listings to share with the caller — a manifest refresh stats the
425
+ // manifest file from the same folder its entries usually sit in.
426
+ listings) {
427
+ // Collection-kind dispatch. Container collections don't have a
428
+ // scannable backing store — refreshCollection builds a synthetic scan
429
+ // from the existing scriptIds and never calls this for them. The
430
+ // remaining branches are scheme-agnostic (raw path / file:// /
431
+ // http(s):// handled uniformly); local directory is the only branch
432
+ // that rejects HTTP, because there's no remote listing API.
433
+ if (depth === 0) {
434
+ if (isContainerCollection(absolutePath)) {
435
+ throw new Error('Internal error: this input kind is refreshed directly, not scanned');
436
+ }
437
+ if (isManifestCollection(absolutePath)) {
438
+ return scanManifest(collectionCanonicalUrl(absolutePath), listings);
439
+ }
440
+ if (isGithubDirCollection(absolutePath)) {
441
+ return scanGithubDir(absolutePath);
442
+ }
443
+ if (isHttpUrl(absolutePath)) {
444
+ // No mention of "container collections": that's how we store a
445
+ // stand-alone script, and the reader adding a URL has no name for
446
+ // it — from the page it just looks like adding the script itself.
447
+ throw new Error(HTTP_URL_NOT_A_COLLECTION);
448
+ }
449
+ // Local-directory only. Normalise file:// → raw path once so the
450
+ // downstream listing + recursion all see a single form.
451
+ absolutePath = locationFsPath(absolutePath);
452
+ }
453
+ // `readonly` because a listing can be batch-shared — see
454
+ // `DirectoryListingCache`. The scan only reads it.
455
+ let entries;
456
+ try {
457
+ entries = await listDirectory(absolutePath);
458
+ }
459
+ catch (err) {
460
+ // Common foot-gun at the top level: the user types a path to a file
461
+ // whose name doesn't end with `.user.js` (e.g. `notes.md`). The
462
+ // listing fetch above asks Chrome for `file://…/notes.md/`, which
463
+ // produces an opaque "could not read" message. Probe by reading the
464
+ // path as a plain file; if that succeeds we know the path exists
465
+ // but isn't a userscript, so swap in a clearer message.
466
+ // Not when the listing was refused for its shape: Chrome serves a
467
+ // listing for a directory URL without the slash too, so the probe
468
+ // would "succeed" on a real folder and blame the user's path.
469
+ const shapeRefused = err instanceof Error && err.message.includes(NOT_A_DIRECTORY_LISTING);
470
+ if (depth === 0 && !shapeRefused && await isReadableFile(absolutePath)) {
471
+ throw new Error(`${absolutePath} is a file, not a folder, and its name doesn't end in .user.js`);
472
+ }
473
+ throw err;
474
+ }
475
+ const results = [];
476
+ const subdirs = [];
477
+ for (const entry of entries) {
478
+ if (!entry.isDir && entry.name.endsWith('.user.js')) {
479
+ const found = { name: entry.name, relativePath: prefix + entry.name };
480
+ if (entry.size !== undefined)
481
+ found.size = entry.size;
482
+ if (entry.mtime !== undefined)
483
+ found.mtime = entry.mtime;
484
+ results.push(found);
485
+ }
486
+ else if (entry.isDir && depth < MAX_SCAN_DEPTH) {
487
+ subdirs.push(entry);
488
+ }
489
+ }
490
+ // Sibling directories are independent, so they are listed together
491
+ // rather than one after another. This used to be an `await` inside the
492
+ // loop, which made the scan cost one round trip per directory however
493
+ // shallow the tree — the dominant phase of a refresh once the other
494
+ // fan-outs were fixed.
495
+ //
496
+ // Rejection still fails the whole scan, as it did: the caller turns
497
+ // that into the collection's `scanError`, and a listing we couldn't
498
+ // read means we don't know what's there.
499
+ const nested = await mapWithConcurrency(subdirs, SCAN_DIRS_CONCURRENCY, (entry) => scanForUserScripts(joinPath(absolutePath, entry.name), depth + 1, prefix + entry.name + '/'));
500
+ for (const sub of nested)
501
+ results.push(...sub);
502
+ // Sorted here, so the concurrent walk above can finish in any order
503
+ // without changing what the caller sees.
504
+ results.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
505
+ return results;
506
+ }
507
+ // ---------- Per-collection-kind scanners ----------
508
+ // Scheme-aware text fetcher used for the manifest body. Per-entry
509
+ // fetches (with their own per-scheme dispatch and conditional-GET
510
+ // handling) live in script-store.ts; this helper only needs to read
511
+ // a body, not negotiate freshness.
512
+ async function fetchManifestText(canonicalUrl) {
513
+ if (isHttpUrl(canonicalUrl)) {
514
+ const fetched = await fetchHttpText(canonicalUrl, { expects: 'JSON' });
515
+ if (fetched.kind !== 'ok') {
516
+ // Unreachable in practice: this call sends no conditional-GET
517
+ // headers, so `fetchHttpText` can only answer `ok` or throw.
518
+ throw new Error(`Internal error: unexpected result reading the manifest (${fetched.kind})`);
519
+ }
520
+ return fetched.text;
521
+ }
522
+ // file:// URLs (the only other scheme we accept here — pathToFileUrl
523
+ // wrapped any raw path before reaching us).
524
+ return await fetchFileUrlText(canonicalUrl);
525
+ }
526
+ // Fetch the manifest and turn each listed entry into a FoundScript.
527
+ // Works for any manifest scheme — http(s):// or file:// — because both
528
+ // share `new URL(entry, base)` resolution semantics and the only piece
529
+ // that differs is the manifest-body fetch.
530
+ //
531
+ // `relativePath` stores the raw entry (typically a bare filename for
532
+ // relative cases) so it survives across refreshes for stable
533
+ // per-script IDs; the per-entry fetcher resolves it back to an
534
+ // absolute URL via scriptFileUrl. Duplicate entries collapse to the
535
+ // first occurrence — the manifest is the user's source of truth, so
536
+ // we treat dupes as a harmless typo rather than an error.
537
+ //
538
+ // The manifest body itself is re-downloaded every refresh (no
539
+ // If-None-Match round-trip on the manifest URL). The per-script fetches
540
+ // still 304 individually, so the bandwidth cost is bounded. See TODO.md
541
+ // "HTTP collections — follow-ups" for the planned fix. Schemes a
542
+ // manifest entry's *resolved* URL is allowed to use. We mirror the
543
+ // suffix-test policy on the collection URL itself (which only accepts
544
+ // http(s) / file: / raw path): a manifest the user added shouldn't be
545
+ // able to escalate the SW's `<all_urls>` reach into things like `data:`
546
+ // (arbitrary inline content), `chrome-extension:` (cross-extension
547
+ // probing), or `blob:` (off-origin smuggling). The user can always add
548
+ // such collections directly if they want them.
549
+ const ALLOWED_ENTRY_SCHEMES = new Set(['http:', 'https:', 'file:']);
550
+ // Resolve a manifest entry against the manifest's canonical URL.
551
+ //
552
+ // On top of plain RFC 3986 resolution, if the entry doesn't bring its
553
+ // own query string and lands in the same origin as the manifest, the
554
+ // manifest's query string is propagated onto the resolved URL. This
555
+ // lets a single auth-bearing manifest URL (e.g. GitHub's
556
+ // `…/script_manifest.json?token=…`) reach its companion scripts
557
+ // without each manifest entry having to repeat the token. Cross-origin
558
+ // or self-querying entries are left alone — the propagation rule only
559
+ // fires when the entry is clearly "another file alongside the
560
+ // manifest". Throws on a malformed entry; the caller layers
561
+ // scheme-gating on top.
562
+ export function resolveManifestEntryUrl(entry, canonicalUrl) {
563
+ const base = new URL(canonicalUrl);
564
+ let resolved;
565
+ try {
566
+ resolved = new URL(entry, canonicalUrl);
567
+ }
568
+ catch {
569
+ throw new Error(`Manifest entry is not a valid URL: ${entry}`);
570
+ }
571
+ if (!resolved.search && base.search && resolved.origin === base.origin) {
572
+ resolved.search = base.search;
573
+ }
574
+ return resolved;
575
+ }
576
+ async function scanManifest(canonicalUrl,
577
+ // One listing per folder the entries sit in, for the stat below.
578
+ listings = new Map()) {
579
+ const body = await fetchManifestText(canonicalUrl);
580
+ const rawEntries = parseManifest(body);
581
+ const seen = new Set();
582
+ const results = [];
583
+ for (const entry of rawEntries) {
584
+ if (seen.has(entry))
585
+ continue;
586
+ seen.add(entry);
587
+ // Resolve once: validates the URL, gates the scheme, and gives us a
588
+ // friendly display name. The actual fetch URL is rebuilt at refresh
589
+ // time via scriptFileUrl from collectionCanonicalUrl + entry.
590
+ const resolved = resolveManifestEntryUrl(entry, canonicalUrl);
591
+ if (!ALLOWED_ENTRY_SCHEMES.has(resolved.protocol)) {
592
+ throw new Error(`Manifest entry scheme ${resolved.protocol} is not allowed (use http, https, or file)`);
593
+ }
594
+ // Use urlBaseName on the resolved URL so percent-encoded segments
595
+ // come out human-readable, mirroring scanSingleFile's HTTP path.
596
+ const name = urlBaseName(resolved.href) || entry;
597
+ const found = { name, relativePath: entry };
598
+ // A `file://` entry gets the same listing stat a directory scan
599
+ // gives its files, so the refresh can skip an unchanged one and a
600
+ // page can tell an edited one (file-changes.ts). Best effort: an
601
+ // entry whose folder won't list just stays always-fetch.
602
+ if (resolved.protocol === 'file:') {
603
+ const stat = await statFileByListing(locationFsPath(resolved.href), listings);
604
+ if (stat?.size !== undefined)
605
+ found.size = stat.size;
606
+ if (stat?.mtime !== undefined)
607
+ found.mtime = stat.mtime;
608
+ }
609
+ results.push(found);
610
+ }
611
+ return results;
612
+ }
613
+ // The listing entry for one file by absolute path — its parent folder
614
+ // listed once per `cache` — or null when it isn't listed or the folder
615
+ // can't be read.
616
+ export async function statFileByListing(absolutePath, cache) {
617
+ try {
618
+ return await statFromListing(parentDir(absolutePath), baseName(absolutePath), cache);
619
+ }
620
+ catch {
621
+ return null;
622
+ }
623
+ }
624
+ // Build the fetchable URL for a specific scanned script. One branch per
625
+ // collection-kind (container, manifest, github-dir, directory); each is
626
+ // scheme-agnostic and works for raw paths, file:// URLs, and http(s)
627
+ // URLs uniformly via `collectionCanonicalUrl` / `locationFsPath`.
628
+ //
629
+ // Why directory uses path-joining rather than URL resolution: the
630
+ // `relativePath` for a local directory comes from Chrome's listing
631
+ // verbatim (e.g. `weird?name.user.js`), and URL resolution would treat
632
+ // `?...` as a query string. `pathToFileUrl` percent-encodes per
633
+ // segment, which survives those characters correctly. Manifest entries
634
+ // are user-authored and expected to already be properly URL-encoded
635
+ // when they contain special characters.
636
+ export function scriptFileUrl(collectionPath, relativePath) {
637
+ if (isContainerCollection(collectionPath)) {
638
+ // Container scripts carry the full URL/path the user added in
639
+ // their `relativePath`. Coerce raw fs paths to file:// URLs so the
640
+ // downstream fetcher sees a uniform URL form.
641
+ if (isHttpUrl(relativePath) || isFileUrl(relativePath) || isScratchUrl(relativePath))
642
+ return relativePath;
643
+ return pathToFileUrl(relativePath);
644
+ }
645
+ if (isManifestCollection(collectionPath)) {
646
+ return resolveManifestEntryUrl(relativePath, collectionCanonicalUrl(collectionPath)).href;
647
+ }
648
+ if (isGithubDirCollection(collectionPath)) {
649
+ return githubDirScriptFileUrl(collectionPath, relativePath);
650
+ }
651
+ // Local directory.
652
+ return pathToFileUrl(joinPath(locationFsPath(collectionPath), relativePath));
653
+ }
654
+ // Human-displayable location for a specific scanned script. Three
655
+ // distinct shapes:
656
+ // - file:// → bare fs path (more readable than the percent-encoded
657
+ // URL form).
658
+ // - github (github-dir collection, github-manifest collection, or
659
+ // any github URL added as a script inside the github container) →
660
+ // `github.com/<o>/<r>/blob/<ref>/<path>` page URL. The raw URL is
661
+ // what we fetch from; the blob URL is what a human would open in
662
+ // a browser.
663
+ // - non-github http(s):// → URL verbatim.
664
+ // This is what the viewer's Location row shows for every collection kind
665
+ // without each collection-kind needing its own special case.
666
+ export function scriptAbsolutePath(collectionPath, relativePath) {
667
+ if (isGithubDirCollection(collectionPath)) {
668
+ return githubDirScriptDisplayUrl(collectionPath, relativePath);
669
+ }
670
+ const url = scriptFileUrl(collectionPath, relativePath);
671
+ // GitHub manifest entries and github-container scripts are stored as
672
+ // raw URLs (after `normalizeGithubInput`). Convert back to the github
673
+ // page URL so the Location row matches a github-dir collection's
674
+ // display.
675
+ const blobUrl = githubRawToBlobUrl(url);
676
+ if (blobUrl)
677
+ return blobUrl;
678
+ return isFileUrl(url) ? locationFsPath(url) : url;
679
+ }