@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.
- package/cli-manifest.json +0 -61
- package/clis/twitter/search.js +3 -3
- package/dist/src/adapter-shadow.js +2 -2
- package/dist/src/browser/analyze.d.ts +10 -99
- package/dist/src/browser/analyze.js +10 -226
- package/dist/src/browser/cdp.js +67 -7
- package/dist/src/browser/daemon-client.d.ts +2 -0
- package/dist/src/browser/daemon-client.js +1 -1
- package/dist/src/browser/extension-capabilities.d.ts +13 -0
- package/dist/src/browser/extension-capabilities.js +22 -0
- package/dist/src/browser/extension-capabilities.test.d.ts +1 -0
- package/dist/src/browser/extension-version-metadata.test.d.ts +1 -0
- package/dist/src/browser/navigation-guard.d.ts +78 -0
- package/dist/src/browser/navigation-guard.js +142 -0
- package/dist/src/browser/navigation-guard.test.d.ts +1 -0
- package/dist/src/browser/page.d.ts +1 -0
- package/dist/src/browser/page.js +20 -1
- package/dist/src/browser/url-policy.d.ts +89 -0
- package/dist/src/browser/url-policy.js +311 -0
- package/dist/src/browser/url-policy.test.d.ts +1 -0
- package/dist/src/browser/verify-fixture.js +2 -2
- package/dist/src/build-manifest.js +4 -2
- package/dist/src/capabilityRouting.d.ts +3 -2
- package/dist/src/capabilityRouting.js +10 -2
- package/dist/src/cli.js +11 -14
- package/dist/src/commanderAdapter.js +5 -5
- package/dist/src/config-paths.d.ts +19 -0
- package/dist/src/config-paths.js +31 -0
- package/dist/src/daemon.js +256 -0
- package/dist/src/discovery.d.ts +10 -3
- package/dist/src/discovery.js +20 -10
- package/dist/src/discovery.test.d.ts +1 -0
- package/dist/src/execution.d.ts +5 -0
- package/dist/src/execution.js +272 -48
- package/dist/src/help.js +8 -8
- package/dist/src/main.js +18 -2
- package/dist/src/manifest-schema.d.ts +9 -0
- package/dist/src/manifest-schema.js +162 -0
- package/dist/src/manifest-schema.test.d.ts +1 -0
- package/dist/src/manifest-types.d.ts +1 -1
- package/dist/src/observation/redaction.js +10 -4
- package/dist/src/plugin.js +11 -8
- package/dist/src/recorder/highlevel/analyze.d.ts +29 -0
- package/dist/src/recorder/highlevel/analyze.js +91 -0
- package/dist/src/recorder/highlevel/init.d.ts +125 -0
- package/dist/src/recorder/highlevel/init.js +279 -0
- package/dist/src/recorder/highlevel/init.test.d.ts +1 -0
- package/dist/src/recorder/highlevel/verify.d.ts +73 -0
- package/dist/src/recorder/highlevel/verify.js +74 -0
- package/dist/src/recorder/http/analyze-runner.d.ts +28 -0
- package/dist/src/recorder/http/analyze-runner.js +78 -0
- package/dist/src/recorder/http/analyze-runner.test.d.ts +1 -0
- package/dist/src/recorder/http/handlers.d.ts +51 -0
- package/dist/src/recorder/http/handlers.js +219 -0
- package/dist/src/recorder/http/wrapper-config.d.ts +20 -0
- package/dist/src/recorder/http/wrapper-config.js +82 -0
- package/dist/src/recorder/http/wrapper-envelope.d.ts +31 -0
- package/dist/src/recorder/http/wrapper-envelope.js +51 -0
- package/dist/src/recorder/http/wrapper-gates.d.ts +16 -0
- package/dist/src/recorder/http/wrapper-gates.js +28 -0
- package/dist/src/recorder/http/wrapper-metrics.d.ts +1 -0
- package/dist/src/recorder/http/wrapper-metrics.js +5 -0
- package/dist/src/recorder/http/wrapper-registry.d.ts +50 -0
- package/dist/src/recorder/http/wrapper-registry.js +98 -0
- package/dist/src/recorder/http/wrapper-server.d.ts +59 -0
- package/dist/src/recorder/http/wrapper-server.js +240 -0
- package/dist/src/recorder/http/wrapper.test.d.ts +1 -0
- package/dist/src/recorder/observability/logger.d.ts +5 -0
- package/dist/src/recorder/observability/logger.js +13 -0
- package/dist/src/recorder/runner/config.d.ts +34 -0
- package/dist/src/recorder/runner/config.js +84 -0
- package/dist/src/recorder/runner/config.test.d.ts +1 -0
- package/dist/src/recorder/runner/reap.d.ts +79 -0
- package/dist/src/recorder/runner/reap.js +230 -0
- package/dist/src/recorder/runner/reap.test.d.ts +1 -0
- package/dist/src/recorder/runner/runner-port.d.ts +95 -0
- package/dist/src/recorder/runner/runner-port.js +518 -0
- package/dist/src/recorder/runner/runner-port.test.d.ts +1 -0
- package/dist/src/recorder/runner/session-keys.d.ts +28 -0
- package/dist/src/recorder/runner/session-keys.js +72 -0
- package/dist/src/recorder/runner/session-keys.test.d.ts +1 -0
- package/dist/src/recorder/runner/verify-runner-main.d.ts +96 -0
- package/dist/src/recorder/runner/verify-runner-main.js +292 -0
- package/dist/src/recorder/runner/verify-runner-main.test.d.ts +1 -0
- package/dist/src/registry-api.d.ts +1 -1
- package/dist/src/registry-api.types.test.d.ts +1 -0
- package/dist/src/registry-transaction.d.ts +42 -0
- package/dist/src/registry-transaction.js +194 -0
- package/dist/src/registry-transaction.test.d.ts +1 -0
- package/dist/src/registry.d.ts +58 -16
- package/dist/src/registry.js +131 -15
- package/dist/src/serialization.d.ts +1 -1
- package/dist/src/serialization.js +3 -3
- package/dist/src/types.d.ts +2 -0
- package/package.json +8 -5
- package/scripts/m1-daemon-navigate-drive.mjs +138 -0
- package/scripts/m1-extension-spike.mjs +237 -0
- package/scripts/m1-live-spike.mjs +156 -0
- package/scripts/m6b-connect-back-spike.sh +31 -0
- package/clis/12306/utils.test.js +0 -331
- package/clis/1688/assets.test.js +0 -39
- package/clis/1688/download.test.js +0 -31
- package/clis/1688/item.test.js +0 -67
- package/clis/1688/search.test.js +0 -75
- package/clis/1688/shared.test.js +0 -57
- package/clis/1688/store.test.js +0 -62
- package/clis/36kr/article.test.js +0 -46
- package/clis/36kr/hot.test.js +0 -15
- package/clis/36kr/news.test.js +0 -85
- package/clis/51job/utils.test.js +0 -69
- package/clis/_atlassian/shared.test.js +0 -170
- package/clis/aibase/news.test.js +0 -59
- package/clis/amazon/bestsellers.test.js +0 -29
- package/clis/amazon/discussion.test.js +0 -124
- package/clis/amazon/offer.test.js +0 -29
- package/clis/amazon/product.test.js +0 -24
- package/clis/amazon/rankings.test.js +0 -41
- package/clis/amazon/search.test.js +0 -22
- package/clis/amazon/shared.test.js +0 -44
- package/clis/apple-podcasts/commands.test.js +0 -119
- package/clis/apple-podcasts/utils.test.js +0 -57
- package/clis/arxiv/arxiv.test.js +0 -112
- package/clis/baidu-scholar/search.test.js +0 -23
- package/clis/barchart/greeks.test.js +0 -138
- package/clis/bilibili/comment.test.js +0 -153
- package/clis/bilibili/comments.test.js +0 -142
- package/clis/bilibili/dynamic.test.js +0 -68
- package/clis/bilibili/hot.test.js +0 -17
- package/clis/bilibili/subtitle.test.js +0 -196
- package/clis/bilibili/summary.test.js +0 -210
- package/clis/bilibili/utils.test.js +0 -61
- package/clis/bilibili/video.test.js +0 -132
- package/clis/binance/commands.test.js +0 -70
- package/clis/bloomberg/utils.test.js +0 -129
- package/clis/booking/booking.test.js +0 -356
- package/clis/boss/chatlist.test.js +0 -211
- package/clis/boss/chatmsg.test.js +0 -230
- package/clis/boss/search.test.js +0 -78
- package/clis/boss/utils.test.js +0 -34
- package/clis/brave/search.test.js +0 -76
- package/clis/chaoxing/utils.test.js +0 -45
- package/clis/chatgpt/commands.test.js +0 -45
- package/clis/chatgpt/envelope.test.js +0 -108
- package/clis/chatgpt/image.test.js +0 -188
- package/clis/chatgpt/utils.test.js +0 -330
- package/clis/chatgpt-app/ax.test.js +0 -35
- package/clis/chatwise/composer.test.js +0 -186
- package/clis/chess/analyze.test.js +0 -79
- package/clis/chess/game.test.js +0 -178
- package/clis/chess/games.test.js +0 -164
- package/clis/chess/stats.test.js +0 -79
- package/clis/chess/utils.test.js +0 -230
- package/clis/claude/ask.test.js +0 -338
- package/clis/claude/commands.test.js +0 -118
- package/clis/claude/utils.test.js +0 -148
- package/clis/cnki/search.test.js +0 -18
- package/clis/codex/sidebar.test.js +0 -329
- package/clis/coingecko/coingecko.test.js +0 -109
- package/clis/confluence/commands.test.js +0 -195
- package/clis/coupang/coupang.test.js +0 -159
- package/clis/coupang/utils.test.js +0 -62
- package/clis/ctrip/ctrip.test.js +0 -719
- package/clis/dblp/dblp.test.js +0 -397
- package/clis/deepseek/ask.test.js +0 -312
- package/clis/deepseek/detail.test.js +0 -81
- package/clis/deepseek/send.test.js +0 -107
- package/clis/deepseek/utils.test.js +0 -370
- package/clis/defillama/defillama.test.js +0 -99
- package/clis/devto/devto.test.js +0 -236
- package/clis/dianping/dianping.test.js +0 -578
- package/clis/douban/download.test.js +0 -170
- package/clis/douban/movie-hot.test.js +0 -14
- package/clis/douban/search.test.js +0 -11
- package/clis/douban/subject.test.js +0 -11
- package/clis/douban/utils.test.js +0 -438
- package/clis/doubao/detail.test.js +0 -42
- package/clis/doubao/history.test.js +0 -37
- package/clis/doubao/utils.test.js +0 -344
- package/clis/douyin/_shared/browser-fetch.test.js +0 -64
- package/clis/douyin/_shared/creation-id.test.js +0 -22
- package/clis/douyin/_shared/imagex-upload.test.js +0 -87
- package/clis/douyin/_shared/sts2.test.js +0 -27
- package/clis/douyin/_shared/text-extra.test.js +0 -37
- package/clis/douyin/_shared/timing.test.js +0 -28
- package/clis/douyin/_shared/tos-upload-short-read.test.js +0 -83
- package/clis/douyin/_shared/tos-upload.test.js +0 -229
- package/clis/douyin/_shared/transcode.test.js +0 -91
- package/clis/douyin/_shared/vod-upload.test.js +0 -38
- package/clis/douyin/activities.test.js +0 -55
- package/clis/douyin/collections.test.js +0 -45
- package/clis/douyin/delete.test.js +0 -100
- package/clis/douyin/draft.test.js +0 -335
- package/clis/douyin/drafts.test.js +0 -11
- package/clis/douyin/hashtag.test.js +0 -171
- package/clis/douyin/location.test.js +0 -23
- package/clis/douyin/profile.test.js +0 -46
- package/clis/douyin/publish-upload-id.test.js +0 -170
- package/clis/douyin/publish.test.js +0 -38
- package/clis/douyin/stats.test.js +0 -22
- package/clis/douyin/update.test.js +0 -11
- package/clis/douyin/user-videos.test.js +0 -151
- package/clis/douyin/videos.test.js +0 -54
- package/clis/duckduckgo/search.test.js +0 -128
- package/clis/duckduckgo/suggest.test.js +0 -66
- package/clis/eastmoney/hot-rank.test.js +0 -59
- package/clis/endoflife/endoflife.test.js +0 -51
- package/clis/facebook/feed.test.js +0 -169
- package/clis/facebook/marketplace.test.js +0 -91
- package/clis/facebook/notifications.test.js +0 -458
- package/clis/facebook/search.test.js +0 -55
- package/clis/flathub/flathub.test.js +0 -90
- package/clis/flomo/memos.test.js +0 -144
- package/clis/gemini/ask.test.js +0 -100
- package/clis/gemini/deep-research-result.test.js +0 -155
- package/clis/gemini/deep-research.test.js +0 -181
- package/clis/gemini/reply-state.test.js +0 -671
- package/clis/gemini/utils.test.js +0 -198
- package/clis/geogebra/geogebra.test.js +0 -175
- package/clis/gitee/search.test.js +0 -65
- package/clis/gitee/user.test.js +0 -63
- package/clis/google/utils.test.js +0 -75
- package/clis/google-scholar/cite.test.js +0 -47
- package/clis/google-scholar/profile.test.js +0 -49
- package/clis/google-scholar/search.test.js +0 -71
- package/clis/goproxy/goproxy.test.js +0 -103
- package/clis/gov-law/commands.test.js +0 -39
- package/clis/gov-policy/gov-policy.test.js +0 -224
- package/clis/grok/ask.test.js +0 -29
- package/clis/grok/utils.test.js +0 -103
- package/clis/hackernews/hackernews.test.js +0 -132
- package/clis/hf/hf.test.js +0 -16
- package/clis/hupu/hot.test.js +0 -224
- package/clis/imdb/utils.test.js +0 -88
- package/clis/indeed/indeed.test.js +0 -375
- package/clis/instagram/_shared/private-publish.test.js +0 -705
- package/clis/instagram/_shared/protocol-capture.test.js +0 -114
- package/clis/instagram/download.test.js +0 -118
- package/clis/instagram/note.test.js +0 -53
- package/clis/instagram/post.test.js +0 -1647
- package/clis/instagram/reel.test.js +0 -167
- package/clis/instagram/story.test.js +0 -134
- package/clis/jd/commands.test.js +0 -41
- package/clis/jd/item.test.js +0 -354
- package/clis/jianyu/search.test.js +0 -153
- package/clis/jianyu/shared/procurement-contract.test.js +0 -72
- package/clis/jianyu/shared/procurement-detail.test.js +0 -84
- package/clis/jike/read.test.js +0 -86
- package/clis/jira/commands.test.js +0 -287
- package/clis/juejin/search.js +0 -123
- package/clis/juejin/search.test.js +0 -98
- package/clis/lesswrong/frontpage.test.js +0 -37
- package/clis/lichess/lichess.test.js +0 -85
- package/clis/linkedin/connect.test.js +0 -213
- package/clis/linkedin/inbox.test.js +0 -152
- package/clis/linkedin/job-detail.test.js +0 -38
- package/clis/linkedin/jobs-preferences.test.js +0 -43
- package/clis/linkedin/people-search.test.js +0 -216
- package/clis/linkedin/post-analytics.test.js +0 -40
- package/clis/linkedin/posts.test.js +0 -40
- package/clis/linkedin/profile-analytics.test.js +0 -67
- package/clis/linkedin/profile-experience.test.js +0 -152
- package/clis/linkedin/profile-projects.test.js +0 -111
- package/clis/linkedin/profile-read.test.js +0 -77
- package/clis/linkedin/safe-send.test.js +0 -204
- package/clis/linkedin/salesnav-inbox.test.js +0 -113
- package/clis/linkedin/salesnav-message.test.js +0 -172
- package/clis/linkedin/salesnav-search.test.js +0 -76
- package/clis/linkedin/salesnav-thread.test.js +0 -79
- package/clis/linkedin/search.test.js +0 -222
- package/clis/linkedin/sent-invitations.test.js +0 -62
- package/clis/linkedin/services-read.test.js +0 -105
- package/clis/linkedin/thread-snapshot.test.js +0 -89
- package/clis/linkedin/timeline.test.js +0 -81
- package/clis/linkedin-learning/course.test.js +0 -114
- package/clis/linkedin-learning/search.test.js +0 -144
- package/clis/linkedin-learning/trending.test.js +0 -123
- package/clis/linux-do/feed.test.js +0 -153
- package/clis/linux-do/topic-content.test.js +0 -59
- package/clis/lobsters/lobsters.test.js +0 -169
- package/clis/notebooklm/add-source.test.js +0 -97
- package/clis/notebooklm/compat.test.js +0 -16
- package/clis/notebooklm/create.test.js +0 -58
- package/clis/notebooklm/generate-audio.test.js +0 -63
- package/clis/notebooklm/generate-slides.test.js +0 -75
- package/clis/notebooklm/history.test.js +0 -58
- package/clis/notebooklm/note-list.test.js +0 -56
- package/clis/notebooklm/notes-get.test.js +0 -72
- package/clis/notebooklm/open.test.js +0 -63
- package/clis/notebooklm/rpc.test.js +0 -131
- package/clis/notebooklm/source-fulltext.test.js +0 -106
- package/clis/notebooklm/source-get.test.js +0 -84
- package/clis/notebooklm/source-guide.test.js +0 -104
- package/clis/notebooklm/summary.test.js +0 -78
- package/clis/notebooklm/utils.test.js +0 -449
- package/clis/notebooklm/write-note.test.js +0 -70
- package/clis/nuget/nuget.test.js +0 -111
- package/clis/oeis/oeis.test.js +0 -88
- package/clis/ones/task-helpers.test.js +0 -12
- package/clis/ones/worklog.test.js +0 -20
- package/clis/openfda/openfda.test.js +0 -114
- package/clis/openreview/openreview.test.js +0 -427
- package/clis/osv/osv.test.js +0 -97
- package/clis/paperreview/commands.test.js +0 -243
- package/clis/paperreview/utils.test.js +0 -49
- package/clis/pixiv/download.test.js +0 -87
- package/clis/pixiv/illusts.test.js +0 -99
- package/clis/pixiv/search.test.js +0 -83
- package/clis/powerchina/search.test.js +0 -93
- package/clis/producthunt/utils.test.js +0 -64
- package/clis/pubmed/pubmed.test.js +0 -276
- package/clis/quark/utils.test.js +0 -58
- package/clis/qwen/utils.test.js +0 -45
- package/clis/reddit/extract-media.test.js +0 -149
- package/clis/reddit/frontpage.test.js +0 -34
- package/clis/reddit/home.test.js +0 -170
- package/clis/reddit/hot.test.js +0 -32
- package/clis/reddit/popular.test.js +0 -26
- package/clis/reddit/read.test.js +0 -335
- package/clis/reddit/reply.test.js +0 -89
- package/clis/reddit/search.test.js +0 -26
- package/clis/reddit/subreddit-info.test.js +0 -163
- package/clis/reddit/subreddit.test.js +0 -31
- package/clis/reddit/subscribed.test.js +0 -168
- package/clis/reddit/whoami.test.js +0 -105
- package/clis/rednote/navigation.test.js +0 -26
- package/clis/rednote/rednote.test.js +0 -222
- package/clis/rest-countries/rest-countries.test.js +0 -83
- package/clis/reuters/reuters.test.js +0 -299
- package/clis/rfc/rfc.test.js +0 -74
- package/clis/sinafinance/stock.test.js +0 -59
- package/clis/spotify/utils.test.js +0 -67
- package/clis/stackoverflow/stackoverflow.test.js +0 -404
- package/clis/steam/steam.test.js +0 -46
- package/clis/substack/utils.test.js +0 -44
- package/clis/suno/commands.test.js +0 -188
- package/clis/suno/download.test.js +0 -151
- package/clis/suno/generate.test.js +0 -252
- package/clis/suno/utils.test.js +0 -329
- package/clis/taobao/commands.test.js +0 -41
- package/clis/tdx/hot-rank.test.js +0 -59
- package/clis/ths/hot-rank.test.js +0 -64
- package/clis/tieba/commands.test.js +0 -79
- package/clis/tieba/utils.test.js +0 -290
- package/clis/tiktok/creator-videos.test.js +0 -113
- package/clis/tiktok/refactor.test.js +0 -389
- package/clis/tiktok/write-refactor.test.js +0 -370
- package/clis/toutiao/toutiao.test.js +0 -378
- package/clis/tvmaze/tvmaze.test.js +0 -93
- package/clis/twitter/bookmark-folder.test.js +0 -395
- package/clis/twitter/bookmark-folders.test.js +0 -152
- package/clis/twitter/bookmark.test.js +0 -74
- package/clis/twitter/bookmarks.test.js +0 -205
- package/clis/twitter/delete.test.js +0 -85
- package/clis/twitter/device-follow.test.js +0 -287
- package/clis/twitter/download.test.js +0 -457
- package/clis/twitter/followers.test.js +0 -62
- package/clis/twitter/following.test.js +0 -338
- package/clis/twitter/hide-reply.test.js +0 -76
- package/clis/twitter/like.test.js +0 -73
- package/clis/twitter/likes.test.js +0 -220
- package/clis/twitter/list-add.test.js +0 -133
- package/clis/twitter/list-create.test.js +0 -169
- package/clis/twitter/list-remove.test.js +0 -110
- package/clis/twitter/list-tweets.test.js +0 -196
- package/clis/twitter/lists.test.js +0 -253
- package/clis/twitter/post.test.js +0 -258
- package/clis/twitter/profile.test.js +0 -39
- package/clis/twitter/quote.test.js +0 -194
- package/clis/twitter/reply.test.js +0 -237
- package/clis/twitter/retweet.test.js +0 -73
- package/clis/twitter/search.test.js +0 -349
- package/clis/twitter/shared.test.js +0 -794
- package/clis/twitter/thread.test.js +0 -58
- package/clis/twitter/timeline.test.js +0 -104
- package/clis/twitter/trending.test.js +0 -15
- package/clis/twitter/tweets.test.js +0 -362
- package/clis/twitter/unbookmark.test.js +0 -73
- package/clis/twitter/unlike.test.js +0 -75
- package/clis/twitter/unretweet.test.js +0 -73
- package/clis/twitter/utils.test.js +0 -169
- package/clis/uisdc/news.test.js +0 -66
- package/clis/uiverse/_shared.test.js +0 -62
- package/clis/uiverse/navigation.test.js +0 -12
- package/clis/upwork/upwork.test.js +0 -566
- package/clis/wanfang/search.test.js +0 -23
- package/clis/web/read.test.js +0 -392
- package/clis/weibo/delete.test.js +0 -94
- package/clis/weibo/envelope.test.js +0 -85
- package/clis/weibo/favorites.test.js +0 -114
- package/clis/weibo/publish.test.js +0 -192
- package/clis/weibo/user-posts.test.js +0 -92
- package/clis/weibo/utils.test.js +0 -36
- package/clis/weixin/drafts.test.js +0 -69
- package/clis/weixin/search.test.js +0 -227
- package/clis/weread/ai-outline.test.js +0 -83
- package/clis/weread/book-search.test.js +0 -242
- package/clis/weread/commands.test.js +0 -398
- package/clis/weread/private-api-regression.test.js +0 -369
- package/clis/weread/search-regression.test.js +0 -494
- package/clis/weread/utils.test.js +0 -128
- package/clis/weread-official/commands.test.js +0 -385
- package/clis/weread-official/utils.test.js +0 -242
- package/clis/wikidata/wikidata.test.js +0 -83
- package/clis/wikipedia/trending.test.js +0 -57
- package/clis/wttr/wttr.test.js +0 -84
- package/clis/xianyu/chat.test.js +0 -84
- package/clis/xianyu/im.test.js +0 -253
- package/clis/xianyu/item.test.js +0 -56
- package/clis/xianyu/publish.test.js +0 -220
- package/clis/xianyu/search.test.js +0 -17
- package/clis/xiaoe/xiaoe.test.js +0 -486
- package/clis/xiaohongshu/comments.test.js +0 -258
- package/clis/xiaohongshu/creator-note-detail.test.js +0 -451
- package/clis/xiaohongshu/creator-notes-summary.test.js +0 -94
- package/clis/xiaohongshu/creator-notes.test.js +0 -281
- package/clis/xiaohongshu/creator-stats.test.js +0 -24
- package/clis/xiaohongshu/delete-note.test.js +0 -172
- package/clis/xiaohongshu/download.test.js +0 -316
- package/clis/xiaohongshu/navigation.test.js +0 -34
- package/clis/xiaohongshu/note.test.js +0 -249
- package/clis/xiaohongshu/publish.test.js +0 -608
- package/clis/xiaohongshu/search.test.js +0 -325
- package/clis/xiaohongshu/user-helpers.test.js +0 -160
- package/clis/xiaoyuzhou/auth.test.js +0 -124
- package/clis/xiaoyuzhou/download.test.js +0 -135
- package/clis/xiaoyuzhou/podcast-episodes.test.js +0 -78
- package/clis/xiaoyuzhou/transcript.test.js +0 -195
- package/clis/xiaoyuzhou/utils.test.js +0 -39
- package/clis/xueqiu/comments.test.js +0 -696
- package/clis/xueqiu/danjuan-utils.test.js +0 -41
- package/clis/xueqiu/utils.test.js +0 -26
- package/clis/yahoo/search.test.js +0 -94
- package/clis/youdao/note.test.js +0 -99
- package/clis/youtube/channel.test.js +0 -59
- package/clis/youtube/feed.test.js +0 -131
- package/clis/youtube/transcript-group.test.js +0 -99
- package/clis/youtube/transcript.test.js +0 -296
- package/clis/youtube/utils.test.js +0 -84
- package/clis/yuanbao/ask.test.js +0 -124
- package/clis/yuanbao/new.test.js +0 -30
- package/clis/yuanbao/shared.test.js +0 -80
- package/clis/zhihu/answer-comments.test.js +0 -287
- package/clis/zhihu/answer-detail.test.js +0 -338
- package/clis/zhihu/answer.test.js +0 -54
- package/clis/zhihu/collection.test.js +0 -337
- package/clis/zhihu/collections.test.js +0 -182
- package/clis/zhihu/comment.test.js +0 -50
- package/clis/zhihu/download.test.js +0 -12
- package/clis/zhihu/favorite.test.js +0 -59
- package/clis/zhihu/follow.test.js +0 -42
- package/clis/zhihu/like.test.js +0 -41
- package/clis/zhihu/question.test.js +0 -251
- package/clis/zhihu/recommend.test.js +0 -143
- package/clis/zhihu/search.test.js +0 -198
- package/clis/zhihu/target.test.js +0 -77
- package/clis/zhihu/text.test.js +0 -24
- package/clis/zhihu/write-shared.test.js +0 -176
- package/clis/zlibrary/commands.test.js +0 -65
- package/clis/zsxq/search.test.js +0 -24
- package/clis/zsxq/topic.test.js +0 -30
- package/clis/zsxq/topics.test.js +0 -24
- package/clis/zsxq/utils.test.js +0 -31
- package/dist/src/adapter-shadow.test.js +0 -49
- package/dist/src/adapter-source.test.js +0 -29
- package/dist/src/browser/analyze.test.js +0 -166
- package/dist/src/browser/article-extract.e2e.test.js +0 -105
- package/dist/src/browser/article-extract.test.js +0 -94
- package/dist/src/browser/ax-snapshot.test.js +0 -91
- package/dist/src/browser/base-page.test.js +0 -758
- package/dist/src/browser/cdp-click-fixture.test.js +0 -87
- package/dist/src/browser/cdp.test.js +0 -79
- package/dist/src/browser/compound.test.js +0 -175
- package/dist/src/browser/daemon-client.test.js +0 -199
- package/dist/src/browser/dom-helpers.test.js +0 -106
- package/dist/src/browser/dom-snapshot.test.js +0 -313
- package/dist/src/browser/errors.test.js +0 -60
- package/dist/src/browser/extract.test.js +0 -129
- package/dist/src/browser/find.test.js +0 -180
- package/dist/src/browser/html-tree.test.js +0 -181
- package/dist/src/browser/network-cache.test.js +0 -58
- package/dist/src/browser/network-key.test.js +0 -49
- package/dist/src/browser/page.test.js +0 -378
- package/dist/src/browser/shape-filter.test.js +0 -101
- package/dist/src/browser/shape.test.js +0 -82
- package/dist/src/browser/stealth.test.js +0 -134
- package/dist/src/browser/target-errors.test.js +0 -94
- package/dist/src/browser/target-resolver.test.js +0 -118
- package/dist/src/browser/utils.test.js +0 -29
- package/dist/src/browser/verify-fixture.test.js +0 -219
- package/dist/src/browser.test.js +0 -232
- package/dist/src/build-manifest.test.js +0 -341
- package/dist/src/capabilityRouting.test.js +0 -63
- package/dist/src/cli-argv-preprocess.test.js +0 -209
- package/dist/src/cli.test.js +0 -2803
- package/dist/src/commanderAdapter.test.js +0 -352
- package/dist/src/commands/daemon.test.js +0 -219
- package/dist/src/completion.test.js +0 -24
- package/dist/src/convention-audit.test.js +0 -247
- package/dist/src/daemon.test.js +0 -60
- package/dist/src/doctor.test.js +0 -263
- package/dist/src/download/article-download.test.js +0 -235
- package/dist/src/download/index.test.js +0 -140
- package/dist/src/download/media-download.test.js +0 -112
- package/dist/src/download/progress.test.js +0 -36
- package/dist/src/electron-apps.test.js +0 -49
- package/dist/src/engine.test.js +0 -312
- package/dist/src/errors.test.js +0 -109
- package/dist/src/execution.test.js +0 -623
- package/dist/src/extension-manifest-regression.test.js +0 -12
- package/dist/src/external.test.js +0 -146
- package/dist/src/help.test.js +0 -58
- package/dist/src/hooks.test.js +0 -92
- package/dist/src/interceptor.test.js +0 -81
- package/dist/src/launcher.test.js +0 -138
- package/dist/src/node-network.test.js +0 -55
- package/dist/src/observation/artifact.test.js +0 -121
- package/dist/src/observation/manager.test.js +0 -13
- package/dist/src/observation/redaction.test.js +0 -32
- package/dist/src/observation/retention.test.js +0 -118
- package/dist/src/observation/ring-buffer.test.js +0 -22
- package/dist/src/output.test.js +0 -53
- package/dist/src/package-exports.test.js +0 -112
- package/dist/src/pipeline/executor.test.js +0 -182
- package/dist/src/pipeline/steps/download.test.js +0 -102
- package/dist/src/pipeline/steps/fetch.test.js +0 -117
- package/dist/src/pipeline/template.test.js +0 -175
- package/dist/src/pipeline/transform.test.js +0 -133
- package/dist/src/plugin-manifest.test.js +0 -179
- package/dist/src/plugin-scaffold.test.js +0 -83
- package/dist/src/plugin.test.js +0 -1324
- package/dist/src/registry.test.js +0 -209
- package/dist/src/runtime-detect.test.js +0 -39
- package/dist/src/serialization.test.js +0 -78
- package/dist/src/snapshotFormatter.test.js +0 -521
- package/dist/src/tui.test.js +0 -19
- package/dist/src/update-check.test.js +0 -116
- package/dist/src/utils.test.js +0 -155
- package/dist/src/validate.test.js +0 -90
- 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[]>;
|
package/dist/src/browser/page.js
CHANGED
|
@@ -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 {};
|