@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,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real RunnerPort (M6a · 08) — the child-process verify runner mechanism.
|
|
3
|
+
*
|
|
4
|
+
* Replaces M5c's stubRunnerPort. Responsibilities (08 Process Rules):
|
|
5
|
+
* - create requestId BEFORE launch; async registry of in-flight runs
|
|
6
|
+
* - write input.json securely: per-request `0700` temp dir (owner/mode/realpath/lstat
|
|
7
|
+
* checked), exclusive `0600` file; raw seed args live here only, never in status/log
|
|
8
|
+
* - spawn `bycli internal verify-runner --jsonl …` with an args array (no shell), minimal env
|
|
9
|
+
* - parse stdout JSONL via the pure parseRunnerEvent; one and only one terminal `result`
|
|
10
|
+
* - hard timeout → SIGTERM (graceful) → SIGKILL; stdout/stderr byte caps
|
|
11
|
+
* - cancel is idempotent; temp dir is always cleaned on done/cancel/timeout
|
|
12
|
+
*
|
|
13
|
+
* M6a does NOT implement: startup reap, browser-adapter execution (needs a daemon Page),
|
|
14
|
+
* concurrency-queue refinement — those are M6b/c. Browser adapters report not-yet from the
|
|
15
|
+
* runner itself (see verify-runner-main.ts).
|
|
16
|
+
*
|
|
17
|
+
* SECURITY: `rawSeedArgs` go into input.json (0600) only. getVerifyStatus returns a
|
|
18
|
+
* summary-only VerifySummary (normalizeRunnerResult) — raw values never reach it.
|
|
19
|
+
*/
|
|
20
|
+
import { spawn as nodeSpawn } from 'node:child_process';
|
|
21
|
+
import * as fs from 'node:fs';
|
|
22
|
+
import * as os from 'node:os';
|
|
23
|
+
import * as path from 'node:path';
|
|
24
|
+
import { randomUUID } from 'node:crypto';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
import { getUserClisDir } from '../../config-paths.js';
|
|
27
|
+
import { parseRunnerEvent, normalizeRunnerResult, buildRunnerArgs, createMetrics, } from '@sovovs/bycli-recorder-core';
|
|
28
|
+
import { createRecorderLogger } from '../observability/logger.js';
|
|
29
|
+
import { resolveRunnerConfig } from './config.js';
|
|
30
|
+
/** How long a terminal run lingers in `runs` for status reads before GC (Codex M9 review · High):
|
|
31
|
+
* long enough for getRunStatus/whenSettled consumers (incl. the M9 wrapper's background finalize)
|
|
32
|
+
* to read the summary, short enough that the map stays bounded. */
|
|
33
|
+
const TERMINAL_RUN_RETENTION_MS = 300_000;
|
|
34
|
+
/** Sentinel input swapped in once a run is terminal — drops the retained StartInput (rawSeedArgs +
|
|
35
|
+
* evidence) so no raw seed / run input lingers in memory after settle. */
|
|
36
|
+
const MINIMAL_INPUT = { name: '', evidenceSeedArgs: {}, rawSeedArgs: {}, fixture: '', trace: '' };
|
|
37
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
/** dist/src/recorder/runner/runner-port.js → dist/src/main.js (same relative layout in src/). */
|
|
39
|
+
const DEFAULT_MAIN_ENTRY = path.resolve(__dirname, '../../main.js');
|
|
40
|
+
/** Dedicated protocol fd (Codex #3): the child writes JSONL here, not stdout, so an adapter's
|
|
41
|
+
* console.log / process.stdout.write can never pollute or forge the protocol stream. The child is
|
|
42
|
+
* spawned with stdio `['ignore','ignore','ignore','pipe']` — adapter stdout/stderr go to /dev/null. */
|
|
43
|
+
const PROTOCOL_FD = 3;
|
|
44
|
+
function defaultResolveAdapterPath(name) {
|
|
45
|
+
const slash = name.indexOf('/');
|
|
46
|
+
const site = slash === -1 ? name : name.slice(0, slash);
|
|
47
|
+
const command = slash === -1 ? name : name.slice(slash + 1);
|
|
48
|
+
return path.join(getUserClisDir(), site, `${command}.js`);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Create a per-request temp dir + input.json with the 08 security guarantees (POSIX:
|
|
52
|
+
* 0700 dir, owner/mode/realpath/lstat checks, 0600 exclusive file). Returns the real temp
|
|
53
|
+
* root and input path. Raw seed args are written here and nowhere else.
|
|
54
|
+
*/
|
|
55
|
+
function writeInputJson(requestId, payload) {
|
|
56
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'bycli-verify-'));
|
|
57
|
+
fs.chmodSync(tempRoot, 0o700);
|
|
58
|
+
const st = fs.lstatSync(tempRoot);
|
|
59
|
+
if (!st.isDirectory() || st.isSymbolicLink())
|
|
60
|
+
throw new Error('verify temp root is not a real directory');
|
|
61
|
+
if (typeof process.getuid === 'function' && st.uid !== process.getuid())
|
|
62
|
+
throw new Error('verify temp root owner mismatch');
|
|
63
|
+
if (typeof process.getuid === 'function' && (st.mode & 0o777) !== 0o700)
|
|
64
|
+
throw new Error('verify temp root mode mismatch');
|
|
65
|
+
const realRoot = fs.realpathSync(tempRoot);
|
|
66
|
+
// 08 child marker: cleanup manifest, no seed args / tokens. `ownerPid` is the process that
|
|
67
|
+
// spawned this runner (daemon or CLI) and stands in for 08's localServiceRunId: startup reap
|
|
68
|
+
// only touches runs whose ownerPid is DEAD (true orphans), so a live sibling's in-flight
|
|
69
|
+
// verify is never killed. `pid` (the child) is backfilled by updateMarkerPid after spawn.
|
|
70
|
+
const markerPath = path.join(realRoot, 'marker.json');
|
|
71
|
+
fs.writeFileSync(markerPath, JSON.stringify({ requestId, pid: null, ownerPid: process.pid, startedAt: Date.now(), tempRoot: realRoot, operation: 'verify' }), { mode: 0o600 });
|
|
72
|
+
const inputPath = path.join(realRoot, `${requestId}-input.json`);
|
|
73
|
+
const fd = fs.openSync(inputPath, 'wx', 0o600); // exclusive create, fails if exists
|
|
74
|
+
try {
|
|
75
|
+
fs.writeSync(fd, JSON.stringify(payload));
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
fs.closeSync(fd);
|
|
79
|
+
}
|
|
80
|
+
return { tempRoot: realRoot, inputPath };
|
|
81
|
+
}
|
|
82
|
+
function safeCleanup(tempRoot) {
|
|
83
|
+
try {
|
|
84
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
catch { /* best-effort; startup reap (M6b) is the backstop */ }
|
|
87
|
+
}
|
|
88
|
+
/** Best-effort byte size of one verify temp dir; per-entry races (a vanished file mid-scan) are skipped. */
|
|
89
|
+
function dirSizeBestEffort(dir) {
|
|
90
|
+
let total = 0;
|
|
91
|
+
let entries;
|
|
92
|
+
try {
|
|
93
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return 0;
|
|
97
|
+
}
|
|
98
|
+
for (const e of entries) {
|
|
99
|
+
const p = path.join(dir, e.name);
|
|
100
|
+
try {
|
|
101
|
+
if (e.isDirectory())
|
|
102
|
+
total += dirSizeBestEffort(p);
|
|
103
|
+
else if (e.isFile())
|
|
104
|
+
total += fs.statSync(p).size;
|
|
105
|
+
}
|
|
106
|
+
catch { /* entry vanished (race) — skip */ }
|
|
107
|
+
}
|
|
108
|
+
return total;
|
|
109
|
+
}
|
|
110
|
+
/** Default temp-usage measurement (#1d): sum of all `bycli-verify-*` dirs under os.tmpdir(). The
|
|
111
|
+
* top-level readdir is NOT guarded — an unreadable tmpdir throws, which the caller treats as
|
|
112
|
+
* over-capacity (fail-closed). Per-dir scanning is best-effort (races are normal). */
|
|
113
|
+
function defaultMeasureTempBytes() {
|
|
114
|
+
const root = os.tmpdir();
|
|
115
|
+
let total = 0;
|
|
116
|
+
for (const e of fs.readdirSync(root, { withFileTypes: true })) {
|
|
117
|
+
if (e.isDirectory() && e.name.startsWith('bycli-verify-'))
|
|
118
|
+
total += dirSizeBestEffort(path.join(root, e.name));
|
|
119
|
+
}
|
|
120
|
+
return total;
|
|
121
|
+
}
|
|
122
|
+
/** Backfill the spawned child pid into marker.json (08 startup reap needs the live pid). */
|
|
123
|
+
function updateMarkerPid(tempRoot, pid) {
|
|
124
|
+
const markerPath = path.join(tempRoot, 'marker.json');
|
|
125
|
+
try {
|
|
126
|
+
const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
|
|
127
|
+
marker.pid = pid;
|
|
128
|
+
fs.writeFileSync(markerPath, JSON.stringify(marker), { mode: 0o600 });
|
|
129
|
+
}
|
|
130
|
+
catch { /* marker missing/unreadable — reap falls back to temp deletion only */ }
|
|
131
|
+
}
|
|
132
|
+
/** Minimal child env (08 isolation): no inherited NODE_OPTIONS, scratch HOME/config/cache,
|
|
133
|
+
* daemon port so the child can reach the daemon `/command` surface (M6b browser adapters).
|
|
134
|
+
* Deliberately omits the user env: no NODE_OPTIONS, no sensitive tokens. The scratch
|
|
135
|
+
* HOME + BYCLI_CONFIG_DIR mean the child cannot read the user's adapters/cookies/config. */
|
|
136
|
+
function buildChildEnv(tempRoot, daemonPort) {
|
|
137
|
+
const env = {
|
|
138
|
+
HOME: tempRoot,
|
|
139
|
+
PATH: process.env.PATH ?? '/usr/bin:/bin',
|
|
140
|
+
BYCLI_CONFIG_DIR: tempRoot,
|
|
141
|
+
BYCLI_CACHE_DIR: tempRoot,
|
|
142
|
+
};
|
|
143
|
+
if (daemonPort !== undefined)
|
|
144
|
+
env.BYCLI_DAEMON_PORT = String(daemonPort);
|
|
145
|
+
return env;
|
|
146
|
+
}
|
|
147
|
+
export function createRunnerPort(opts = {}) {
|
|
148
|
+
const config = opts.config ?? resolveRunnerConfig();
|
|
149
|
+
const launcher = opts.launcher ?? { command: process.execPath, prefixArgs: [DEFAULT_MAIN_ENTRY] };
|
|
150
|
+
const resolveAdapterPath = opts.resolveAdapterPath ?? defaultResolveAdapterPath;
|
|
151
|
+
const spawnImpl = opts.spawnImpl ?? nodeSpawn;
|
|
152
|
+
const terminalRetentionMs = opts.terminalRetentionMs ?? TERMINAL_RUN_RETENTION_MS;
|
|
153
|
+
// #1c observability: default to a throwaway registry + silent logger so standalone/test construction
|
|
154
|
+
// stays zero-config and the existing runner tests don't change. The daemon injects shared instances.
|
|
155
|
+
const metrics = opts.metrics ?? createMetrics();
|
|
156
|
+
const logger = opts.logger ?? createRecorderLogger('error', () => { });
|
|
157
|
+
// #1d temp guard (off unless tempCapacity injected — only the daemon enforces).
|
|
158
|
+
const tempCapacity = opts.tempCapacity;
|
|
159
|
+
const measureTempBytes = opts.measureTempBytes ?? defaultMeasureTempBytes;
|
|
160
|
+
const sweepTemp = opts.sweepTemp ?? (() => { });
|
|
161
|
+
const runs = new Map();
|
|
162
|
+
const queue = []; // requestIds in 'queued' state, FIFO
|
|
163
|
+
const runningCount = () => { let n = 0; for (const r of runs.values())
|
|
164
|
+
if (r.status === 'running')
|
|
165
|
+
n++; return n; };
|
|
166
|
+
/** Reach a terminal state exactly once (idempotent). Clears the kill + result-grace timers,
|
|
167
|
+
* cleans temp, resolves, then pulls the next queued run into the freed slot. The grace timer
|
|
168
|
+
* (a pending SIGKILL after a timeout) deliberately survives — it is cleared on child exit. */
|
|
169
|
+
function settle(run, status, summary) {
|
|
170
|
+
if (run.status !== 'running' && run.status !== 'queued')
|
|
171
|
+
return; // already terminal
|
|
172
|
+
run.status = status;
|
|
173
|
+
run.summary = summary;
|
|
174
|
+
// #1c observability — single terminal choke point: every settle records duration + outcome, plus
|
|
175
|
+
// the 09 named counters (runner timeout / protocol error). errorCode is the bounded ErrorCode enum;
|
|
176
|
+
// never logs raw seed/stdout/stderr/trace (summary is already normalized to summary-only).
|
|
177
|
+
const durationMs = Date.now() - run.startedAt;
|
|
178
|
+
const errorCode = summary.ok ? undefined : summary.error?.code;
|
|
179
|
+
metrics.observe('runner_verify_duration_ms', durationMs);
|
|
180
|
+
metrics.inc('runner_verify_total', { status });
|
|
181
|
+
if (status === 'timeout')
|
|
182
|
+
metrics.inc('runner_timeout_total');
|
|
183
|
+
if (errorCode === 'runner_protocol_error')
|
|
184
|
+
metrics.inc('runner_protocol_error_total');
|
|
185
|
+
logger.info('runner.verify', { requestId: run.requestId, status, errorCode, durationMs, queueDepth: queue.length });
|
|
186
|
+
if (run.killTimer)
|
|
187
|
+
clearTimeout(run.killTimer);
|
|
188
|
+
if (run.resultGrace)
|
|
189
|
+
clearTimeout(run.resultGrace);
|
|
190
|
+
if (run.tempRoot)
|
|
191
|
+
safeCleanup(run.tempRoot);
|
|
192
|
+
run.input = MINIMAL_INPUT; // drop retained StartInput (rawSeedArgs/evidence) once terminal — never needed again
|
|
193
|
+
run.resolveSettled(summary);
|
|
194
|
+
// Retain the terminal run briefly so getRunStatus/whenSettled consumers can read the summary,
|
|
195
|
+
// then GC it — the `runs` map otherwise grows unbounded and retains run metadata forever
|
|
196
|
+
// (Codex M9 review · High). Unref'd so it never keeps the process alive.
|
|
197
|
+
setTimeout(() => { runs.delete(run.requestId); }, terminalRetentionMs).unref();
|
|
198
|
+
scheduleNext();
|
|
199
|
+
}
|
|
200
|
+
/** #1d temp-store capacity gate: is there room for another verify temp dir? Over the high watermark
|
|
201
|
+
* → shed orphan/aged dirs (sweepTemp) and re-measure once; still over → temp_store_full. A
|
|
202
|
+
* measurement throw counts as over-capacity (fail-closed — never fill the disk on a bad read). */
|
|
203
|
+
function checkTempCapacity() {
|
|
204
|
+
if (!tempCapacity)
|
|
205
|
+
return { ok: true };
|
|
206
|
+
const highBytes = tempCapacity.maxBytes * tempCapacity.highWatermarkRatio;
|
|
207
|
+
let used;
|
|
208
|
+
try {
|
|
209
|
+
used = measureTempBytes();
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
metrics.inc('temp_store_full_total');
|
|
213
|
+
return { ok: false, reason: 'verify temp usage unmeasurable (fail-closed)' };
|
|
214
|
+
}
|
|
215
|
+
if (used <= highBytes)
|
|
216
|
+
return { ok: true };
|
|
217
|
+
metrics.inc('temp_store_pressure_total');
|
|
218
|
+
try {
|
|
219
|
+
sweepTemp();
|
|
220
|
+
}
|
|
221
|
+
catch { /* best-effort shed */ }
|
|
222
|
+
let after;
|
|
223
|
+
try {
|
|
224
|
+
after = measureTempBytes();
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
metrics.inc('temp_store_full_total');
|
|
228
|
+
return { ok: false, reason: 'verify temp usage unmeasurable after sweep (fail-closed)' };
|
|
229
|
+
}
|
|
230
|
+
if (after <= highBytes)
|
|
231
|
+
return { ok: true };
|
|
232
|
+
metrics.inc('temp_store_full_total');
|
|
233
|
+
return { ok: false, reason: `verify temp store over capacity (${after} > ${Math.floor(highBytes)} bytes, high watermark)` };
|
|
234
|
+
}
|
|
235
|
+
/** Launch a queued run: write input.json, spawn the child, wire timers + JSONL handling.
|
|
236
|
+
* A synchronous launch failure (temp/spawn) settles the run rather than throwing — the
|
|
237
|
+
* requestId is already minted, so callers always observe a terminal status. */
|
|
238
|
+
function launch(run) {
|
|
239
|
+
// #1d: refuse to write a new temp dir past the high watermark (fail-closed) before doing any work.
|
|
240
|
+
const cap = checkTempCapacity();
|
|
241
|
+
if (!cap.ok) {
|
|
242
|
+
settle(run, 'failed', { ok: false, error: { code: 'temp_store_full', message: cap.reason } });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
run.status = 'running';
|
|
246
|
+
run.startedAt = Date.now();
|
|
247
|
+
const { requestId, input } = run;
|
|
248
|
+
let inputPath;
|
|
249
|
+
let child;
|
|
250
|
+
try {
|
|
251
|
+
const written = writeInputJson(requestId, {
|
|
252
|
+
requestId,
|
|
253
|
+
name: input.name,
|
|
254
|
+
adapterPath: input.adapterPath ?? resolveAdapterPath(input.name),
|
|
255
|
+
executionSeedArgs: input.rawSeedArgs, // raw → input.json only
|
|
256
|
+
fixture: input.fixture,
|
|
257
|
+
trace: input.trace,
|
|
258
|
+
// contextId intentionally NOT written — M6b verifies against the daemon default
|
|
259
|
+
// profile; RunnerInput.contextId is a tested forward seam (see verify-runner-main).
|
|
260
|
+
});
|
|
261
|
+
run.tempRoot = written.tempRoot;
|
|
262
|
+
inputPath = written.inputPath;
|
|
263
|
+
// raw seed args are now persisted to the 0600 input.json; drop them from memory immediately so a
|
|
264
|
+
// long-running verify doesn't hold the raw secret for its whole lifetime (Codex M9 review · High).
|
|
265
|
+
input.rawSeedArgs = {};
|
|
266
|
+
// maxRuntimeMs (#7): the child's orphan self-watchdog deadline — above our own timeout +
|
|
267
|
+
// grace so it only fires if WE die before killing it (e.g. win32 reaper can't kill the child).
|
|
268
|
+
const maxRuntimeMs = config.timeoutMs + config.killGraceMs + 5000;
|
|
269
|
+
const args = [...launcher.prefixArgs, ...buildRunnerArgs({ requestId, name: input.name, inputPath, protocolFd: PROTOCOL_FD, maxRuntimeMs })];
|
|
270
|
+
// Adapter stdin/stdout/stderr → /dev/null; the JSONL protocol gets its own pipe at fd 3, so
|
|
271
|
+
// user console.log can neither break (pollute) nor forge it (Codex #3). Parent reads stdio[3].
|
|
272
|
+
const spawnOpts = { env: buildChildEnv(written.tempRoot, opts.daemonPort), stdio: ['ignore', 'ignore', 'ignore', 'pipe'] };
|
|
273
|
+
child = spawnImpl(launcher.command, args, spawnOpts);
|
|
274
|
+
run.child = child;
|
|
275
|
+
if (typeof child.pid === 'number')
|
|
276
|
+
updateMarkerPid(written.tempRoot, child.pid);
|
|
277
|
+
}
|
|
278
|
+
catch (err) {
|
|
279
|
+
settle(run, 'failed', { ok: false, error: { code: 'runner_protocol_error', message: `launch failed: ${err instanceof Error ? err.message : String(err)}` } });
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
run.killTimer = setTimeout(() => {
|
|
283
|
+
try {
|
|
284
|
+
child.kill('SIGTERM');
|
|
285
|
+
}
|
|
286
|
+
catch { /* already gone */ }
|
|
287
|
+
run.graceTimer = setTimeout(() => { try {
|
|
288
|
+
child.kill('SIGKILL');
|
|
289
|
+
}
|
|
290
|
+
catch { /* already gone */ } }, config.killGraceMs);
|
|
291
|
+
settle(run, 'timeout', { ok: false, error: { code: 'verify_timeout', message: `verify exceeded ${config.timeoutMs}ms` } });
|
|
292
|
+
}, config.timeoutMs);
|
|
293
|
+
// ── protocol JSONL on fd 3 — buffer-until-exit (M6c strict duplicate-result, 08:67) ──
|
|
294
|
+
// The protocol is read ONLY from the dedicated fd (Codex #3); the adapter's stdout/stderr are
|
|
295
|
+
// /dev/null, so user output can neither pollute nor forge it. The first `result` is RECORDED,
|
|
296
|
+
// not settled immediately: we keep reading so a SECOND result (or a post-result protocol
|
|
297
|
+
// violation) is caught as runner_protocol_error. The recorded result settles on child exit; a
|
|
298
|
+
// child that emits a result then never exits is force-killed after a short grace and settled
|
|
299
|
+
// with the captured result (not a timeout). `stdoutLimitBytes` now caps the protocol stream.
|
|
300
|
+
let protocolBytes = 0;
|
|
301
|
+
let buf = '';
|
|
302
|
+
let sawResult = false;
|
|
303
|
+
let pending = null;
|
|
304
|
+
const finishWithPending = () => {
|
|
305
|
+
if (run.status !== 'running')
|
|
306
|
+
return;
|
|
307
|
+
if (sawResult && pending)
|
|
308
|
+
settle(run, pending.ok ? 'succeeded' : 'failed', pending);
|
|
309
|
+
else
|
|
310
|
+
settle(run, 'failed', { ok: false, error: { code: 'runner_protocol_error', message: 'child exited without a result event' } });
|
|
311
|
+
};
|
|
312
|
+
const protocol = child.stdio[PROTOCOL_FD];
|
|
313
|
+
protocol?.on('data', (chunk) => {
|
|
314
|
+
if (run.status !== 'running')
|
|
315
|
+
return;
|
|
316
|
+
protocolBytes += chunk.length;
|
|
317
|
+
if (protocolBytes > config.stdoutLimitBytes) {
|
|
318
|
+
try {
|
|
319
|
+
child.kill('SIGKILL');
|
|
320
|
+
}
|
|
321
|
+
catch { /* already gone */ }
|
|
322
|
+
settle(run, 'failed', { ok: false, error: { code: 'output_truncated', message: `runner protocol exceeded ${config.stdoutLimitBytes} bytes` } });
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
buf += chunk.toString('utf8');
|
|
326
|
+
let nl;
|
|
327
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
328
|
+
const line = buf.slice(0, nl);
|
|
329
|
+
buf = buf.slice(nl + 1);
|
|
330
|
+
if (!line.trim())
|
|
331
|
+
continue;
|
|
332
|
+
const parsed = parseRunnerEvent(line, requestId, config.jsonlLineLimit);
|
|
333
|
+
if (!parsed.ok) {
|
|
334
|
+
try {
|
|
335
|
+
child.kill('SIGKILL');
|
|
336
|
+
}
|
|
337
|
+
catch { /* already gone */ }
|
|
338
|
+
settle(run, 'failed', { ok: false, error: { code: 'runner_protocol_error', message: parsed.reason } });
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
if (parsed.event.type === 'result') {
|
|
342
|
+
if (sawResult) {
|
|
343
|
+
// A second terminal result is a protocol violation (08: one and only one result).
|
|
344
|
+
try {
|
|
345
|
+
child.kill('SIGKILL');
|
|
346
|
+
}
|
|
347
|
+
catch { /* already gone */ }
|
|
348
|
+
settle(run, 'failed', { ok: false, error: { code: 'runner_protocol_error', message: 'duplicate result event' } });
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
sawResult = true;
|
|
352
|
+
pending = normalizeRunnerResult(parsed.event);
|
|
353
|
+
// Settle on exit; force-kill + settle if the child lingers past the grace.
|
|
354
|
+
run.resultGrace = setTimeout(() => { try {
|
|
355
|
+
child.kill('SIGKILL');
|
|
356
|
+
}
|
|
357
|
+
catch { /* gone */ } finishWithPending(); }, config.killGraceMs);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
child.on('error', (err) => {
|
|
362
|
+
settle(run, 'failed', { ok: false, error: { code: 'runner_protocol_error', message: `spawn failed: ${err.message}` } });
|
|
363
|
+
});
|
|
364
|
+
// 'close' (not 'exit'): stdout is only guaranteed fully drained at close, so a buffered
|
|
365
|
+
// terminal result is always seen before we decide "no result". Settle with the captured
|
|
366
|
+
// result if any, else a no-result protocol violation (unless already settled via
|
|
367
|
+
// timeout/cancel/cap/duplicate).
|
|
368
|
+
child.on('close', () => {
|
|
369
|
+
if (run.graceTimer)
|
|
370
|
+
clearTimeout(run.graceTimer);
|
|
371
|
+
finishWithPending();
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
/** Pull queued runs into freed slots, FIFO, up to maxConcurrency. */
|
|
375
|
+
function scheduleNext() {
|
|
376
|
+
while (runningCount() < config.maxConcurrency && queue.length > 0) {
|
|
377
|
+
const id = queue.shift();
|
|
378
|
+
const run = runs.get(id);
|
|
379
|
+
if (run && run.status === 'queued')
|
|
380
|
+
launch(run);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/** A request id not already tracked (canonical from the caller, else generated with collision
|
|
384
|
+
* avoidance — Codex #5: a generated id must never alias a live run). */
|
|
385
|
+
function freshRequestId(provided) {
|
|
386
|
+
if (provided !== undefined)
|
|
387
|
+
return provided;
|
|
388
|
+
let id;
|
|
389
|
+
do {
|
|
390
|
+
id = `req_${randomUUID().replace(/-/g, '').slice(0, 16)}`;
|
|
391
|
+
} while (runs.has(id));
|
|
392
|
+
return id;
|
|
393
|
+
}
|
|
394
|
+
function startVerify(input) {
|
|
395
|
+
// Idempotency / anti-clobber (Codex #5 · P2): a repeat start for an id already tracked must NOT
|
|
396
|
+
// overwrite the existing run — `runs.set` would orphan the live child + its kill/grace timers and
|
|
397
|
+
// leak the temp dir, while status/cancel would silently retarget the new run. Return the existing
|
|
398
|
+
// id instead (a canonical-id retry — e.g. be re-forwarding after a network blip — is idempotent;
|
|
399
|
+
// the caller polls getRunStatus for whatever state that run is in).
|
|
400
|
+
if (input.requestId !== undefined) {
|
|
401
|
+
const existing = runs.get(input.requestId);
|
|
402
|
+
if (existing)
|
|
403
|
+
return Promise.resolve({ requestId: existing.requestId });
|
|
404
|
+
}
|
|
405
|
+
// requestId created BEFORE launch (08). Use the caller's canonical id when provided
|
|
406
|
+
// (be ↔ daemon ↔ runner share one id); else generate (standalone CLI / tests).
|
|
407
|
+
const requestId = freshRequestId(input.requestId);
|
|
408
|
+
// Concurrency gate (synchronous, in-memory): launch if a slot is free, else enqueue, else
|
|
409
|
+
// reject queue_full (08 `queued` status; 09 HIGH_LEVEL_QUEUE_LIMIT; 03 queue_full → 429).
|
|
410
|
+
// Invariant maintained by scheduleNext: runningCount < max ⟹ queue empty, so a free slot
|
|
411
|
+
// never jumps the queue.
|
|
412
|
+
const canLaunch = runningCount() < config.maxConcurrency;
|
|
413
|
+
if (!canLaunch && queue.length >= config.queueLimit) {
|
|
414
|
+
metrics.inc('runner_queue_rejected_total'); // #1c · 09 queue rejected count
|
|
415
|
+
return Promise.reject(Object.assign(new Error(`verify queue is full (limit ${config.queueLimit})`), { code: 'queue_full' }));
|
|
416
|
+
}
|
|
417
|
+
let resolveSettled;
|
|
418
|
+
const settled = new Promise((res) => { resolveSettled = res; });
|
|
419
|
+
const run = { requestId, status: 'queued', startedAt: Date.now(), input, summary: null, settled, resolveSettled };
|
|
420
|
+
runs.set(requestId, run);
|
|
421
|
+
if (canLaunch)
|
|
422
|
+
launch(run);
|
|
423
|
+
else {
|
|
424
|
+
queue.push(requestId);
|
|
425
|
+
metrics.observe('runner_queue_depth', queue.length);
|
|
426
|
+
} // #1c · 09 queue depth
|
|
427
|
+
return Promise.resolve({ requestId });
|
|
428
|
+
}
|
|
429
|
+
async function getVerifyStatus(requestId) {
|
|
430
|
+
return runs.get(requestId)?.summary ?? null; // null while queued/running; VerifySummary once terminal
|
|
431
|
+
}
|
|
432
|
+
async function cancelVerify(requestId) {
|
|
433
|
+
const run = runs.get(requestId);
|
|
434
|
+
if (!run)
|
|
435
|
+
return { cancelled: false };
|
|
436
|
+
if (run.status !== 'running' && run.status !== 'queued') {
|
|
437
|
+
// Idempotent: already terminal. "cancelled" only if it ended via cancellation.
|
|
438
|
+
return { cancelled: run.status === 'cancelled' };
|
|
439
|
+
}
|
|
440
|
+
if (run.status === 'queued') {
|
|
441
|
+
const i = queue.indexOf(requestId);
|
|
442
|
+
if (i >= 0)
|
|
443
|
+
queue.splice(i, 1); // drop from queue; no child/temp to clean
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
// SIGTERM first so the child can release its browser lease (#4), then SIGKILL after the grace
|
|
447
|
+
// (mirrors the timeout path). The grace timer is unref'd — settle() already finalizes the run.
|
|
448
|
+
const child = run.child;
|
|
449
|
+
try {
|
|
450
|
+
child?.kill('SIGTERM');
|
|
451
|
+
}
|
|
452
|
+
catch { /* already gone */ }
|
|
453
|
+
if (child)
|
|
454
|
+
setTimeout(() => { try {
|
|
455
|
+
child.kill('SIGKILL');
|
|
456
|
+
}
|
|
457
|
+
catch { /* gone */ } }, config.killGraceMs).unref?.();
|
|
458
|
+
}
|
|
459
|
+
settle(run, 'cancelled', { ok: false, error: { code: 'runner_protocol_error', message: 'cancelled' } });
|
|
460
|
+
return { cancelled: true };
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
startVerify,
|
|
464
|
+
getVerifyStatus,
|
|
465
|
+
cancelVerify,
|
|
466
|
+
whenSettled: (requestId) => runs.get(requestId)?.settled ?? null,
|
|
467
|
+
activeCount: () => runningCount(),
|
|
468
|
+
queuedCount: () => queue.length,
|
|
469
|
+
getRunStatus: (requestId) => {
|
|
470
|
+
const run = runs.get(requestId);
|
|
471
|
+
return run ? { status: run.status, summary: run.summary } : null;
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
/** Lazily-constructed process-wide default (used by verifyAdapter when no runner is injected). */
|
|
476
|
+
let _default = null;
|
|
477
|
+
let _defaultDaemonPort;
|
|
478
|
+
let _defaultMetrics;
|
|
479
|
+
let _defaultLogger;
|
|
480
|
+
let _defaultTempCapacity;
|
|
481
|
+
let _defaultSweepTemp;
|
|
482
|
+
/**
|
|
483
|
+
* Configure the daemon port the default runner hands to child processes via
|
|
484
|
+
* BYCLI_DAEMON_PORT (M6b: browser adapters connect back to this daemon for a Page).
|
|
485
|
+
* The daemon calls this at startup BEFORE the first defaultRunnerPort() use; calling it
|
|
486
|
+
* after the singleton is built is a no-op (the port is fixed at construction).
|
|
487
|
+
*/
|
|
488
|
+
export function setDefaultRunnerDaemonPort(port) {
|
|
489
|
+
_defaultDaemonPort = port;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Share the daemon's metrics + structured logger with the default runner (#1c), so runner counters
|
|
493
|
+
* (timeout / queue depth / protocol error / verify duration) surface on the daemon's GET /metrics and
|
|
494
|
+
* runner verify logs share the daemon's level. Same injection contract as setDefaultRunnerDaemonPort:
|
|
495
|
+
* the daemon calls this at startup BEFORE the first defaultRunnerPort() use; later calls are no-ops.
|
|
496
|
+
*/
|
|
497
|
+
export function setDefaultRunnerObservability(metrics, logger) {
|
|
498
|
+
_defaultMetrics = metrics;
|
|
499
|
+
_defaultLogger = logger;
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Enable the temp-store capacity guard on the default runner (#1d). Only the daemon (the long-running
|
|
503
|
+
* owner of verify temp dirs) calls this; one-shot CLI verify leaves it off. `sweepTemp` is the daemon's
|
|
504
|
+
* on-pressure shed (its reap). Same injection contract: call BEFORE the first defaultRunnerPort() use.
|
|
505
|
+
*/
|
|
506
|
+
export function setDefaultRunnerTempGuard(capacity, sweepTemp) {
|
|
507
|
+
_defaultTempCapacity = capacity;
|
|
508
|
+
_defaultSweepTemp = sweepTemp;
|
|
509
|
+
}
|
|
510
|
+
export function defaultRunnerPort() {
|
|
511
|
+
return (_default ??= createRunnerPort({
|
|
512
|
+
daemonPort: _defaultDaemonPort,
|
|
513
|
+
metrics: _defaultMetrics,
|
|
514
|
+
logger: _defaultLogger,
|
|
515
|
+
tempCapacity: _defaultTempCapacity,
|
|
516
|
+
sweepTemp: _defaultSweepTemp,
|
|
517
|
+
}));
|
|
518
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** A recording session is short-lived; an hour is a generous ceiling before the salt is dropped. */
|
|
2
|
+
export declare const DEFAULT_SESSION_KEY_TTL_MS = 3600000;
|
|
3
|
+
export declare class SessionKeyRegistry {
|
|
4
|
+
private readonly ttlMs;
|
|
5
|
+
private readonly now;
|
|
6
|
+
/** Injectable secret source (tests); defaults to a 256-bit CSPRNG salt. */
|
|
7
|
+
private readonly mintKey;
|
|
8
|
+
private readonly salts;
|
|
9
|
+
private fallbackKey;
|
|
10
|
+
constructor(ttlMs?: number, now?: () => number,
|
|
11
|
+
/** Injectable secret source (tests); defaults to a 256-bit CSPRNG salt. */
|
|
12
|
+
mintKey?: () => string);
|
|
13
|
+
/**
|
|
14
|
+
* The per-session HMAC key: lazily minted on first use and stable for the session's lifetime
|
|
15
|
+
* (within the TTL). An absent `sessionId` (standalone CLI verify with no recorder session) falls
|
|
16
|
+
* back to a single process-wide random secret — still session-unlinkable across daemon restarts and
|
|
17
|
+
* never derived from anything on the wire.
|
|
18
|
+
*/
|
|
19
|
+
keyFor(sessionId?: string): string;
|
|
20
|
+
/** Explicit eviction when a session ends (best-effort; the TTL sweep is the backstop). */
|
|
21
|
+
evict(sessionId: string): void;
|
|
22
|
+
/** Drop every salt older than the TTL. Called on access and (optionally) on a periodic sweep. */
|
|
23
|
+
sweepExpired(): number;
|
|
24
|
+
/** Live salt count (introspection / tests). */
|
|
25
|
+
size(): number;
|
|
26
|
+
}
|
|
27
|
+
/** Process-wide registry used by the daemon /v1/verify handler. Reset implicitly on daemon restart. */
|
|
28
|
+
export declare function defaultSessionKeyRegistry(): SessionKeyRegistry;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-session HMAC key registry (M7a · 04:95-111).
|
|
3
|
+
*
|
|
4
|
+
* The verify evidence HMAC (`deriveEvidenceSeedArgs`) must be keyed by a per-session secret so that
|
|
5
|
+
* seed-arg evidence is scoped to a recorder session (`hmacScope: 'recorder_session'`), is not
|
|
6
|
+
* reversible, and is not comparable across sessions. The secret is a random salt held ONLY in daemon
|
|
7
|
+
* process memory:
|
|
8
|
+
* - it NEVER crosses the wire (be sends the non-secret `sessionId`; the daemon mints + holds the
|
|
9
|
+
* salt) — 04:111 forbids the salt from entering any request/response;
|
|
10
|
+
* - restarting the daemon drops every salt → automatic rotation (04:95);
|
|
11
|
+
* - a salt also TTL-expires so an abandoned session does not pin its secret forever.
|
|
12
|
+
*
|
|
13
|
+
* This replaces the M5c/M6 placeholder key `daemon-${PORT}`, which was process-wide (every session
|
|
14
|
+
* shared one deterministic key derived from the port — not a per-session secret).
|
|
15
|
+
*/
|
|
16
|
+
import { randomBytes } from 'node:crypto';
|
|
17
|
+
/** A recording session is short-lived; an hour is a generous ceiling before the salt is dropped. */
|
|
18
|
+
export const DEFAULT_SESSION_KEY_TTL_MS = 3_600_000;
|
|
19
|
+
export class SessionKeyRegistry {
|
|
20
|
+
ttlMs;
|
|
21
|
+
now;
|
|
22
|
+
mintKey;
|
|
23
|
+
salts = new Map();
|
|
24
|
+
fallbackKey = null;
|
|
25
|
+
constructor(ttlMs = DEFAULT_SESSION_KEY_TTL_MS, now = Date.now,
|
|
26
|
+
/** Injectable secret source (tests); defaults to a 256-bit CSPRNG salt. */
|
|
27
|
+
mintKey = () => randomBytes(32).toString('hex')) {
|
|
28
|
+
this.ttlMs = ttlMs;
|
|
29
|
+
this.now = now;
|
|
30
|
+
this.mintKey = mintKey;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The per-session HMAC key: lazily minted on first use and stable for the session's lifetime
|
|
34
|
+
* (within the TTL). An absent `sessionId` (standalone CLI verify with no recorder session) falls
|
|
35
|
+
* back to a single process-wide random secret — still session-unlinkable across daemon restarts and
|
|
36
|
+
* never derived from anything on the wire.
|
|
37
|
+
*/
|
|
38
|
+
keyFor(sessionId) {
|
|
39
|
+
if (!sessionId) {
|
|
40
|
+
this.fallbackKey ??= this.mintKey();
|
|
41
|
+
return this.fallbackKey;
|
|
42
|
+
}
|
|
43
|
+
this.sweepExpired();
|
|
44
|
+
const existing = this.salts.get(sessionId);
|
|
45
|
+
if (existing)
|
|
46
|
+
return existing.key;
|
|
47
|
+
const key = this.mintKey();
|
|
48
|
+
this.salts.set(sessionId, { key, createdAt: this.now() });
|
|
49
|
+
return key;
|
|
50
|
+
}
|
|
51
|
+
/** Explicit eviction when a session ends (best-effort; the TTL sweep is the backstop). */
|
|
52
|
+
evict(sessionId) { this.salts.delete(sessionId); }
|
|
53
|
+
/** Drop every salt older than the TTL. Called on access and (optionally) on a periodic sweep. */
|
|
54
|
+
sweepExpired() {
|
|
55
|
+
const cutoff = this.now() - this.ttlMs;
|
|
56
|
+
let dropped = 0;
|
|
57
|
+
for (const [sid, e] of this.salts) {
|
|
58
|
+
if (e.createdAt < cutoff) {
|
|
59
|
+
this.salts.delete(sid);
|
|
60
|
+
dropped++;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return dropped;
|
|
64
|
+
}
|
|
65
|
+
/** Live salt count (introspection / tests). */
|
|
66
|
+
size() { return this.salts.size; }
|
|
67
|
+
}
|
|
68
|
+
let singleton = null;
|
|
69
|
+
/** Process-wide registry used by the daemon /v1/verify handler. Reset implicitly on daemon restart. */
|
|
70
|
+
export function defaultSessionKeyRegistry() {
|
|
71
|
+
return (singleton ??= new SessionKeyRegistry());
|
|
72
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|