@sovovs/bycli 1.8.0 → 2.1.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 (539) hide show
  1. package/cli-manifest.json +0 -61
  2. package/clis/twitter/search.js +3 -3
  3. package/dist/src/adapter-shadow.js +2 -2
  4. package/dist/src/browser/analyze.d.ts +10 -99
  5. package/dist/src/browser/analyze.js +10 -226
  6. package/dist/src/browser/cdp.js +67 -7
  7. package/dist/src/browser/daemon-client.d.ts +2 -0
  8. package/dist/src/browser/daemon-client.js +1 -1
  9. package/dist/src/browser/extension-capabilities.d.ts +13 -0
  10. package/dist/src/browser/extension-capabilities.js +22 -0
  11. package/dist/src/browser/extension-capabilities.test.d.ts +1 -0
  12. package/dist/src/browser/extension-version-metadata.test.d.ts +1 -0
  13. package/dist/src/browser/navigation-guard.d.ts +78 -0
  14. package/dist/src/browser/navigation-guard.js +142 -0
  15. package/dist/src/browser/navigation-guard.test.d.ts +1 -0
  16. package/dist/src/browser/page.d.ts +1 -0
  17. package/dist/src/browser/page.js +20 -1
  18. package/dist/src/browser/url-policy.d.ts +89 -0
  19. package/dist/src/browser/url-policy.js +311 -0
  20. package/dist/src/browser/url-policy.test.d.ts +1 -0
  21. package/dist/src/browser/verify-fixture.js +2 -2
  22. package/dist/src/build-manifest.js +4 -2
  23. package/dist/src/capabilityRouting.d.ts +3 -2
  24. package/dist/src/capabilityRouting.js +10 -2
  25. package/dist/src/cli.js +11 -14
  26. package/dist/src/commanderAdapter.js +5 -5
  27. package/dist/src/config-paths.d.ts +19 -0
  28. package/dist/src/config-paths.js +31 -0
  29. package/dist/src/daemon.js +256 -0
  30. package/dist/src/discovery.d.ts +10 -3
  31. package/dist/src/discovery.js +20 -10
  32. package/dist/src/discovery.test.d.ts +1 -0
  33. package/dist/src/execution.d.ts +5 -0
  34. package/dist/src/execution.js +272 -48
  35. package/dist/src/help.js +8 -8
  36. package/dist/src/main.js +18 -2
  37. package/dist/src/manifest-schema.d.ts +9 -0
  38. package/dist/src/manifest-schema.js +162 -0
  39. package/dist/src/manifest-schema.test.d.ts +1 -0
  40. package/dist/src/manifest-types.d.ts +1 -1
  41. package/dist/src/observation/redaction.js +10 -4
  42. package/dist/src/plugin.js +11 -8
  43. package/dist/src/recorder/highlevel/analyze.d.ts +29 -0
  44. package/dist/src/recorder/highlevel/analyze.js +91 -0
  45. package/dist/src/recorder/highlevel/init.d.ts +125 -0
  46. package/dist/src/recorder/highlevel/init.js +279 -0
  47. package/dist/src/recorder/highlevel/init.test.d.ts +1 -0
  48. package/dist/src/recorder/highlevel/verify.d.ts +73 -0
  49. package/dist/src/recorder/highlevel/verify.js +74 -0
  50. package/dist/src/recorder/http/analyze-runner.d.ts +28 -0
  51. package/dist/src/recorder/http/analyze-runner.js +78 -0
  52. package/dist/src/recorder/http/analyze-runner.test.d.ts +1 -0
  53. package/dist/src/recorder/http/handlers.d.ts +51 -0
  54. package/dist/src/recorder/http/handlers.js +219 -0
  55. package/dist/src/recorder/http/wrapper-config.d.ts +20 -0
  56. package/dist/src/recorder/http/wrapper-config.js +82 -0
  57. package/dist/src/recorder/http/wrapper-envelope.d.ts +31 -0
  58. package/dist/src/recorder/http/wrapper-envelope.js +51 -0
  59. package/dist/src/recorder/http/wrapper-gates.d.ts +16 -0
  60. package/dist/src/recorder/http/wrapper-gates.js +28 -0
  61. package/dist/src/recorder/http/wrapper-metrics.d.ts +1 -0
  62. package/dist/src/recorder/http/wrapper-metrics.js +5 -0
  63. package/dist/src/recorder/http/wrapper-registry.d.ts +50 -0
  64. package/dist/src/recorder/http/wrapper-registry.js +98 -0
  65. package/dist/src/recorder/http/wrapper-server.d.ts +59 -0
  66. package/dist/src/recorder/http/wrapper-server.js +240 -0
  67. package/dist/src/recorder/http/wrapper.test.d.ts +1 -0
  68. package/dist/src/recorder/observability/logger.d.ts +5 -0
  69. package/dist/src/recorder/observability/logger.js +13 -0
  70. package/dist/src/recorder/runner/config.d.ts +34 -0
  71. package/dist/src/recorder/runner/config.js +84 -0
  72. package/dist/src/recorder/runner/config.test.d.ts +1 -0
  73. package/dist/src/recorder/runner/reap.d.ts +79 -0
  74. package/dist/src/recorder/runner/reap.js +230 -0
  75. package/dist/src/recorder/runner/reap.test.d.ts +1 -0
  76. package/dist/src/recorder/runner/runner-port.d.ts +95 -0
  77. package/dist/src/recorder/runner/runner-port.js +518 -0
  78. package/dist/src/recorder/runner/runner-port.test.d.ts +1 -0
  79. package/dist/src/recorder/runner/session-keys.d.ts +28 -0
  80. package/dist/src/recorder/runner/session-keys.js +72 -0
  81. package/dist/src/recorder/runner/session-keys.test.d.ts +1 -0
  82. package/dist/src/recorder/runner/verify-runner-main.d.ts +96 -0
  83. package/dist/src/recorder/runner/verify-runner-main.js +292 -0
  84. package/dist/src/recorder/runner/verify-runner-main.test.d.ts +1 -0
  85. package/dist/src/registry-api.d.ts +1 -1
  86. package/dist/src/registry-api.types.test.d.ts +1 -0
  87. package/dist/src/registry-transaction.d.ts +42 -0
  88. package/dist/src/registry-transaction.js +194 -0
  89. package/dist/src/registry-transaction.test.d.ts +1 -0
  90. package/dist/src/registry.d.ts +58 -16
  91. package/dist/src/registry.js +131 -15
  92. package/dist/src/serialization.d.ts +1 -1
  93. package/dist/src/serialization.js +3 -3
  94. package/dist/src/types.d.ts +2 -0
  95. package/package.json +8 -5
  96. package/scripts/m1-daemon-navigate-drive.mjs +138 -0
  97. package/scripts/m1-extension-spike.mjs +237 -0
  98. package/scripts/m1-live-spike.mjs +156 -0
  99. package/scripts/m6b-connect-back-spike.sh +31 -0
  100. package/clis/12306/utils.test.js +0 -331
  101. package/clis/1688/assets.test.js +0 -39
  102. package/clis/1688/download.test.js +0 -31
  103. package/clis/1688/item.test.js +0 -67
  104. package/clis/1688/search.test.js +0 -75
  105. package/clis/1688/shared.test.js +0 -57
  106. package/clis/1688/store.test.js +0 -62
  107. package/clis/36kr/article.test.js +0 -46
  108. package/clis/36kr/hot.test.js +0 -15
  109. package/clis/36kr/news.test.js +0 -85
  110. package/clis/51job/utils.test.js +0 -69
  111. package/clis/_atlassian/shared.test.js +0 -170
  112. package/clis/aibase/news.test.js +0 -59
  113. package/clis/amazon/bestsellers.test.js +0 -29
  114. package/clis/amazon/discussion.test.js +0 -124
  115. package/clis/amazon/offer.test.js +0 -29
  116. package/clis/amazon/product.test.js +0 -24
  117. package/clis/amazon/rankings.test.js +0 -41
  118. package/clis/amazon/search.test.js +0 -22
  119. package/clis/amazon/shared.test.js +0 -44
  120. package/clis/apple-podcasts/commands.test.js +0 -119
  121. package/clis/apple-podcasts/utils.test.js +0 -57
  122. package/clis/arxiv/arxiv.test.js +0 -112
  123. package/clis/baidu-scholar/search.test.js +0 -23
  124. package/clis/barchart/greeks.test.js +0 -138
  125. package/clis/bilibili/comment.test.js +0 -153
  126. package/clis/bilibili/comments.test.js +0 -142
  127. package/clis/bilibili/dynamic.test.js +0 -68
  128. package/clis/bilibili/hot.test.js +0 -17
  129. package/clis/bilibili/subtitle.test.js +0 -196
  130. package/clis/bilibili/summary.test.js +0 -210
  131. package/clis/bilibili/utils.test.js +0 -61
  132. package/clis/bilibili/video.test.js +0 -132
  133. package/clis/binance/commands.test.js +0 -70
  134. package/clis/bloomberg/utils.test.js +0 -129
  135. package/clis/booking/booking.test.js +0 -356
  136. package/clis/boss/chatlist.test.js +0 -211
  137. package/clis/boss/chatmsg.test.js +0 -230
  138. package/clis/boss/search.test.js +0 -78
  139. package/clis/boss/utils.test.js +0 -34
  140. package/clis/brave/search.test.js +0 -76
  141. package/clis/chaoxing/utils.test.js +0 -45
  142. package/clis/chatgpt/commands.test.js +0 -45
  143. package/clis/chatgpt/envelope.test.js +0 -108
  144. package/clis/chatgpt/image.test.js +0 -188
  145. package/clis/chatgpt/utils.test.js +0 -330
  146. package/clis/chatgpt-app/ax.test.js +0 -35
  147. package/clis/chatwise/composer.test.js +0 -186
  148. package/clis/chess/analyze.test.js +0 -79
  149. package/clis/chess/game.test.js +0 -178
  150. package/clis/chess/games.test.js +0 -164
  151. package/clis/chess/stats.test.js +0 -79
  152. package/clis/chess/utils.test.js +0 -230
  153. package/clis/claude/ask.test.js +0 -338
  154. package/clis/claude/commands.test.js +0 -118
  155. package/clis/claude/utils.test.js +0 -148
  156. package/clis/cnki/search.test.js +0 -18
  157. package/clis/codex/sidebar.test.js +0 -329
  158. package/clis/coingecko/coingecko.test.js +0 -109
  159. package/clis/confluence/commands.test.js +0 -195
  160. package/clis/coupang/coupang.test.js +0 -159
  161. package/clis/coupang/utils.test.js +0 -62
  162. package/clis/ctrip/ctrip.test.js +0 -719
  163. package/clis/dblp/dblp.test.js +0 -397
  164. package/clis/deepseek/ask.test.js +0 -312
  165. package/clis/deepseek/detail.test.js +0 -81
  166. package/clis/deepseek/send.test.js +0 -107
  167. package/clis/deepseek/utils.test.js +0 -370
  168. package/clis/defillama/defillama.test.js +0 -99
  169. package/clis/devto/devto.test.js +0 -236
  170. package/clis/dianping/dianping.test.js +0 -578
  171. package/clis/douban/download.test.js +0 -170
  172. package/clis/douban/movie-hot.test.js +0 -14
  173. package/clis/douban/search.test.js +0 -11
  174. package/clis/douban/subject.test.js +0 -11
  175. package/clis/douban/utils.test.js +0 -438
  176. package/clis/doubao/detail.test.js +0 -42
  177. package/clis/doubao/history.test.js +0 -37
  178. package/clis/doubao/utils.test.js +0 -344
  179. package/clis/douyin/_shared/browser-fetch.test.js +0 -64
  180. package/clis/douyin/_shared/creation-id.test.js +0 -22
  181. package/clis/douyin/_shared/imagex-upload.test.js +0 -87
  182. package/clis/douyin/_shared/sts2.test.js +0 -27
  183. package/clis/douyin/_shared/text-extra.test.js +0 -37
  184. package/clis/douyin/_shared/timing.test.js +0 -28
  185. package/clis/douyin/_shared/tos-upload-short-read.test.js +0 -83
  186. package/clis/douyin/_shared/tos-upload.test.js +0 -229
  187. package/clis/douyin/_shared/transcode.test.js +0 -91
  188. package/clis/douyin/_shared/vod-upload.test.js +0 -38
  189. package/clis/douyin/activities.test.js +0 -55
  190. package/clis/douyin/collections.test.js +0 -45
  191. package/clis/douyin/delete.test.js +0 -100
  192. package/clis/douyin/draft.test.js +0 -335
  193. package/clis/douyin/drafts.test.js +0 -11
  194. package/clis/douyin/hashtag.test.js +0 -171
  195. package/clis/douyin/location.test.js +0 -23
  196. package/clis/douyin/profile.test.js +0 -46
  197. package/clis/douyin/publish-upload-id.test.js +0 -170
  198. package/clis/douyin/publish.test.js +0 -38
  199. package/clis/douyin/stats.test.js +0 -22
  200. package/clis/douyin/update.test.js +0 -11
  201. package/clis/douyin/user-videos.test.js +0 -151
  202. package/clis/douyin/videos.test.js +0 -54
  203. package/clis/duckduckgo/search.test.js +0 -128
  204. package/clis/duckduckgo/suggest.test.js +0 -66
  205. package/clis/eastmoney/hot-rank.test.js +0 -59
  206. package/clis/endoflife/endoflife.test.js +0 -51
  207. package/clis/facebook/feed.test.js +0 -169
  208. package/clis/facebook/marketplace.test.js +0 -91
  209. package/clis/facebook/notifications.test.js +0 -458
  210. package/clis/facebook/search.test.js +0 -55
  211. package/clis/flathub/flathub.test.js +0 -90
  212. package/clis/flomo/memos.test.js +0 -144
  213. package/clis/gemini/ask.test.js +0 -100
  214. package/clis/gemini/deep-research-result.test.js +0 -155
  215. package/clis/gemini/deep-research.test.js +0 -181
  216. package/clis/gemini/reply-state.test.js +0 -671
  217. package/clis/gemini/utils.test.js +0 -198
  218. package/clis/geogebra/geogebra.test.js +0 -175
  219. package/clis/gitee/search.test.js +0 -65
  220. package/clis/gitee/user.test.js +0 -63
  221. package/clis/google/utils.test.js +0 -75
  222. package/clis/google-scholar/cite.test.js +0 -47
  223. package/clis/google-scholar/profile.test.js +0 -49
  224. package/clis/google-scholar/search.test.js +0 -71
  225. package/clis/goproxy/goproxy.test.js +0 -103
  226. package/clis/gov-law/commands.test.js +0 -39
  227. package/clis/gov-policy/gov-policy.test.js +0 -224
  228. package/clis/grok/ask.test.js +0 -29
  229. package/clis/grok/utils.test.js +0 -103
  230. package/clis/hackernews/hackernews.test.js +0 -132
  231. package/clis/hf/hf.test.js +0 -16
  232. package/clis/hupu/hot.test.js +0 -224
  233. package/clis/imdb/utils.test.js +0 -88
  234. package/clis/indeed/indeed.test.js +0 -375
  235. package/clis/instagram/_shared/private-publish.test.js +0 -705
  236. package/clis/instagram/_shared/protocol-capture.test.js +0 -114
  237. package/clis/instagram/download.test.js +0 -118
  238. package/clis/instagram/note.test.js +0 -53
  239. package/clis/instagram/post.test.js +0 -1647
  240. package/clis/instagram/reel.test.js +0 -167
  241. package/clis/instagram/story.test.js +0 -134
  242. package/clis/jd/commands.test.js +0 -41
  243. package/clis/jd/item.test.js +0 -354
  244. package/clis/jianyu/search.test.js +0 -153
  245. package/clis/jianyu/shared/procurement-contract.test.js +0 -72
  246. package/clis/jianyu/shared/procurement-detail.test.js +0 -84
  247. package/clis/jike/read.test.js +0 -86
  248. package/clis/jira/commands.test.js +0 -287
  249. package/clis/juejin/search.js +0 -123
  250. package/clis/juejin/search.test.js +0 -98
  251. package/clis/lesswrong/frontpage.test.js +0 -37
  252. package/clis/lichess/lichess.test.js +0 -85
  253. package/clis/linkedin/connect.test.js +0 -213
  254. package/clis/linkedin/inbox.test.js +0 -152
  255. package/clis/linkedin/job-detail.test.js +0 -38
  256. package/clis/linkedin/jobs-preferences.test.js +0 -43
  257. package/clis/linkedin/people-search.test.js +0 -216
  258. package/clis/linkedin/post-analytics.test.js +0 -40
  259. package/clis/linkedin/posts.test.js +0 -40
  260. package/clis/linkedin/profile-analytics.test.js +0 -67
  261. package/clis/linkedin/profile-experience.test.js +0 -152
  262. package/clis/linkedin/profile-projects.test.js +0 -111
  263. package/clis/linkedin/profile-read.test.js +0 -77
  264. package/clis/linkedin/safe-send.test.js +0 -204
  265. package/clis/linkedin/salesnav-inbox.test.js +0 -113
  266. package/clis/linkedin/salesnav-message.test.js +0 -172
  267. package/clis/linkedin/salesnav-search.test.js +0 -76
  268. package/clis/linkedin/salesnav-thread.test.js +0 -79
  269. package/clis/linkedin/search.test.js +0 -222
  270. package/clis/linkedin/sent-invitations.test.js +0 -62
  271. package/clis/linkedin/services-read.test.js +0 -105
  272. package/clis/linkedin/thread-snapshot.test.js +0 -89
  273. package/clis/linkedin/timeline.test.js +0 -81
  274. package/clis/linkedin-learning/course.test.js +0 -114
  275. package/clis/linkedin-learning/search.test.js +0 -144
  276. package/clis/linkedin-learning/trending.test.js +0 -123
  277. package/clis/linux-do/feed.test.js +0 -153
  278. package/clis/linux-do/topic-content.test.js +0 -59
  279. package/clis/lobsters/lobsters.test.js +0 -169
  280. package/clis/notebooklm/add-source.test.js +0 -97
  281. package/clis/notebooklm/compat.test.js +0 -16
  282. package/clis/notebooklm/create.test.js +0 -58
  283. package/clis/notebooklm/generate-audio.test.js +0 -63
  284. package/clis/notebooklm/generate-slides.test.js +0 -75
  285. package/clis/notebooklm/history.test.js +0 -58
  286. package/clis/notebooklm/note-list.test.js +0 -56
  287. package/clis/notebooklm/notes-get.test.js +0 -72
  288. package/clis/notebooklm/open.test.js +0 -63
  289. package/clis/notebooklm/rpc.test.js +0 -131
  290. package/clis/notebooklm/source-fulltext.test.js +0 -106
  291. package/clis/notebooklm/source-get.test.js +0 -84
  292. package/clis/notebooklm/source-guide.test.js +0 -104
  293. package/clis/notebooklm/summary.test.js +0 -78
  294. package/clis/notebooklm/utils.test.js +0 -449
  295. package/clis/notebooklm/write-note.test.js +0 -70
  296. package/clis/nuget/nuget.test.js +0 -111
  297. package/clis/oeis/oeis.test.js +0 -88
  298. package/clis/ones/task-helpers.test.js +0 -12
  299. package/clis/ones/worklog.test.js +0 -20
  300. package/clis/openfda/openfda.test.js +0 -114
  301. package/clis/openreview/openreview.test.js +0 -427
  302. package/clis/osv/osv.test.js +0 -97
  303. package/clis/paperreview/commands.test.js +0 -243
  304. package/clis/paperreview/utils.test.js +0 -49
  305. package/clis/pixiv/download.test.js +0 -87
  306. package/clis/pixiv/illusts.test.js +0 -99
  307. package/clis/pixiv/search.test.js +0 -83
  308. package/clis/powerchina/search.test.js +0 -93
  309. package/clis/producthunt/utils.test.js +0 -64
  310. package/clis/pubmed/pubmed.test.js +0 -276
  311. package/clis/quark/utils.test.js +0 -58
  312. package/clis/qwen/utils.test.js +0 -45
  313. package/clis/reddit/extract-media.test.js +0 -149
  314. package/clis/reddit/frontpage.test.js +0 -34
  315. package/clis/reddit/home.test.js +0 -170
  316. package/clis/reddit/hot.test.js +0 -32
  317. package/clis/reddit/popular.test.js +0 -26
  318. package/clis/reddit/read.test.js +0 -335
  319. package/clis/reddit/reply.test.js +0 -89
  320. package/clis/reddit/search.test.js +0 -26
  321. package/clis/reddit/subreddit-info.test.js +0 -163
  322. package/clis/reddit/subreddit.test.js +0 -31
  323. package/clis/reddit/subscribed.test.js +0 -168
  324. package/clis/reddit/whoami.test.js +0 -105
  325. package/clis/rednote/navigation.test.js +0 -26
  326. package/clis/rednote/rednote.test.js +0 -222
  327. package/clis/rest-countries/rest-countries.test.js +0 -83
  328. package/clis/reuters/reuters.test.js +0 -299
  329. package/clis/rfc/rfc.test.js +0 -74
  330. package/clis/sinafinance/stock.test.js +0 -59
  331. package/clis/spotify/utils.test.js +0 -67
  332. package/clis/stackoverflow/stackoverflow.test.js +0 -404
  333. package/clis/steam/steam.test.js +0 -46
  334. package/clis/substack/utils.test.js +0 -44
  335. package/clis/suno/commands.test.js +0 -188
  336. package/clis/suno/download.test.js +0 -151
  337. package/clis/suno/generate.test.js +0 -252
  338. package/clis/suno/utils.test.js +0 -329
  339. package/clis/taobao/commands.test.js +0 -41
  340. package/clis/tdx/hot-rank.test.js +0 -59
  341. package/clis/ths/hot-rank.test.js +0 -64
  342. package/clis/tieba/commands.test.js +0 -79
  343. package/clis/tieba/utils.test.js +0 -290
  344. package/clis/tiktok/creator-videos.test.js +0 -113
  345. package/clis/tiktok/refactor.test.js +0 -389
  346. package/clis/tiktok/write-refactor.test.js +0 -370
  347. package/clis/toutiao/toutiao.test.js +0 -378
  348. package/clis/tvmaze/tvmaze.test.js +0 -93
  349. package/clis/twitter/bookmark-folder.test.js +0 -395
  350. package/clis/twitter/bookmark-folders.test.js +0 -152
  351. package/clis/twitter/bookmark.test.js +0 -74
  352. package/clis/twitter/bookmarks.test.js +0 -205
  353. package/clis/twitter/delete.test.js +0 -85
  354. package/clis/twitter/device-follow.test.js +0 -287
  355. package/clis/twitter/download.test.js +0 -457
  356. package/clis/twitter/followers.test.js +0 -62
  357. package/clis/twitter/following.test.js +0 -338
  358. package/clis/twitter/hide-reply.test.js +0 -76
  359. package/clis/twitter/like.test.js +0 -73
  360. package/clis/twitter/likes.test.js +0 -220
  361. package/clis/twitter/list-add.test.js +0 -133
  362. package/clis/twitter/list-create.test.js +0 -169
  363. package/clis/twitter/list-remove.test.js +0 -110
  364. package/clis/twitter/list-tweets.test.js +0 -196
  365. package/clis/twitter/lists.test.js +0 -253
  366. package/clis/twitter/post.test.js +0 -258
  367. package/clis/twitter/profile.test.js +0 -39
  368. package/clis/twitter/quote.test.js +0 -194
  369. package/clis/twitter/reply.test.js +0 -237
  370. package/clis/twitter/retweet.test.js +0 -73
  371. package/clis/twitter/search.test.js +0 -349
  372. package/clis/twitter/shared.test.js +0 -794
  373. package/clis/twitter/thread.test.js +0 -58
  374. package/clis/twitter/timeline.test.js +0 -104
  375. package/clis/twitter/trending.test.js +0 -15
  376. package/clis/twitter/tweets.test.js +0 -362
  377. package/clis/twitter/unbookmark.test.js +0 -73
  378. package/clis/twitter/unlike.test.js +0 -75
  379. package/clis/twitter/unretweet.test.js +0 -73
  380. package/clis/twitter/utils.test.js +0 -169
  381. package/clis/uisdc/news.test.js +0 -66
  382. package/clis/uiverse/_shared.test.js +0 -62
  383. package/clis/uiverse/navigation.test.js +0 -12
  384. package/clis/upwork/upwork.test.js +0 -566
  385. package/clis/wanfang/search.test.js +0 -23
  386. package/clis/web/read.test.js +0 -392
  387. package/clis/weibo/delete.test.js +0 -94
  388. package/clis/weibo/envelope.test.js +0 -85
  389. package/clis/weibo/favorites.test.js +0 -114
  390. package/clis/weibo/publish.test.js +0 -192
  391. package/clis/weibo/user-posts.test.js +0 -92
  392. package/clis/weibo/utils.test.js +0 -36
  393. package/clis/weixin/drafts.test.js +0 -69
  394. package/clis/weixin/search.test.js +0 -227
  395. package/clis/weread/ai-outline.test.js +0 -83
  396. package/clis/weread/book-search.test.js +0 -242
  397. package/clis/weread/commands.test.js +0 -398
  398. package/clis/weread/private-api-regression.test.js +0 -369
  399. package/clis/weread/search-regression.test.js +0 -494
  400. package/clis/weread/utils.test.js +0 -128
  401. package/clis/weread-official/commands.test.js +0 -385
  402. package/clis/weread-official/utils.test.js +0 -242
  403. package/clis/wikidata/wikidata.test.js +0 -83
  404. package/clis/wikipedia/trending.test.js +0 -57
  405. package/clis/wttr/wttr.test.js +0 -84
  406. package/clis/xianyu/chat.test.js +0 -84
  407. package/clis/xianyu/im.test.js +0 -253
  408. package/clis/xianyu/item.test.js +0 -56
  409. package/clis/xianyu/publish.test.js +0 -220
  410. package/clis/xianyu/search.test.js +0 -17
  411. package/clis/xiaoe/xiaoe.test.js +0 -486
  412. package/clis/xiaohongshu/comments.test.js +0 -258
  413. package/clis/xiaohongshu/creator-note-detail.test.js +0 -451
  414. package/clis/xiaohongshu/creator-notes-summary.test.js +0 -94
  415. package/clis/xiaohongshu/creator-notes.test.js +0 -281
  416. package/clis/xiaohongshu/creator-stats.test.js +0 -24
  417. package/clis/xiaohongshu/delete-note.test.js +0 -172
  418. package/clis/xiaohongshu/download.test.js +0 -316
  419. package/clis/xiaohongshu/navigation.test.js +0 -34
  420. package/clis/xiaohongshu/note.test.js +0 -249
  421. package/clis/xiaohongshu/publish.test.js +0 -608
  422. package/clis/xiaohongshu/search.test.js +0 -325
  423. package/clis/xiaohongshu/user-helpers.test.js +0 -160
  424. package/clis/xiaoyuzhou/auth.test.js +0 -124
  425. package/clis/xiaoyuzhou/download.test.js +0 -135
  426. package/clis/xiaoyuzhou/podcast-episodes.test.js +0 -78
  427. package/clis/xiaoyuzhou/transcript.test.js +0 -195
  428. package/clis/xiaoyuzhou/utils.test.js +0 -39
  429. package/clis/xueqiu/comments.test.js +0 -696
  430. package/clis/xueqiu/danjuan-utils.test.js +0 -41
  431. package/clis/xueqiu/utils.test.js +0 -26
  432. package/clis/yahoo/search.test.js +0 -94
  433. package/clis/youdao/note.test.js +0 -99
  434. package/clis/youtube/channel.test.js +0 -59
  435. package/clis/youtube/feed.test.js +0 -131
  436. package/clis/youtube/transcript-group.test.js +0 -99
  437. package/clis/youtube/transcript.test.js +0 -296
  438. package/clis/youtube/utils.test.js +0 -84
  439. package/clis/yuanbao/ask.test.js +0 -124
  440. package/clis/yuanbao/new.test.js +0 -30
  441. package/clis/yuanbao/shared.test.js +0 -80
  442. package/clis/zhihu/answer-comments.test.js +0 -287
  443. package/clis/zhihu/answer-detail.test.js +0 -338
  444. package/clis/zhihu/answer.test.js +0 -54
  445. package/clis/zhihu/collection.test.js +0 -337
  446. package/clis/zhihu/collections.test.js +0 -182
  447. package/clis/zhihu/comment.test.js +0 -50
  448. package/clis/zhihu/download.test.js +0 -12
  449. package/clis/zhihu/favorite.test.js +0 -59
  450. package/clis/zhihu/follow.test.js +0 -42
  451. package/clis/zhihu/like.test.js +0 -41
  452. package/clis/zhihu/question.test.js +0 -251
  453. package/clis/zhihu/recommend.test.js +0 -143
  454. package/clis/zhihu/search.test.js +0 -198
  455. package/clis/zhihu/target.test.js +0 -77
  456. package/clis/zhihu/text.test.js +0 -24
  457. package/clis/zhihu/write-shared.test.js +0 -176
  458. package/clis/zlibrary/commands.test.js +0 -65
  459. package/clis/zsxq/search.test.js +0 -24
  460. package/clis/zsxq/topic.test.js +0 -30
  461. package/clis/zsxq/topics.test.js +0 -24
  462. package/clis/zsxq/utils.test.js +0 -31
  463. package/dist/src/adapter-shadow.test.js +0 -49
  464. package/dist/src/adapter-source.test.js +0 -29
  465. package/dist/src/browser/analyze.test.js +0 -166
  466. package/dist/src/browser/article-extract.e2e.test.js +0 -105
  467. package/dist/src/browser/article-extract.test.js +0 -94
  468. package/dist/src/browser/ax-snapshot.test.js +0 -91
  469. package/dist/src/browser/base-page.test.js +0 -758
  470. package/dist/src/browser/cdp-click-fixture.test.js +0 -87
  471. package/dist/src/browser/cdp.test.js +0 -79
  472. package/dist/src/browser/compound.test.js +0 -175
  473. package/dist/src/browser/daemon-client.test.js +0 -199
  474. package/dist/src/browser/dom-helpers.test.js +0 -106
  475. package/dist/src/browser/dom-snapshot.test.js +0 -313
  476. package/dist/src/browser/errors.test.js +0 -60
  477. package/dist/src/browser/extract.test.js +0 -129
  478. package/dist/src/browser/find.test.js +0 -180
  479. package/dist/src/browser/html-tree.test.js +0 -181
  480. package/dist/src/browser/network-cache.test.js +0 -58
  481. package/dist/src/browser/network-key.test.js +0 -49
  482. package/dist/src/browser/page.test.js +0 -378
  483. package/dist/src/browser/shape-filter.test.js +0 -101
  484. package/dist/src/browser/shape.test.js +0 -82
  485. package/dist/src/browser/stealth.test.js +0 -134
  486. package/dist/src/browser/target-errors.test.js +0 -94
  487. package/dist/src/browser/target-resolver.test.js +0 -118
  488. package/dist/src/browser/utils.test.js +0 -29
  489. package/dist/src/browser/verify-fixture.test.js +0 -219
  490. package/dist/src/browser.test.js +0 -232
  491. package/dist/src/build-manifest.test.js +0 -341
  492. package/dist/src/capabilityRouting.test.js +0 -63
  493. package/dist/src/cli-argv-preprocess.test.js +0 -209
  494. package/dist/src/cli.test.js +0 -2803
  495. package/dist/src/commanderAdapter.test.js +0 -352
  496. package/dist/src/commands/daemon.test.js +0 -219
  497. package/dist/src/completion.test.js +0 -24
  498. package/dist/src/convention-audit.test.js +0 -247
  499. package/dist/src/daemon.test.js +0 -60
  500. package/dist/src/doctor.test.js +0 -263
  501. package/dist/src/download/article-download.test.js +0 -235
  502. package/dist/src/download/index.test.js +0 -140
  503. package/dist/src/download/media-download.test.js +0 -112
  504. package/dist/src/download/progress.test.js +0 -36
  505. package/dist/src/electron-apps.test.js +0 -49
  506. package/dist/src/engine.test.js +0 -312
  507. package/dist/src/errors.test.js +0 -109
  508. package/dist/src/execution.test.js +0 -623
  509. package/dist/src/extension-manifest-regression.test.js +0 -12
  510. package/dist/src/external.test.js +0 -146
  511. package/dist/src/help.test.js +0 -58
  512. package/dist/src/hooks.test.js +0 -92
  513. package/dist/src/interceptor.test.js +0 -81
  514. package/dist/src/launcher.test.js +0 -138
  515. package/dist/src/node-network.test.js +0 -55
  516. package/dist/src/observation/artifact.test.js +0 -121
  517. package/dist/src/observation/manager.test.js +0 -13
  518. package/dist/src/observation/redaction.test.js +0 -32
  519. package/dist/src/observation/retention.test.js +0 -118
  520. package/dist/src/observation/ring-buffer.test.js +0 -22
  521. package/dist/src/output.test.js +0 -53
  522. package/dist/src/package-exports.test.js +0 -112
  523. package/dist/src/pipeline/executor.test.js +0 -182
  524. package/dist/src/pipeline/steps/download.test.js +0 -102
  525. package/dist/src/pipeline/steps/fetch.test.js +0 -117
  526. package/dist/src/pipeline/template.test.js +0 -175
  527. package/dist/src/pipeline/transform.test.js +0 -133
  528. package/dist/src/plugin-manifest.test.js +0 -179
  529. package/dist/src/plugin-scaffold.test.js +0 -83
  530. package/dist/src/plugin.test.js +0 -1324
  531. package/dist/src/registry.test.js +0 -209
  532. package/dist/src/runtime-detect.test.js +0 -39
  533. package/dist/src/serialization.test.js +0 -78
  534. package/dist/src/snapshotFormatter.test.js +0 -521
  535. package/dist/src/tui.test.js +0 -19
  536. package/dist/src/update-check.test.js +0 -116
  537. package/dist/src/utils.test.js +0 -155
  538. package/dist/src/validate.test.js +0 -90
  539. package/dist/src/weixin-download.test.js +0 -45
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Navigation interception guard (M1 spike, direct-CDP / Node side).
3
+ *
4
+ * Closes the P0 hole where navigation fired before request interception was armed.
5
+ * The contract (ADR-0002, ADR-0006, 04-security-model.md) is:
6
+ *
7
+ * 1. Pre-check the target URL (syntax + DNS precheck) BEFORE any navigation.
8
+ * 2. Arm Fetch interception (resourceType Document, Request stage) and confirm the
9
+ * handler is registered BEFORE the single Page.navigate.
10
+ * 3. Re-run the full URL policy on every paused main-frame Document request
11
+ * (initial + every redirect / secondary main-frame request) and failRequest
12
+ * anything that does not pass — so a forbidden redirect target receives 0 bytes.
13
+ * 4. If interception cannot be armed, FAIL CLOSED — never fall back to bare
14
+ * navigation. Callers surface `navigation_redirect_requires_interception`.
15
+ *
16
+ * remoteIPAddress observed on Network.responseReceived is logged only — it is
17
+ * post-connect and therefore `ip-observed-only`, NOT a security boundary (ADR-0006).
18
+ *
19
+ * This module is runtime-bound only through the small InterceptBridge interface,
20
+ * so it is unit-testable with a fake bridge (no real CDP / network).
21
+ */
22
+ import { type DnsResolver, type UrlPolicyReason } from './url-policy.js';
23
+ /** Minimal CDP surface this guard needs (satisfied by CDPBridge). */
24
+ export interface InterceptBridge {
25
+ send(method: string, params?: Record<string, unknown>): Promise<unknown>;
26
+ on(event: string, handler: (params: unknown) => void): void;
27
+ off(event: string, handler: (params: unknown) => void): void;
28
+ }
29
+ export declare class InterceptionUnavailableError extends Error {
30
+ readonly code = "navigation_redirect_requires_interception";
31
+ constructor(detail: string);
32
+ }
33
+ export declare class NavigationBlockedError extends Error {
34
+ readonly url: string;
35
+ readonly reason: UrlPolicyReason;
36
+ readonly code = "navigation_blocked_by_policy";
37
+ constructor(url: string, reason: UrlPolicyReason, detail: string);
38
+ }
39
+ export interface BlockedRecord {
40
+ url: string;
41
+ reason: UrlPolicyReason;
42
+ }
43
+ export interface NavigationGuard {
44
+ /** URLs that were failRequest'd by policy (initial + redirects). */
45
+ readonly blocked: ReadonlyArray<BlockedRecord>;
46
+ /** remoteIPAddress values observed post-connect (logged, not a boundary). */
47
+ readonly observedIps: ReadonlyArray<string>;
48
+ /** Tear down interception + listeners. Idempotent. */
49
+ dispose(): Promise<void>;
50
+ }
51
+ /**
52
+ * Arm Fetch interception and policy re-check, fail-closed. Must be called and
53
+ * resolved BEFORE the caller issues Page.navigate. Throws
54
+ * InterceptionUnavailableError if Fetch cannot be enabled.
55
+ */
56
+ export declare function armNavigationGuard(bridge: InterceptBridge, resolver: DnsResolver, opts?: {
57
+ allowLiteralIp?: boolean;
58
+ observeIp?: boolean;
59
+ }): Promise<NavigationGuard>;
60
+ /**
61
+ * Full guarded navigation for the direct-CDP path. Pre-checks the URL, arms the
62
+ * guard, runs the caller-supplied navigate thunk (which issues Page.navigate), and
63
+ * keeps the guard live for redirects until the caller disposes it.
64
+ *
65
+ * Returns the live guard so the caller can inspect blocked/observedIps and dispose
66
+ * after the page settles. Throws NavigationBlockedError if the initial target fails
67
+ * policy (we never arm/navigate to a known-bad target), or
68
+ * InterceptionUnavailableError if interception cannot be armed.
69
+ */
70
+ export declare function guardedNavigate(bridge: InterceptBridge, url: string, resolver: DnsResolver, navigate: () => Promise<void>, opts?: {
71
+ allowLiteralIp?: boolean;
72
+ observeIp?: boolean;
73
+ }): Promise<NavigationGuard>;
74
+ /** Convenience: synchronous pre-screen without DNS, for callers that only have the
75
+ * syntax layer available (e.g. the extension service worker, which cannot resolve). */
76
+ export declare function screenSyntaxOnly(url: string, opts?: {
77
+ allowLiteralIp?: boolean;
78
+ }): import("./url-policy.js").UrlPolicyResult;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Navigation interception guard (M1 spike, direct-CDP / Node side).
3
+ *
4
+ * Closes the P0 hole where navigation fired before request interception was armed.
5
+ * The contract (ADR-0002, ADR-0006, 04-security-model.md) is:
6
+ *
7
+ * 1. Pre-check the target URL (syntax + DNS precheck) BEFORE any navigation.
8
+ * 2. Arm Fetch interception (resourceType Document, Request stage) and confirm the
9
+ * handler is registered BEFORE the single Page.navigate.
10
+ * 3. Re-run the full URL policy on every paused main-frame Document request
11
+ * (initial + every redirect / secondary main-frame request) and failRequest
12
+ * anything that does not pass — so a forbidden redirect target receives 0 bytes.
13
+ * 4. If interception cannot be armed, FAIL CLOSED — never fall back to bare
14
+ * navigation. Callers surface `navigation_redirect_requires_interception`.
15
+ *
16
+ * remoteIPAddress observed on Network.responseReceived is logged only — it is
17
+ * post-connect and therefore `ip-observed-only`, NOT a security boundary (ADR-0006).
18
+ *
19
+ * This module is runtime-bound only through the small InterceptBridge interface,
20
+ * so it is unit-testable with a fake bridge (no real CDP / network).
21
+ */
22
+ import { checkUrlPolicy, checkUrlSyntax } from './url-policy.js';
23
+ export class InterceptionUnavailableError extends Error {
24
+ code = 'navigation_redirect_requires_interception';
25
+ constructor(detail) {
26
+ super(`navigation request interception unavailable: ${detail}`);
27
+ this.name = 'InterceptionUnavailableError';
28
+ }
29
+ }
30
+ export class NavigationBlockedError extends Error {
31
+ url;
32
+ reason;
33
+ code = 'navigation_blocked_by_policy';
34
+ constructor(url, reason, detail) {
35
+ super(`navigation blocked: ${detail}`);
36
+ this.url = url;
37
+ this.reason = reason;
38
+ this.name = 'NavigationBlockedError';
39
+ }
40
+ }
41
+ /**
42
+ * Arm Fetch interception and policy re-check, fail-closed. Must be called and
43
+ * resolved BEFORE the caller issues Page.navigate. Throws
44
+ * InterceptionUnavailableError if Fetch cannot be enabled.
45
+ */
46
+ export async function armNavigationGuard(bridge, resolver, opts = {}) {
47
+ const blocked = [];
48
+ const observedIps = [];
49
+ // Fetch.enable scoped to top-level documents at the Request stage so we block
50
+ // before the request is sent. resourceType uses Network.ResourceType values;
51
+ // 'Document' is the main frame (P1-1).
52
+ try {
53
+ await bridge.send('Fetch.enable', {
54
+ patterns: [{ urlPattern: '*', resourceType: 'Document', requestStage: 'Request' }],
55
+ });
56
+ }
57
+ catch (err) {
58
+ throw new InterceptionUnavailableError(err instanceof Error ? err.message : String(err));
59
+ }
60
+ const onPaused = (raw) => {
61
+ const p = raw;
62
+ // Only enforce policy on main-frame Document requests; let sub-resources through
63
+ // unmodified (page sub-resource SSRF is out of scope for the navigation spike).
64
+ if (p.resourceType !== 'Document') {
65
+ void bridge.send('Fetch.continueRequest', { requestId: p.requestId }).catch(() => { });
66
+ return;
67
+ }
68
+ void (async () => {
69
+ const verdict = await checkUrlPolicy(p.request.url, resolver, opts).catch(() => null);
70
+ if (verdict && verdict.ok) {
71
+ await bridge.send('Fetch.continueRequest', { requestId: p.requestId }).catch(() => { });
72
+ }
73
+ else {
74
+ if (verdict && !verdict.ok)
75
+ blocked.push({ url: p.request.url, reason: verdict.reason });
76
+ // BlockedByClient → the request is never sent to the target.
77
+ await bridge
78
+ .send('Fetch.failRequest', { requestId: p.requestId, errorReason: 'BlockedByClient' })
79
+ .catch(() => { });
80
+ }
81
+ })();
82
+ };
83
+ const onResponse = (raw) => {
84
+ const p = raw;
85
+ const ip = p.response?.remoteIPAddress;
86
+ if (ip)
87
+ observedIps.push(ip);
88
+ };
89
+ bridge.on('Fetch.requestPaused', onPaused);
90
+ if (opts.observeIp) {
91
+ // Network domain must be enabled elsewhere for this to fire; we only attach the
92
+ // observer. Missing IP (cache/SW/failed request) is tolerated — never a boundary.
93
+ bridge.on('Network.responseReceived', onResponse);
94
+ }
95
+ let disposed = false;
96
+ return {
97
+ blocked,
98
+ observedIps,
99
+ async dispose() {
100
+ if (disposed)
101
+ return;
102
+ disposed = true;
103
+ bridge.off('Fetch.requestPaused', onPaused);
104
+ if (opts.observeIp)
105
+ bridge.off('Network.responseReceived', onResponse);
106
+ await bridge.send('Fetch.disable').catch(() => { });
107
+ },
108
+ };
109
+ }
110
+ /**
111
+ * Full guarded navigation for the direct-CDP path. Pre-checks the URL, arms the
112
+ * guard, runs the caller-supplied navigate thunk (which issues Page.navigate), and
113
+ * keeps the guard live for redirects until the caller disposes it.
114
+ *
115
+ * Returns the live guard so the caller can inspect blocked/observedIps and dispose
116
+ * after the page settles. Throws NavigationBlockedError if the initial target fails
117
+ * policy (we never arm/navigate to a known-bad target), or
118
+ * InterceptionUnavailableError if interception cannot be armed.
119
+ */
120
+ export async function guardedNavigate(bridge, url, resolver, navigate, opts = {}) {
121
+ // 1. Pre-check the initial target before doing anything else.
122
+ const pre = await checkUrlPolicy(url, resolver, opts);
123
+ if (!pre.ok) {
124
+ throw new NavigationBlockedError(url, pre.reason, pre.detail);
125
+ }
126
+ // 2. Arm interception (fail-closed) and confirm before navigating.
127
+ const guard = await armNavigationGuard(bridge, resolver, opts);
128
+ // 3. Navigate exactly once, now that the guard is live.
129
+ try {
130
+ await navigate();
131
+ }
132
+ catch (err) {
133
+ await guard.dispose();
134
+ throw err;
135
+ }
136
+ return guard;
137
+ }
138
+ /** Convenience: synchronous pre-screen without DNS, for callers that only have the
139
+ * syntax layer available (e.g. the extension service worker, which cannot resolve). */
140
+ export function screenSyntaxOnly(url, opts = {}) {
141
+ return checkUrlSyntax(url, opts);
142
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -44,6 +44,7 @@ export declare class Page extends BasePage {
44
44
  domain?: string;
45
45
  url?: string;
46
46
  }): Promise<BrowserCookie[]>;
