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,48 @@
1
+ export type ResolvedKind = 'http' | 'file' | 'data';
2
+ export interface ResolvedResource {
3
+ kind: ResolvedKind;
4
+ url: string;
5
+ }
6
+ export type ResolveFailReason = 'empty' | 'malformed-url' | 'unsupported-scheme' | 'outside-script-folder' | 'relative-in-scratch';
7
+ export type ResolveFailure = {
8
+ failed: true;
9
+ reason: Exclude<ResolveFailReason, 'unsupported-scheme'>;
10
+ } | {
11
+ failed: true;
12
+ reason: 'unsupported-scheme';
13
+ scheme: string;
14
+ };
15
+ export declare function resolveScriptResource(ref: string, scriptFileUrl: string): ResolvedResource | null;
16
+ export declare function resolveScriptResourceDetailed(ref: string, scriptFileUrl: string): ResolvedResource | ResolveFailure;
17
+ export interface FetchBytesOptions {
18
+ maxBytes: number;
19
+ timeoutMs?: number;
20
+ acceptContentType?: (contentType: string) => boolean;
21
+ conditional?: {
22
+ etag?: string;
23
+ lastModified?: string;
24
+ };
25
+ }
26
+ export interface FetchedBytes {
27
+ body: ArrayBuffer;
28
+ contentType: string;
29
+ finalUrl: string;
30
+ etag?: string;
31
+ lastModified?: string;
32
+ fsMtime?: number;
33
+ }
34
+ export interface NotModified {
35
+ notModified: true;
36
+ }
37
+ export declare function isNotModified(v: FetchedBytes | NotModified | FetchFailure): v is NotModified;
38
+ export type FetchFailReason = 'unsupported-scheme' | 'no-response' | 'timeout' | 'unreadable-file' | 'http-error' | 'wrong-content-type' | 'empty' | 'oversize';
39
+ export type FetchFailure = {
40
+ failed: true;
41
+ reason: Exclude<FetchFailReason, 'http-error'>;
42
+ } | {
43
+ failed: true;
44
+ reason: 'http-error';
45
+ status: number;
46
+ };
47
+ export declare function fetchResourceBytes(url: string, opts: FetchBytesOptions): Promise<FetchedBytes | NotModified | FetchFailure>;
48
+ export declare function fetchResourceText(url: string, opts: FetchBytesOptions): Promise<string | null>;
@@ -0,0 +1,436 @@
1
+ // Resolution + loading of resources referenced from a userscript
2
+ // metadata block — e.g. `@icon` today, `@require` planned. Centralizes
3
+ // the scheme-dispatch + relative-path-against-the-script rule so
4
+ // each consumer doesn't reinvent it.
5
+ //
6
+ // Reference forms accepted by `resolveScriptResource`:
7
+ // - absolute http(s):// URL → kind 'http' (passed through verbatim;
8
+ // any origin is allowed — the script
9
+ // author explicitly pointed there)
10
+ // - `data:` URL → kind 'data' (caller handles inline;
11
+ // not fetched here)
12
+ // - relative path → resolved against `scriptFileUrl`.
13
+ // Against a *local* (file://) script the
14
+ // result must still live at or under the
15
+ // script's own directory (containment
16
+ // check on the resolved URL). Against an
17
+ // http(s) script it may climb — see
18
+ // "Containment is about local files"
19
+ // below.
20
+ //
21
+ // Notably absent: absolute `file://` URLs. The extension holds
22
+ // `file:///*` host permission so any caller code can read arbitrary
23
+ // local files via the byte fetcher; we don't want a malicious
24
+ // HTTP-sourced userscript pointing `@icon file:///etc/passwd` and
25
+ // having us inline a host file into the collections-page DOM. A file://
26
+ // result is only reachable as the *resolution* of a relative path
27
+ // against a file:// script, in which case the containment check
28
+ // confines it to the script's own directory tree.
29
+ //
30
+ // Containment is about local files, so it applies only to a local
31
+ // base. A `../lib/util.js` next to a published script is the ordinary
32
+ // layout of a userscript repo served over HTTP, and Violentmonkey
33
+ // resolves it (`vetUrl` in `src/background/utils/url.js`, applied by
34
+ // `buildPathMap`) with no containment at all. Refusing it bought
35
+ // nothing: an http(s)-loaded script may already name an absolute URL on
36
+ // any origin, so climbing out of its own directory grants it no reach
37
+ // it didn't have. Against a `file://` script the same move would reach
38
+ // out of the script's tree into the rest of the disk, which is exactly
39
+ // what the rule exists to stop — there, the sanctioned way to reach a
40
+ // sibling `lib/` is a search root the user named (see
41
+ // `local-file-overrides.ts`).
42
+ //
43
+ // Containment check vs. textual `..` filter: a textual filter on the
44
+ // raw reference can be bypassed by percent-encoding (`%2e%2e` is
45
+ // normalized into a `..` directory-up move by the URL parser before
46
+ // we ever see the resolved form). Comparing the resolved URL string
47
+ // against the parent-directory URL string catches every encoding —
48
+ // literal `..`, `%2e%2e`, leading `/`, etc. — in one rule.
49
+ //
50
+ // The host script's parent dir is derived from `scriptFileUrl`, which
51
+ // is the canonical fetchable URL we already store on every script
52
+ // record — works uniformly for file://, http(s)://, and entries
53
+ // reached via a manifest.
54
+ import { fsMtimeFromLastModified, isFileUrl, isHttpUrl } from './file-locations.js';
55
+ import { githubAuthHeaders } from './github-auth.js';
56
+ import { mustHopToWorker, readHttpViaWorker, replyBytes } from './http-read.js';
57
+ const ABSOLUTE_URL_RE = /^[a-z][a-z0-9+.-]*:/i;
58
+ // Yes/no form, for the callers that only need a URL or nothing —
59
+ // syntax highlighting's link detection, the icon resolver, the corpus
60
+ // validator. A thin wrapper rather than a second implementation: the
61
+ // two used to be one function and the rules must not drift.
62
+ export function resolveScriptResource(ref, scriptFileUrl) {
63
+ const result = resolveScriptResourceDetailed(ref, scriptFileUrl);
64
+ return 'failed' in result ? null : result;
65
+ }
66
+ // Same rules, but says which one rejected the ref. Used where the
67
+ // answer becomes a message a user reads.
68
+ export function resolveScriptResourceDetailed(ref, scriptFileUrl) {
69
+ const trimmed = ref.trim();
70
+ if (!trimmed)
71
+ return { failed: true, reason: 'empty' };
72
+ if (trimmed.startsWith('data:')) {
73
+ return { kind: 'data', url: trimmed };
74
+ }
75
+ // Absolute http(s):// URL: the script author wrote a complete URL,
76
+ // any origin is allowed. Note that absolute `file://` is *not*
77
+ // accepted here — see module header for why.
78
+ if (ABSOLUTE_URL_RE.test(trimmed)) {
79
+ let parsed;
80
+ try {
81
+ parsed = new URL(trimmed);
82
+ }
83
+ catch {
84
+ return { failed: true, reason: 'malformed-url' };
85
+ }
86
+ if (parsed.protocol === 'http:' || parsed.protocol === 'https:') {
87
+ return { kind: 'http', url: parsed.toString() };
88
+ }
89
+ return { failed: true, reason: 'unsupported-scheme', scheme: parsed.protocol };
90
+ }
91
+ // Relative path. Resolve against the script's own URL. Against a
92
+ // local script the result must still be confined to the script's own
93
+ // directory tree; against a remote one it need not (see the module
94
+ // header). We compare URL strings rather than path strings so the
95
+ // check accounts for origin, scheme, host, and port atomically — the
96
+ // resolved URL must share the same prefix as `<parentBase>/`.
97
+ let resolved;
98
+ let parentBase;
99
+ try {
100
+ resolved = new URL(trimmed, scriptFileUrl);
101
+ parentBase = new URL('./', scriptFileUrl);
102
+ }
103
+ catch {
104
+ return { failed: true, reason: 'malformed-url' };
105
+ }
106
+ // `parentBase` carries the base's scheme, so it answers "is the host
107
+ // script remote?" without a third parse of the same string.
108
+ const remoteBase = parentBase.protocol === 'http:' || parentBase.protocol === 'https:';
109
+ const parentStr = parentBase.toString();
110
+ const resolvedStr = resolved.toString();
111
+ if (!remoteBase && !resolvedStr.startsWith(parentStr)) {
112
+ return { failed: true, reason: 'outside-script-folder' };
113
+ }
114
+ if (resolved.protocol === 'http:' || resolved.protocol === 'https:') {
115
+ return { kind: 'http', url: resolvedStr };
116
+ }
117
+ if (resolved.protocol === 'file:') {
118
+ return { kind: 'file', url: resolvedStr };
119
+ }
120
+ // A scratch script (`scratch:///name.user.js`, see directories.ts) is
121
+ // stored as text alone, so it has no folder a relative path could name.
122
+ if (resolved.protocol === 'scratch:')
123
+ return { failed: true, reason: 'relative-in-scratch' };
124
+ // A relative ref that lands on some other scheme — only reachable
125
+ // when the script's own URL has one, which the collection pipeline
126
+ // doesn't produce today.
127
+ return { failed: true, reason: 'unsupported-scheme', scheme: resolved.protocol };
128
+ }
129
+ // ---------- Bounded byte fetch ----------
130
+ //
131
+ // Scheme-aware bounded fetcher used by resource loaders. http(s) goes
132
+ // through `fetch()`; file:// goes through XMLHttpRequest in page
133
+ // contexts (where `fetch()` would return an opaque chrome-extension →
134
+ // file:// response) and falls back to `fetch()` in service-worker
135
+ // contexts (no XHR). Both legs apply the same size cap and timeout.
136
+ const DEFAULT_FETCH_TIMEOUT_MS = 5000;
137
+ export function isNotModified(v) {
138
+ return v.notModified === true;
139
+ }
140
+ const fetchFailed = (reason) => ({ failed: true, reason });
141
+ const httpError = (status) => ({ failed: true, reason: 'http-error', status });
142
+ export async function fetchResourceBytes(url, opts) {
143
+ if (isHttpUrl(url))
144
+ return fetchHttpBytes(url, opts);
145
+ if (isFileUrl(url))
146
+ return fetchFileBytes(url, opts);
147
+ return fetchFailed('unsupported-scheme');
148
+ }
149
+ // Page contexts hand the read to the service worker — a document
150
+ // fetch acts on the response's `Link: rel=preload` headers, which our
151
+ // pages can't have. See src/http-read.ts for the whole story.
152
+ async function fetchHttpBytes(url, opts) {
153
+ if (mustHopToWorker()) {
154
+ const viaSw = await fetchHttpBytesViaWorker(url, opts);
155
+ if (viaSw)
156
+ return viaSw;
157
+ }
158
+ return fetchHttpBytesHere(url, opts);
159
+ }
160
+ // Map the worker's report of what came back onto this module's own
161
+ // verdict about it — which statuses are failures, which content types
162
+ // are acceptable, what counts as too big. Returns null when the worker
163
+ // never answered, so the caller falls back to fetching here.
164
+ async function fetchHttpBytesViaWorker(url, opts) {
165
+ const reply = await readHttpViaWorker({
166
+ url,
167
+ as: 'bytes',
168
+ // The conditional headers have to cross the hop too. Page contexts
169
+ // are where the refresh pipeline actually runs — `collections.ts`
170
+ // imports it directly — so dropping them here would leave every
171
+ // user-facing refresh unconditional while the worker-side path
172
+ // (and every test driving it) looked correct.
173
+ headers: conditionalHeaders(opts.conditional),
174
+ maxBytes: opts.maxBytes,
175
+ timeoutMs: opts.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS,
176
+ });
177
+ if (!reply)
178
+ return null;
179
+ if (reply.kind === 'error')
180
+ return fetchFailed(reply.reason);
181
+ // Before the success-range check, for the same reason the local path
182
+ // tests it before `response.ok`: 304 sits outside 200-299 and would
183
+ // otherwise be reported as an HTTP error.
184
+ if (reply.status === 304)
185
+ return { notModified: true };
186
+ if (reply.status < 200 || reply.status > 299)
187
+ return httpError(reply.status);
188
+ const contentType = reply.contentType.toLowerCase();
189
+ if (opts.acceptContentType && contentType && !opts.acceptContentType(contentType)) {
190
+ return fetchFailed('wrong-content-type');
191
+ }
192
+ const body = replyBytes(reply);
193
+ // Re-checked here rather than trusted: these caps are what each
194
+ // caller's own limit is expressed in, and `body` is what they get.
195
+ if (body.byteLength === 0)
196
+ return fetchFailed('empty');
197
+ if (body.byteLength > opts.maxBytes)
198
+ return fetchFailed('oversize');
199
+ const out = { body, contentType, finalUrl: reply.finalUrl };
200
+ if (reply.etag)
201
+ out.etag = reply.etag;
202
+ if (reply.lastModified)
203
+ out.lastModified = reply.lastModified;
204
+ return out;
205
+ }
206
+ // The fetch itself. Runs in the service worker for page-initiated
207
+ // reads (see above) and directly in whatever context asked otherwise.
208
+ async function fetchHttpBytesHere(url, opts) {
209
+ const controller = new AbortController();
210
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS);
211
+ let response;
212
+ try {
213
+ response = await fetch(url, {
214
+ cache: 'no-store',
215
+ redirect: 'follow',
216
+ referrerPolicy: 'no-referrer',
217
+ signal: controller.signal,
218
+ // Attach GitHub auth header when the URL targets a GitHub host so
219
+ // `@require` / `@resource` / icon fetches against private raw
220
+ // URLs work. Host-gated inside `githubAuthHeaders` so non-GitHub
221
+ // hosts never see the token.
222
+ headers: { ...githubAuthHeaders(url), ...conditionalHeaders(opts.conditional) },
223
+ });
224
+ }
225
+ catch {
226
+ // Our own timer is the only thing that aborts this request, so an
227
+ // aborted signal identifies the timeout exactly rather than
228
+ // leaving it merged with a network failure.
229
+ return controller.signal.aborted ? fetchFailed('timeout') : fetchFailed('no-response');
230
+ }
231
+ finally {
232
+ clearTimeout(timer);
233
+ }
234
+ // Before the `ok` check: 304 is not in the 200-299 range, so testing
235
+ // `response.ok` first would report the server's confirmation that
236
+ // nothing changed as an HTTP error.
237
+ if (response.status === 304)
238
+ return { notModified: true };
239
+ if (!response.ok)
240
+ return httpError(response.status);
241
+ const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
242
+ if (opts.acceptContentType && contentType && !opts.acceptContentType(contentType)) {
243
+ return fetchFailed('wrong-content-type');
244
+ }
245
+ const cl = response.headers.get('content-length');
246
+ if (cl && Number(cl) > opts.maxBytes)
247
+ return fetchFailed('oversize');
248
+ let body;
249
+ try {
250
+ body = await response.arrayBuffer();
251
+ }
252
+ catch {
253
+ return fetchFailed('no-response');
254
+ }
255
+ if (body.byteLength === 0)
256
+ return fetchFailed('empty');
257
+ if (body.byteLength > opts.maxBytes)
258
+ return fetchFailed('oversize');
259
+ const out = { body, contentType, finalUrl: response.url || url };
260
+ const etag = response.headers.get('etag');
261
+ const lastModified = response.headers.get('last-modified');
262
+ if (etag)
263
+ out.etag = etag;
264
+ if (lastModified)
265
+ out.lastModified = lastModified;
266
+ return out;
267
+ }
268
+ // `If-None-Match` wins where a server offers both: an ETag is exact,
269
+ // where `Last-Modified` has one-second resolution and cannot represent
270
+ // two edits inside the same second. Sending both would let the server
271
+ // pick the weaker one.
272
+ function conditionalHeaders(conditional) {
273
+ if (!conditional)
274
+ return {};
275
+ if (conditional.etag)
276
+ return { 'If-None-Match': conditional.etag };
277
+ if (conditional.lastModified)
278
+ return { 'If-Modified-Since': conditional.lastModified };
279
+ return {};
280
+ }
281
+ async function fetchFileBytes(url, opts) {
282
+ // Page contexts: XHR avoids the chrome-extension → file:// CORS
283
+ // opaque-response trap that bites `fetch()`. SW contexts: no XHR,
284
+ // but `fetch()` is permitted for file:// when host_permissions
285
+ // grants `file:///*`.
286
+ if (typeof XMLHttpRequest !== 'undefined') {
287
+ return fetchFileBytesXhr(url, opts);
288
+ }
289
+ return fetchFileBytesFetch(url, opts);
290
+ }
291
+ function fetchFileBytesXhr(url, opts) {
292
+ return new Promise((resolve) => {
293
+ const xhr = new XMLHttpRequest();
294
+ let settled = false;
295
+ const finish = (value) => {
296
+ if (settled)
297
+ return;
298
+ settled = true;
299
+ resolve(value);
300
+ };
301
+ // The one branch here that knows its cause with certainty: we are
302
+ // inside our own timer.
303
+ const timer = setTimeout(() => {
304
+ try {
305
+ xhr.abort();
306
+ }
307
+ catch { /* ignore */ }
308
+ finish(fetchFailed('timeout'));
309
+ }, opts.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS);
310
+ xhr.open('GET', url);
311
+ xhr.responseType = 'arraybuffer';
312
+ xhr.onload = () => {
313
+ clearTimeout(timer);
314
+ const body = xhr.response;
315
+ // Defensive: `onload` with no body shouldn't happen — a failed
316
+ // load fires `onerror`, and a 0-byte file yields an empty buffer,
317
+ // not null. Guarded so that if it ever does, it isn't mistaken
318
+ // for an empty file.
319
+ if (!body) {
320
+ finish(fetchFailed('unreadable-file'));
321
+ return;
322
+ }
323
+ if (body.byteLength === 0) {
324
+ finish(fetchFailed('empty'));
325
+ return;
326
+ }
327
+ if (body.byteLength > opts.maxBytes) {
328
+ finish(fetchFailed('oversize'));
329
+ return;
330
+ }
331
+ // file:// XHRs don't carry an HTTP layer, so Content-Type is
332
+ // whatever Chrome inferred from the extension (often '' or
333
+ // text/plain for images). Run the predicate only when we have
334
+ // something usable; otherwise let the caller infer from the
335
+ // filename / bytes.
336
+ const ct = (xhr.getResponseHeader('content-type') ?? '').toLowerCase();
337
+ if (opts.acceptContentType && ct && !opts.acceptContentType(ct)) {
338
+ finish(fetchFailed('wrong-content-type'));
339
+ return;
340
+ }
341
+ finish({
342
+ body, contentType: ct, finalUrl: url,
343
+ ...fsMtimeField(xhr.getResponseHeader('last-modified')),
344
+ });
345
+ };
346
+ // A file:// XHR carries no HTTP layer, so a file that isn't there
347
+ // and one we're not allowed to read arrive identically — hence one
348
+ // reason covering both, rather than a guess between them.
349
+ xhr.onerror = () => {
350
+ clearTimeout(timer);
351
+ finish(fetchFailed('unreadable-file'));
352
+ };
353
+ try {
354
+ xhr.send();
355
+ }
356
+ catch {
357
+ clearTimeout(timer);
358
+ finish(fetchFailed('unreadable-file'));
359
+ }
360
+ });
361
+ }
362
+ async function fetchFileBytesFetch(url, opts) {
363
+ const controller = new AbortController();
364
+ const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS);
365
+ let response;
366
+ try {
367
+ response = await fetch(url, { signal: controller.signal });
368
+ }
369
+ catch {
370
+ return controller.signal.aborted ? fetchFailed('timeout') : fetchFailed('unreadable-file');
371
+ }
372
+ finally {
373
+ clearTimeout(timer);
374
+ }
375
+ // `status === 0` is normal for a successful file:// read. A non-ok
376
+ // status is NOT reported as an HTTP error: a `file:` URL has no HTTP
377
+ // layer, and whatever status this leg's `fetch` synthesises means the
378
+ // same thing the XHR leg's `onerror` does — the file wouldn't read.
379
+ // Reporting it as `HTTP <n>` would make the two legs describe one
380
+ // situation differently, which is what the corpus validator's fetch
381
+ // shim exposed.
382
+ if (!response.ok && response.status !== 0)
383
+ return fetchFailed('unreadable-file');
384
+ // Answered, but we aren't allowed to look.
385
+ if (response.type === 'opaque')
386
+ return fetchFailed('unreadable-file');
387
+ const contentType = (response.headers.get('content-type') ?? '').toLowerCase();
388
+ if (opts.acceptContentType && contentType && !opts.acceptContentType(contentType)) {
389
+ return fetchFailed('wrong-content-type');
390
+ }
391
+ let body;
392
+ try {
393
+ body = await response.arrayBuffer();
394
+ }
395
+ catch {
396
+ // Same leg, same file — matches the `unreadable-file` above rather
397
+ // than blaming the network for a local read.
398
+ return fetchFailed('unreadable-file');
399
+ }
400
+ if (body.byteLength === 0)
401
+ return fetchFailed('empty');
402
+ if (body.byteLength > opts.maxBytes)
403
+ return fetchFailed('oversize');
404
+ return {
405
+ body, contentType, finalUrl: response.url || url,
406
+ ...fsMtimeField(response.headers.get('last-modified')),
407
+ };
408
+ }
409
+ function fsMtimeField(header) {
410
+ const fsMtime = fsMtimeFromLastModified(header);
411
+ return fsMtime !== undefined ? { fsMtime } : {};
412
+ }
413
+ // ---------- Text + suffix helpers ----------
414
+ // Convenience for text-shaped consumers. Wraps fetchResourceBytes and
415
+ // decodes UTF-8. Returns null on any failure the bytes-fetcher
416
+ // reports, plus null on a decode failure (rare — `fatal: false` rules
417
+ // out throws).
418
+ //
419
+ // Still yes/no: its one caller is the version probe, whose row renders
420
+ // in a definition-list cell with no space for a reason. Switch it to
421
+ // the failure shape if that row ever gets room — the same condition
422
+ // TODO.md attaches to the probe's own wording.
423
+ export async function fetchResourceText(url, opts) {
424
+ const res = await fetchResourceBytes(url, opts);
425
+ // A caller wanting text has nothing to do with a 304 — it holds no
426
+ // prior copy to pair the answer with. Treated as "no text", the same
427
+ // as a failure.
428
+ if ('failed' in res || isNotModified(res))
429
+ return null;
430
+ try {
431
+ return new TextDecoder('utf-8', { fatal: false }).decode(res.body);
432
+ }
433
+ catch {
434
+ return null;
435
+ }
436
+ }
@@ -0,0 +1,44 @@
1
+ export declare const LOCATION_SHAPES = "must be a URL or absolute file path";
2
+ export declare function isHttpUrl(input: string): boolean;
3
+ export declare function isFileUrl(input: string): boolean;
4
+ export type ContainerKind = 'file' | 'http' | 'github';
5
+ export declare function containerPath(kind: ContainerKind): string;
6
+ export declare function newContainerPath(): string;
7
+ export declare function isLocalDirectoryCollection(collectionPath: string): boolean;
8
+ export declare function isContainerCollection(input: string): boolean;
9
+ export declare function containerKind(input: string): ContainerKind | null;
10
+ export type CollectionKind = 'scripts' | 'directory' | 'manifest';
11
+ export declare function collectionKind(path: string): CollectionKind;
12
+ export declare function urlPathname(input: string): string;
13
+ export declare function collectionCanonicalUrl(input: string): string;
14
+ export declare function locationFsPath(input: string): string;
15
+ export declare function isNonPosixFileUrl(input: string): boolean;
16
+ export declare function canonicalCollectionInput(input: string): string;
17
+ export declare function isSingleFileInput(input: string): boolean;
18
+ export declare function isManifestCollection(input: string): boolean;
19
+ export declare function pathToFileUrl(absolutePath: string): string;
20
+ export declare function joinPath(base: string, child: string): string;
21
+ export declare function parentDir(absolutePath: string): string;
22
+ export declare function baseName(absolutePath: string): string;
23
+ export declare function readFileUrlText(url: string): Promise<string>;
24
+ export declare function readFileUrlWithStat(url: string): Promise<{
25
+ text: string;
26
+ size: number;
27
+ fsMtime?: number;
28
+ }>;
29
+ export declare function fsMtimeFromLastModified(header: string | null): number | undefined;
30
+ export interface ListingEntry {
31
+ name: string;
32
+ isDir: boolean;
33
+ size?: number;
34
+ mtime?: number;
35
+ }
36
+ export declare function parseListing(html: string): ListingEntry[];
37
+ export type DirectoryListingCache = Map<string, Promise<readonly ListingEntry[]>>;
38
+ export declare function getDirectoryListingFetches(): number;
39
+ export declare function resetDirectoryListingFetches(): void;
40
+ export declare function isDirectoryListing(html: string): boolean;
41
+ export declare const NOT_A_DIRECTORY_LISTING = "Chrome did not return a directory listing for this folder";
42
+ export declare function listDirectory(absolutePath: string, cache?: DirectoryListingCache): Promise<readonly ListingEntry[]>;
43
+ export declare function hasCachedListing(cache: DirectoryListingCache | undefined, absoluteDir: string): boolean;
44
+ export declare function statFromListing(absoluteDir: string, fileName: string, cache?: DirectoryListingCache): Promise<ListingEntry | null>;