47
+ focusWindow(): Promise<void>;
47
48
  /** Release the current browser session lease in the extension */
48
49
  closeWindow(): Promise<void>;
49
50
  tabs(): Promise<unknown[]>;
@@ -8,7 +8,8 @@
8
8
  * by the navigate action and pass it to all subsequent commands. This ensures
9
9
  * page-scoped operations target the correct page without guessing.
10
10
  */
11
- import { sendCommand, sendCommandFull } from './daemon-client.js';
11
+ import { fetchDaemonStatus, sendCommand, sendCommandFull } from './daemon-client.js';
12
+ import { extensionCapabilityHint, FOCUS_WINDOW_CAPABILITY } from './extension-capabilities.js';
12
13
  import { buildEvaluateExpression } from './utils.js';
13
14
  import { saveBase64ToFile } from '../utils.js';
14
15
  import { generateStealthJs } from './stealth.js';
@@ -180,6 +181,24 @@ export class Page extends BasePage {
180
181
  const result = await sendCommand('cookies', { ...this._sessionOpts(), ...opts });
181
182
  return Array.isArray(result) ? result : [];
182
183
  }
184
+ async focusWindow() {
185
+ const status = await fetchDaemonStatus(this.contextId ? { contextId: this.contextId } : undefined);
186
+ const selectedExtensionIsConnected = status?.extensionConnected === true
187
+ && status.profileRequired !== true
188
+ && status.profileDisconnected !== true;
189
+ const capabilities = status?.extensionCapabilities;
190
+ const capabilityListIsDefinitive = Array.isArray(capabilities);
191
+ if (selectedExtensionIsConnected
192
+ && capabilityListIsDefinitive
193
+ && !capabilities.includes(FOCUS_WINDOW_CAPABILITY)) {
194
+ throw new Error(`Connected Browser Bridge does not advertise ${FOCUS_WINDOW_CAPABILITY}. ${extensionCapabilityHint(FOCUS_WINDOW_CAPABILITY)}`);
195
+ }
196
+ await sendCommandFull('tabs', {
197
+ op: 'focus',
198
+ ...this._cmdOpts(),
199
+ ...this._sessionOpts(),
200
+ });
201
+ }
183
202
  /** Release the current browser session lease in the extension */
184
203
  async closeWindow() {
185
204
  try {
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Navigation URL policy (M1 spike).
3
+ *
4
+ * Pure, runtime-free validation of a navigation target against the security
5
+ * model in dashboard-docs/system/adapter-recorder-system/04-security-model.md and
6
+ * adr/0006-dns-rebinding-ip-enforcement.md.
7
+ *
8
+ * Two layers, deliberately separated:
9
+ * 1. checkUrlSyntax(url) — synchronous: protocol / userinfo / literal-IP /
10
+ * forbidden-host checks. No I/O.
11
+ * 2. resolveAndCheckHost(...) — asynchronous: resolves every A/AAAA record for a
12
+ * *domain* host and rejects if any resolved IP is
13
+ * forbidden. DNS resolver is injected for testing.
14
+ *
15
+ * SECURITY BOUNDARY NOTE (ADR-0006): the DNS precheck in layer 2 filters only the
16
+ * records visible *at check time*. It does NOT prove the IP the browser actually
17
+ * connects to (separate resolver, DNS cache, proxy, Happy Eyeballs, TTL-window
18
+ * rebind). This TOCTOU window is not removable here. Passing this policy is
19
+ * `ip-observed-only`, never `strict-ip-enforced`. Callers must still arm
20
+ * before-send request interception and re-run this policy on every redirected /
21
+ * secondary main-frame request.
22
+ */
23
+ /** Why a URL was rejected. Stable codes — used by tests and error mapping. */
24
+ export type UrlPolicyReason = 'forbidden_protocol' | 'userinfo_present' | 'empty_host' | 'literal_ip_host' | 'forbidden_ip' | 'unresolvable_host' | 'no_dns_records';
25
+ export interface UrlPolicyOk {
26
+ ok: true;
27
+ /** Canonicalized URL string after normalization. */
28
+ url: string;
29
+ /** Normalized hostname (lowercased, trailing dot stripped, IDN punycode, IPv6 canonical). */
30
+ hostname: string;
31
+ /** True when the host was an IP literal — only reachable when allowLiteralIp is set. */
32
+ isIpLiteral: boolean;
33
+ }
34
+ export interface UrlPolicyReject {
35
+ ok: false;
36
+ reason: UrlPolicyReason;
37
+ /** Human detail for logs/errors (never includes secrets). */
38
+ detail: string;
39
+ /** The raw authority (host[:port]) exactly as supplied, for input-form assertions. */
40
+ rawAuthority?: string;
41
+ }
42
+ export type UrlPolicyResult = UrlPolicyOk | UrlPolicyReject;
43
+ /** Injected DNS resolver — node:dns/promises in production, a stub in tests. */
44
+ export interface DnsResolver {
45
+ resolve4(hostname: string): Promise<string[]>;
46
+ resolve6(hostname: string): Promise<string[]>;
47
+ }
48
+ /**
49
+ * Extract the raw authority (everything between `://` and the first `/?#`) from the
50
+ * input string, before any URL-parser canonicalization. Used to assert on the
51
+ * *input form* — e.g. that `http://0177.0.0.1/` was supplied as an octal literal —
52
+ * since WHATWG URL silently rewrites those into `127.0.0.1` (P1-4).
53
+ */
54
+ export declare function rawAuthorityOf(input: string): string | undefined;
55
+ /**
56
+ * Decide whether a hostname (already lowercased, trailing-dot stripped) is an IP
57
+ * literal in ANY encoding the browser would accept: dotted-decimal, IPv6 in
58
+ * brackets, decimal/octal/hex/shorthand IPv4. Returns the canonical IP string when
59
+ * it is a literal, or null when it is a real domain name.
60
+ */
61
+ export declare function asIpLiteral(host: string): string | null;
62
+ /**
63
+ * Parse the loose IPv4 forms browsers accept (decimal, octal, hex, 1-4 dotted
64
+ * parts with mixed radices). Returns canonical dotted-decimal, or null if not a
65
+ * valid IPv4 literal. Mirrors the "parts" algorithm in the WHATWG URL spec.
66
+ */
67
+ export declare function parseIpv4Loose(host: string): string | null;
68
+ /** True when an IP literal (v4 or v6, any normalized form) is in a forbidden range. */
69
+ export declare function isForbiddenIp(ip: string): boolean;
70
+ /**
71
+ * Synchronous URL syntax + literal-host policy. No DNS, no I/O.
72
+ *
73
+ * @param input raw navigation URL
74
+ * @param opts.allowLiteralIp when true, an IP-literal host is allowed *if* it is
75
+ * not in a forbidden range (used by tooling that
76
+ * targets explicit IPs); default false — navigation
77
+ * targets must be domain names.
78
+ */
79
+ export declare function checkUrlSyntax(input: string, opts?: {
80
+ allowLiteralIp?: boolean;
81
+ }): UrlPolicyResult;
82
+ /**
83
+ * Full policy: syntax check, then resolve every A/AAAA record for a domain host and
84
+ * reject if any resolved IP is forbidden (DNS precheck — see SECURITY BOUNDARY NOTE).
85
+ * IP-literal hosts skip DNS (already vetted by checkUrlSyntax).
86
+ */
87
+ export declare function checkUrlPolicy(input: string, resolver: DnsResolver, opts?: {
88
+ allowLiteralIp?: boolean;
89
+ }): Promise<UrlPolicyResult>;
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Navigation URL policy (M1 spike).
3
+ *
4
+ * Pure, runtime-free validation of a navigation target against the security
5
+ * model in dashboard-docs/system/adapter-recorder-system/04-security-model.md and
6
+ * adr/0006-dns-rebinding-ip-enforcement.md.
7
+ *
8
+ * Two layers, deliberately separated:
9
+ * 1. checkUrlSyntax(url) — synchronous: protocol / userinfo / literal-IP /
10
+ * forbidden-host checks. No I/O.
11
+ * 2. resolveAndCheckHost(...) — asynchronous: resolves every A/AAAA record for a
12
+ * *domain* host and rejects if any resolved IP is
13
+ * forbidden. DNS resolver is injected for testing.
14
+ *
15
+ * SECURITY BOUNDARY NOTE (ADR-0006): the DNS precheck in layer 2 filters only the
16
+ * records visible *at check time*. It does NOT prove the IP the browser actually
17
+ * connects to (separate resolver, DNS cache, proxy, Happy Eyeballs, TTL-window
18
+ * rebind). This TOCTOU window is not removable here. Passing this policy is
19
+ * `ip-observed-only`, never `strict-ip-enforced`. Callers must still arm
20
+ * before-send request interception and re-run this policy on every redirected /
21
+ * secondary main-frame request.
22
+ */
23
+ import { isIP } from 'node:net';
24
+ const ALLOWED_PROTOCOLS = new Set(['http:', 'https:']);
25
+ /**
26
+ * Extract the raw authority (everything between `://` and the first `/?#`) from the
27
+ * input string, before any URL-parser canonicalization. Used to assert on the
28
+ * *input form* — e.g. that `http://0177.0.0.1/` was supplied as an octal literal —
29
+ * since WHATWG URL silently rewrites those into `127.0.0.1` (P1-4).
30
+ */
31
+ export function rawAuthorityOf(input) {
32
+ const m = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/([^/?#]*)/.exec(input.trim());
33
+ if (!m)
34
+ return undefined;
35
+ // Strip userinfo for the authority-host portion but keep the host[:port].
36
+ const authority = m[1];
37
+ const at = authority.lastIndexOf('@');
38
+ return at >= 0 ? authority.slice(at + 1) : authority;
39
+ }
40
+ /**
41
+ * Decide whether a hostname (already lowercased, trailing-dot stripped) is an IP
42
+ * literal in ANY encoding the browser would accept: dotted-decimal, IPv6 in
43
+ * brackets, decimal/octal/hex/shorthand IPv4. Returns the canonical IP string when
44
+ * it is a literal, or null when it is a real domain name.
45
+ */
46
+ export function asIpLiteral(host) {
47
+ // IPv6 literal arrives bracketed from URL.hostname only when constructed from a
48
+ // bracketed form; URL strips the brackets, so test the bare form too.
49
+ const unbracketed = host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
50
+ const v = isIP(unbracketed);
51
+ if (v === 4 || v === 6)
52
+ return unbracketed;
53
+ // IPv4 alternative encodings: decimal (2130706433), octal (0177.0.0.1),
54
+ // hex (0x7f.0.0.1), and shorthand (127.1). WHATWG URL already canonicalizes
55
+ // most of these, but we re-derive defensively in case a caller bypasses it.
56
+ const parsed = parseIpv4Loose(host);
57
+ return parsed;
58
+ }
59
+ /**
60
+ * Parse the loose IPv4 forms browsers accept (decimal, octal, hex, 1-4 dotted
61
+ * parts with mixed radices). Returns canonical dotted-decimal, or null if not a
62
+ * valid IPv4 literal. Mirrors the "parts" algorithm in the WHATWG URL spec.
63
+ */
64
+ export function parseIpv4Loose(host) {
65
+ if (host.length === 0)
66
+ return null;
67
+ const parts = host.split('.');
68
+ // A trailing empty part (from a trailing dot) is tolerated by the spec; reject
69
+ // here since trailing dots are stripped before this is called.
70
+ if (parts.length > 4)
71
+ return null;
72
+ if (parts.some((p) => p.length === 0))
73
+ return null;
74
+ const numbers = [];
75
+ for (const part of parts) {
76
+ const n = parseRadixPart(part);
77
+ if (n === null)
78
+ return null;
79
+ numbers.push(n);
80
+ }
81
+ // Each non-final part must fit in a byte; the final part absorbs the remaining bytes.
82
+ for (let i = 0; i < numbers.length - 1; i++) {
83
+ if (numbers[i] > 255)
84
+ return null;
85
+ }
86
+ const last = numbers[numbers.length - 1];
87
+ const maxLast = 256 ** (5 - numbers.length);
88
+ if (last >= maxLast)
89
+ return null;
90
+ let ipv4 = last;
91
+ for (let i = 0; i < numbers.length - 1; i++) {
92
+ ipv4 += numbers[i] * 256 ** (3 - i);
93
+ }
94
+ if (ipv4 > 0xffffffff)
95
+ return null;
96
+ return [
97
+ (ipv4 >>> 24) & 0xff,
98
+ (ipv4 >>> 16) & 0xff,
99
+ (ipv4 >>> 8) & 0xff,
100
+ ipv4 & 0xff,
101
+ ].join('.');
102
+ }
103
+ function parseRadixPart(part) {
104
+ let radix = 10;
105
+ let digits = part;
106
+ if (/^0[xX]/.test(part)) {
107
+ radix = 16;
108
+ digits = part.slice(2);
109
+ if (digits.length === 0)
110
+ return null;
111
+ if (!/^[0-9a-fA-F]+$/.test(digits))
112
+ return null;
113
+ }
114
+ else if (part.length > 1 && part[0] === '0') {
115
+ radix = 8;
116
+ digits = part.slice(1);
117
+ if (!/^[0-7]+$/.test(digits))
118
+ return null;
119
+ }
120
+ else {
121
+ if (!/^[0-9]+$/.test(part))
122
+ return null;
123
+ }
124
+ const n = parseInt(digits, radix);
125
+ return Number.isNaN(n) ? null : n;
126
+ }
127
+ /** IPv4 forbidden ranges as [network, prefixBits] CIDR pairs. */
128
+ const FORBIDDEN_IPV4 = [
129
+ ['0.0.0.0', 8], // "this" network / unspecified
130
+ ['10.0.0.0', 8], // RFC1918 private
131
+ ['100.64.0.0', 10], // RFC6598 CGNAT
132
+ ['127.0.0.0', 8], // loopback
133
+ ['169.254.0.0', 16], // link-local (incl. 169.254.169.254 cloud metadata)
134
+ ['172.16.0.0', 12], // RFC1918 private
135
+ ['192.0.0.0', 24], // IETF protocol assignments
136
+ ['192.0.2.0', 24], // TEST-NET-1 documentation
137
+ ['192.168.0.0', 16], // RFC1918 private
138
+ ['198.18.0.0', 15], // RFC2544 benchmarking
139
+ ['198.51.100.0', 24], // TEST-NET-2 documentation
140
+ ['203.0.113.0', 24], // TEST-NET-3 documentation
141
+ ['224.0.0.0', 4], // multicast
142
+ ['240.0.0.0', 4], // reserved (incl. 255.255.255.255 broadcast)
143
+ ];
144
+ function ipv4ToInt(ip) {
145
+ const parts = ip.split('.').map((p) => parseInt(p, 10));
146
+ return ((parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]) >>> 0;
147
+ }
148
+ function ipv4InForbiddenRange(ip) {
149
+ const addr = ipv4ToInt(ip);
150
+ for (const [net, bits] of FORBIDDEN_IPV4) {
151
+ const mask = bits === 0 ? 0 : (0xffffffff << (32 - bits)) >>> 0;
152
+ if ((addr & mask) === (ipv4ToInt(net) & mask))
153
+ return true;
154
+ }
155
+ return false;
156
+ }
157
+ /** Expand an IPv6 address to its 8 16-bit groups (numbers), handling `::` and
158
+ * embedded IPv4 tails. Returns null when not a valid IPv6 literal. */
159
+ function ipv6Groups(ip) {
160
+ let addr = ip;
161
+ // Embedded IPv4 tail (e.g. ::ffff:127.0.0.1 or 64:ff9b::192.0.2.1).
162
+ const lastColon = addr.lastIndexOf(':');
163
+ const tail = addr.slice(lastColon + 1);
164
+ if (tail.includes('.')) {
165
+ if (isIP(tail) !== 4)
166
+ return null;
167
+ const v4 = ipv4ToInt(tail);
168
+ addr = addr.slice(0, lastColon + 1) + ((v4 >>> 16) & 0xffff).toString(16) + ':' + (v4 & 0xffff).toString(16);
169
+ }
170
+ const halves = addr.split('::');
171
+ if (halves.length > 2)
172
+ return null;
173
+ const head = halves[0].length ? halves[0].split(':') : [];
174
+ const back = halves.length === 2 ? (halves[1].length ? halves[1].split(':') : []) : null;
175
+ const parse = (groups) => {
176
+ const out = [];
177
+ for (const g of groups) {
178
+ if (!/^[0-9a-fA-F]{1,4}$/.test(g))
179
+ return null;
180
+ out.push(parseInt(g, 16));
181
+ }
182
+ return out;
183
+ };
184
+ const headNums = parse(head);
185
+ if (headNums === null)
186
+ return null;
187
+ if (back === null) {
188
+ return headNums.length === 8 ? headNums : null;
189
+ }
190
+ const backNums = parse(back);
191
+ if (backNums === null)
192
+ return null;
193
+ const fill = 8 - headNums.length - backNums.length;
194
+ if (fill < 0)
195
+ return null;
196
+ return [...headNums, ...new Array(fill).fill(0), ...backNums];
197
+ }
198
+ function ipv6InForbiddenRange(ip) {
199
+ const groups = ipv6Groups(ip);
200
+ if (!groups)
201
+ return false;
202
+ // Unspecified :: and loopback ::1
203
+ if (groups.every((g) => g === 0))
204
+ return true; // ::
205
+ if (groups.slice(0, 7).every((g) => g === 0) && groups[7] === 1)
206
+ return true; // ::1
207
+ // IPv4-mapped ::ffff:0:0/96 and IPv4-compatible — defer to the embedded v4 range check.
208
+ if (groups.slice(0, 5).every((g) => g === 0) && groups[5] === 0xffff) {
209
+ const v4 = `${(groups[6] >> 8) & 0xff}.${groups[6] & 0xff}.${(groups[7] >> 8) & 0xff}.${groups[7] & 0xff}`;
210
+ return ipv4InForbiddenRange(v4);
211
+ }
212
+ const first = groups[0];
213
+ // fc00::/7 unique-local
214
+ if ((first & 0xfe00) === 0xfc00)
215
+ return true;
216
+ // fe80::/10 link-local
217
+ if ((first & 0xffc0) === 0xfe80)
218
+ return true;
219
+ // ff00::/8 multicast
220
+ if ((first & 0xff00) === 0xff00)
221
+ return true;
222
+ // fd00:ec2::254 cloud metadata is inside fc00::/7, already covered.
223
+ return false;
224
+ }
225
+ /** True when an IP literal (v4 or v6, any normalized form) is in a forbidden range. */
226
+ export function isForbiddenIp(ip) {
227
+ const v = isIP(ip);
228
+ if (v === 4)
229
+ return ipv4InForbiddenRange(ip);
230
+ if (v === 6)
231
+ return ipv6InForbiddenRange(ip);
232
+ // Loose IPv4 encodings normalize first.
233
+ const canon = parseIpv4Loose(ip);
234
+ return canon ? ipv4InForbiddenRange(canon) : false;
235
+ }
236
+ /**
237
+ * Synchronous URL syntax + literal-host policy. No DNS, no I/O.
238
+ *
239
+ * @param input raw navigation URL
240
+ * @param opts.allowLiteralIp when true, an IP-literal host is allowed *if* it is
241
+ * not in a forbidden range (used by tooling that
242
+ * targets explicit IPs); default false — navigation
243
+ * targets must be domain names.
244
+ */
245
+ export function checkUrlSyntax(input, opts = {}) {
246
+ const rawAuthority = rawAuthorityOf(input);
247
+ let parsed;
248
+ try {
249
+ parsed = new URL(input);
250
+ }
251
+ catch {
252
+ return { ok: false, reason: 'empty_host', detail: 'unparseable URL', rawAuthority };
253
+ }
254
+ if (!ALLOWED_PROTOCOLS.has(parsed.protocol)) {
255
+ return { ok: false, reason: 'forbidden_protocol', detail: `protocol ${parsed.protocol} not allowed`, rawAuthority };
256
+ }
257
+ if (parsed.username !== '' || parsed.password !== '') {
258
+ return { ok: false, reason: 'userinfo_present', detail: 'userinfo (user:pass@) not allowed', rawAuthority };
259
+ }
260
+ // Normalize hostname: URL already lowercases, IDN-punycodes, and canonicalizes
261
+ // IPv6; strip a trailing dot ourselves.
262
+ let hostname = parsed.hostname;
263
+ if (hostname.endsWith('.'))
264
+ hostname = hostname.slice(0, -1);
265
+ if (hostname === '') {
266
+ return { ok: false, reason: 'empty_host', detail: 'empty host', rawAuthority };
267
+ }
268
+ const ipLiteral = asIpLiteral(hostname);
269
+ if (ipLiteral !== null) {
270
+ if (isForbiddenIp(ipLiteral)) {
271
+ return { ok: false, reason: 'forbidden_ip', detail: `literal IP ${ipLiteral} is forbidden`, rawAuthority };
272
+ }
273
+ if (!opts.allowLiteralIp) {
274
+ return { ok: false, reason: 'literal_ip_host', detail: `literal IP host ${ipLiteral} not allowed for navigation`, rawAuthority };
275
+ }
276
+ return { ok: true, url: parsed.toString(), hostname: ipLiteral, isIpLiteral: true };
277
+ }
278
+ return { ok: true, url: parsed.toString(), hostname, isIpLiteral: false };
279
+ }
280
+ /**
281
+ * Full policy: syntax check, then resolve every A/AAAA record for a domain host and
282
+ * reject if any resolved IP is forbidden (DNS precheck — see SECURITY BOUNDARY NOTE).
283
+ * IP-literal hosts skip DNS (already vetted by checkUrlSyntax).
284
+ */
285
+ export async function checkUrlPolicy(input, resolver, opts = {}) {
286
+ const syntax = checkUrlSyntax(input, opts);
287
+ if (!syntax.ok)
288
+ return syntax;
289
+ if (syntax.isIpLiteral)
290
+ return syntax;
291
+ let records;
292
+ try {
293
+ const [v4, v6] = await Promise.all([
294
+ resolver.resolve4(syntax.hostname).catch(() => []),
295
+ resolver.resolve6(syntax.hostname).catch(() => []),
296
+ ]);
297
+ records = [...v4, ...v6];
298
+ }
299
+ catch {
300
+ return { ok: false, reason: 'unresolvable_host', detail: `DNS resolution failed for ${syntax.hostname}` };
301
+ }
302
+ if (records.length === 0) {
303
+ return { ok: false, reason: 'no_dns_records', detail: `no A/AAAA records for ${syntax.hostname}` };
304
+ }
305
+ for (const ip of records) {
306
+ if (isForbiddenIp(ip)) {
307
+ return { ok: false, reason: 'forbidden_ip', detail: `${syntax.hostname} resolves to forbidden IP ${ip}` };
308
+ }
309
+ }
310
+ return syntax;
311
+ }
@@ -0,0 +1 @@
1
+ export {};