@hraness/ghostget 0.17.2
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/CHANGELOG.md +458 -0
- package/LICENSE +21 -0
- package/README.md +1359 -0
- package/bunfig.toml +2 -0
- package/dist/apple-photos-client.js +704 -0
- package/dist/beeper-client.js +4602 -0
- package/dist/client.js +1605 -0
- package/dist/index-26yq8q16.js +31 -0
- package/dist/index-9t13b4k7.js +508 -0
- package/dist/index-axwm9b3v.js +5 -0
- package/dist/index-gwk7rbyj.js +30 -0
- package/dist/index.js +701 -0
- package/dist/messaging.js +1130 -0
- package/dist/omni-client.js +1406 -0
- package/dist/whatsapp-client.js +355 -0
- package/docs/imessage-direct-provider.md +106 -0
- package/docs/rental-listings.md +94 -0
- package/package.json +600 -0
- package/skills/ghostget/SKILL.md +251 -0
- package/skills/ghostget/agents/openai.yaml +4 -0
- package/skills/ghostget/references/adapter-contract.md +181 -0
- package/skills/ghostget/references/apple-photos.md +115 -0
- package/skills/ghostget/references/article-drafts.md +235 -0
- package/skills/ghostget/references/code-owned-provider-scaffold.md +134 -0
- package/skills/ghostget/references/cross-posting-video.md +139 -0
- package/skills/ghostget/references/cross-posting.md +82 -0
- package/skills/ghostget/references/derivation.md +390 -0
- package/skills/ghostget/references/hraness-social-profile-stats.json +269 -0
- package/skills/ghostget/references/install.md +66 -0
- package/skills/ghostget/references/linkedin-adapter.md +442 -0
- package/skills/ghostget/references/messaging.md +168 -0
- package/skills/ghostget/references/meta-comet-contract.md +153 -0
- package/skills/ghostget/references/platform-patterns.md +122 -0
- package/skills/ghostget/references/provider-plugins.md +496 -0
- package/skills/ghostget/references/reddit-flair.md +78 -0
- package/skills/ghostget/references/safety-and-state.md +214 -0
- package/skills/ghostget/references/settlement-and-duplicate-cleanup.md +79 -0
- package/skills/ghostget/references/social-platform-routing.md +66 -0
- package/skills/ghostget/references/social-profile-stats.md +195 -0
- package/skills/ghostget/references/social-video-platform-routing.md +89 -0
- package/skills/ghostget/references/whatsapp-adapter.md +101 -0
- package/skills/ghostget/references/x-adapter.md +332 -0
- package/skills/ghostget/references/x-ai-disclosure.md +71 -0
- package/src/apple-photos-cli.ts +639 -0
- package/src/apple-photos-client-types.ts +168 -0
- package/src/apple-photos-client.ts +201 -0
- package/src/apple-photos-contact-evidence.ts +838 -0
- package/src/apple-photos-local-source.ts +1131 -0
- package/src/args.ts +2053 -0
- package/src/article-draft-document.ts +417 -0
- package/src/article-draft-embeds.ts +123 -0
- package/src/article-draft-images.ts +214 -0
- package/src/assets/adapter-template/wrench-adapter.json +40 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.json +1500 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v1.0.0.json +129 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v1.1.0.json +207 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.0.0.json +1463 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.1.0.json +1463 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.2.0.json +1475 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.3.0.json +1488 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.json +684 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.0.0.json +581 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.1.0.json +581 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.2.0.json +581 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.3.0.json +615 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.4.0.json +684 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.5.0.json +684 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.6.0.json +684 -0
- package/src/assets/adapters/clasificados/wrench-web-adapter.json +54 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.json +750 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.v1.0.0.json +706 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.v1.1.0.json +703 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.v1.2.0.json +750 -0
- package/src/assets/adapters/facebook-group/wrench-web-adapter.json +682 -0
- package/src/assets/adapters/facebook-group/wrench-web-adapter.v1.0.0.json +583 -0
- package/src/assets/adapters/facebook-group/wrench-web-adapter.v1.1.0.json +682 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.json +437 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.0.0.json +383 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.1.0.json +437 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.1.1.json +437 -0
- package/src/assets/adapters/facebook-page/wrench-web-adapter.json +880 -0
- package/src/assets/adapters/facebook-page/wrench-web-adapter.v1.0.0.json +740 -0
- package/src/assets/adapters/facebook-page/wrench-web-adapter.v1.1.0.json +880 -0
- package/src/assets/adapters/github/wrench-web-adapter.json +69 -0
- package/src/assets/adapters/github/wrench-web-adapter.v1.0.0.json +42 -0
- package/src/assets/adapters/github/wrench-web-adapter.v1.1.0.json +69 -0
- package/src/assets/adapters/gmail/wrench-adapter.json +161 -0
- package/src/assets/adapters/gmail/wrench-adapter.v1.2.0.json +157 -0
- package/src/assets/adapters/hacker-news/wrench-web-adapter.json +315 -0
- package/src/assets/adapters/hacker-news/wrench-web-adapter.v1.0.0.json +315 -0
- package/src/assets/adapters/imessage/wrench-web-adapter.json +236 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.json +719 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.0.0.json +627 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.1.0.json +609 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.2.0.json +640 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.3.0.json +671 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.4.0.json +710 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.5.0.json +710 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.6.0.json +719 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.7.0.json +719 -0
- package/src/assets/adapters/linkedin/wrench-adapter.json +442 -0
- package/src/assets/adapters/linkedin/wrench-adapter.v0.4.0.json +124 -0
- package/src/assets/adapters/linkedin/wrench-adapter.v1.0.0.json +411 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.json +914 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.0.0.json +604 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.1.0.json +604 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.10.0.json +809 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.11.0.json +809 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.12.0.json +820 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.13.0.json +819 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.14.0.json +819 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.15.0.json +819 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.16.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.17.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.18.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.19.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.2.0.json +731 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.3.0.json +768 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.4.0.json +808 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.5.0.json +808 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.6.0.json +808 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.7.0.json +825 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.8.0.json +792 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.9.0.json +809 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.json +855 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.0.0.json +577 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.1.0.json +604 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.10.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.11.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.12.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.2.0.json +653 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.3.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.4.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.5.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.6.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.7.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.8.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.9.0.json +713 -0
- package/src/assets/adapters/substack/wrench-web-adapter.json +936 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.0.0.json +817 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.1.0.json +812 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.2.0.json +812 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.3.0.json +902 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.4.0.json +936 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.5.0.json +936 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.6.0.json +936 -0
- package/src/assets/adapters/threads/wrench-web-adapter.json +692 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.0.0.json +629 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.1.0.json +623 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.2.0.json +622 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.3.0.json +622 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.4.0.json +622 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.5.0.json +699 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.6.0.json +693 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.7.0.json +692 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.json +762 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.0.0.json +681 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.1.0.json +708 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.2.0.json +762 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.3.0.json +762 -0
- package/src/assets/adapters/twitch/wrench-web-adapter.json +42 -0
- package/src/assets/adapters/twitch/wrench-web-adapter.v1.0.0.json +42 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.json +400 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.0.0.json +355 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.1.0.json +386 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.2.0.json +386 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.3.0.json +400 -0
- package/src/assets/adapters/x/wrench-adapter.json +749 -0
- package/src/assets/adapters/x/wrench-adapter.v1.0.0.json +700 -0
- package/src/assets/adapters/x/wrench-adapter.v1.1.0.json +707 -0
- package/src/assets/adapters/x/wrench-web-adapter.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.1.0.json +579 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.10.0.json +685 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.11.0.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.12.0.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.13.0.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.2.0.json +578 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.3.0.json +610 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.4.0.json +617 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.5.0.json +634 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.6.0.json +632 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.7.0.json +649 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.8.0.json +649 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.9.0.json +683 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.json +561 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.0.0.json +465 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.1.0.json +519 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.2.0.json +561 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.3.0.json +561 -0
- package/src/assets/code-owned-provider-template/plugin.test.ts.template +54 -0
- package/src/assets/code-owned-provider-template/plugin.ts.template +102 -0
- package/src/assets/code-owned-provider-template/runtime.internal.test.ts.template +44 -0
- package/src/assets/code-owned-provider-template/runtime.ts.template +59 -0
- package/src/assets/code-owned-provider-template/wrench-adapter.json.template +33 -0
- package/src/assets/fonts/nebula-sans/LICENSE.txt +96 -0
- package/src/assets/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
- package/src/assets/fonts/nebula-sans/PROVENANCE.md +14 -0
- package/src/assets.d.ts +4 -0
- package/src/auth.ts +1157 -0
- package/src/beeper-client-types.ts +169 -0
- package/src/beeper-client.ts +250 -0
- package/src/beeper-contact-interactions-cli.ts +180 -0
- package/src/beeper-contact-interactions.ts +1612 -0
- package/src/beeper-message-bundle-v1.ts +257 -0
- package/src/beeper-message-like-me-cli.ts +84 -0
- package/src/beeper-message-like-me-export.ts +2274 -0
- package/src/beeper-message-like-me-recovery.ts +931 -0
- package/src/beeper-message-like-me-source.ts +4034 -0
- package/src/browser-admission.ts +928 -0
- package/src/browser-snapshots.ts +260 -0
- package/src/browser.ts +3873 -0
- package/src/canonical-json.ts +60 -0
- package/src/catalog-cli.ts +638 -0
- package/src/cli.ts +208 -0
- package/src/client-types.ts +301 -0
- package/src/client.ts +2726 -0
- package/src/confirmed-write-failure.ts +44 -0
- package/src/confirmed-write-model.ts +72 -0
- package/src/confirmed-write-platform.ts +1055 -0
- package/src/confirmed-write-program.ts +224 -0
- package/src/confirmed-write-runtime.ts +16 -0
- package/src/cursor-token.ts +228 -0
- package/src/derivation-dnr-readiness.ts +367 -0
- package/src/derivation-file-chooser.ts +384 -0
- package/src/derivation-network-boundary.ts +835 -0
- package/src/derivation-network-guard.ts +782 -0
- package/src/derivation-network-proxy-helper.ts +440 -0
- package/src/derivation-network-proxy.ts +385 -0
- package/src/derive-command-helper.ts +693 -0
- package/src/derive-fixtures.ts +343 -0
- package/src/derive-review.ts +980 -0
- package/src/derive.ts +4754 -0
- package/src/fixtures/beeper-message-like-me-v1/accounts.ndjson +2 -0
- package/src/fixtures/beeper-message-like-me-v1/conversations.ndjson +1 -0
- package/src/fixtures/beeper-message-like-me-v1/manifest.json +1 -0
- package/src/fixtures/beeper-message-like-me-v1/messages.ndjson +2 -0
- package/src/fixtures/beeper-message-like-me-v1/participants.ndjson +3 -0
- package/src/fixtures/beeper-message-like-me-v1/reactions.ndjson +1 -0
- package/src/fixtures/beeper-message-like-me-v1/tombstones.ndjson +1 -0
- package/src/ghostget.ts +3672 -0
- package/src/gmail-capture.ts +1244 -0
- package/src/har-internal.ts +1242 -0
- package/src/har.ts +743 -0
- package/src/index.ts +218 -0
- package/src/invocation-read-platform.ts +55 -0
- package/src/invocation-read-program.ts +93 -0
- package/src/invocation-read-runtime.ts +11 -0
- package/src/linked-device-lifecycle-admission.ts +450 -0
- package/src/linked-device-lifecycle-journal.ts +1560 -0
- package/src/linked-device-lifecycle-runtime.ts +1602 -0
- package/src/local-cli-admission.ts +135 -0
- package/src/local-cli-contracts.ts +271 -0
- package/src/local-cli-execution.ts +98 -0
- package/src/local-cli-surface-contract.ts +1510 -0
- package/src/local-cli-tool-identity.ts +336 -0
- package/src/media/archive.ts +2325 -0
- package/src/media/args.ts +303 -0
- package/src/media/cli.ts +287 -0
- package/src/media/doctor.ts +475 -0
- package/src/media/ffmpeg.ts +1330 -0
- package/src/media/http-capture.ts +711 -0
- package/src/media/http-probe.ts +593 -0
- package/src/media/http.ts +387 -0
- package/src/media/index.ts +10 -0
- package/src/media/local-transcription.ts +613 -0
- package/src/media/lock.ts +321 -0
- package/src/media/manifest.ts +1712 -0
- package/src/media/metadata.ts +906 -0
- package/src/media/process.ts +723 -0
- package/src/media/revision.ts +166 -0
- package/src/media/runtime-closure.ts +1063 -0
- package/src/media/source-router.ts +187 -0
- package/src/media/transcriber-config.ts +1122 -0
- package/src/media/transcript-persistence-model.ts +15 -0
- package/src/media/transcript-persistence-platform.ts +89 -0
- package/src/media/transcript-persistence-program.ts +39 -0
- package/src/media/transcript-persistence-runtime.ts +24 -0
- package/src/media/transcript.ts +616 -0
- package/src/media/utf8-order.ts +30 -0
- package/src/media/whisper-cpp.ts +464 -0
- package/src/media/whisper-language.ts +58 -0
- package/src/media/yt-dlp.ts +439 -0
- package/src/message-like-me-agentic-messaging.ts +882 -0
- package/src/messaging-action-store.ts +793 -0
- package/src/messaging-runtime.ts +2136 -0
- package/src/messaging-store.ts +539 -0
- package/src/messaging-types.ts +1663 -0
- package/src/messaging.ts +334 -0
- package/src/model.ts +2671 -0
- package/src/oauth-google.ts +888 -0
- package/src/omni-client-types.ts +306 -0
- package/src/omni-client.ts +2225 -0
- package/src/omni-limits.ts +34 -0
- package/src/omni-model.ts +1614 -0
- package/src/omni-request.ts +379 -0
- package/src/omni-runtime.ts +1632 -0
- package/src/operation-deadline.ts +189 -0
- package/src/path-helper.ts +1656 -0
- package/src/pinned-https.ts +194 -0
- package/src/plan-assets.ts +832 -0
- package/src/platform-catalog.ts +1832 -0
- package/src/plugins/beeper-linked-device/plugin.ts +310 -0
- package/src/plugins/bluesky-web/plugin.ts +178 -0
- package/src/plugins/clasificados-web/plugin.ts +66 -0
- package/src/plugins/github-web/plugin.ts +80 -0
- package/src/plugins/gmail-official/plugin.ts +110 -0
- package/src/plugins/hacker-news-web/plugin.ts +52 -0
- package/src/plugins/imessage-direct/plugin.ts +195 -0
- package/src/plugins/imessage-direct/vendor/0001-fix-keep-AppleScript-send-payloads-out-of-child-argv.patch +1430 -0
- package/src/plugins/imessage-direct/vendor/0002-feat-rpc-add-exact-chat-lookup.patch +225 -0
- package/src/plugins/imessage-direct/vendor/provenance.json +80 -0
- package/src/plugins/linkedin-official/plugin.ts +84 -0
- package/src/plugins/linkedin-web/plugin.ts +412 -0
- package/src/plugins/meta-web/plugin.ts +317 -0
- package/src/plugins/reddit-web/flair.ts +492 -0
- package/src/plugins/reddit-web/plugin.ts +477 -0
- package/src/plugins/substack-web/plugin.ts +124 -0
- package/src/plugins/tiktok-web/plugin.ts +67 -0
- package/src/plugins/twitch-web/plugin.ts +54 -0
- package/src/plugins/whatsapp-linked-device/plugin.ts +105 -0
- package/src/plugins/x-official/plugin.ts +64 -0
- package/src/plugins/x-web/plugin.ts +488 -0
- package/src/plugins/youtube-web/plugin.ts +101 -0
- package/src/portable-run-recovery.ts +466 -0
- package/src/process-identity.ts +501 -0
- package/src/profile-clone-helper.ts +104 -0
- package/src/provider-catalog-views.ts +51 -0
- package/src/provider-context.ts +69 -0
- package/src/provider-contract-definitions-x.ts +45 -0
- package/src/provider-contract-definitions.ts +948 -0
- package/src/provider-contract-input-gmail.ts +176 -0
- package/src/provider-contract-input-linkedin.ts +77 -0
- package/src/provider-contract-input-x.ts +304 -0
- package/src/provider-contract-planning.ts +23 -0
- package/src/provider-contract-semantic-identity.ts +54 -0
- package/src/provider-contracts.ts +251 -0
- package/src/provider-http.ts +723 -0
- package/src/provider-plugin-auth.ts +36 -0
- package/src/provider-plugin-builtins.ts +229 -0
- package/src/provider-plugin-cleanup-barrier.ts +229 -0
- package/src/provider-plugin-cleanup-execution.ts +79 -0
- package/src/provider-plugin-cleanup-resource.ts +470 -0
- package/src/provider-plugin-contract-identity.ts +707 -0
- package/src/provider-plugin-host.ts +1972 -0
- package/src/provider-plugin-identifiers.ts +60 -0
- package/src/provider-plugin-invocation-lease.ts +889 -0
- package/src/provider-plugin-lifecycle-kernel.ts +566 -0
- package/src/provider-plugin-lifecycle.ts +1455 -0
- package/src/provider-plugin-module-analysis.ts +336 -0
- package/src/provider-plugin-package.ts +2530 -0
- package/src/provider-plugin-portable-authority.ts +106 -0
- package/src/provider-plugin-portable-catalog.ts +94 -0
- package/src/provider-plugin-portable-identity.ts +315 -0
- package/src/provider-plugin-portable-runtime.ts +2057 -0
- package/src/provider-plugin-protocol.ts +1625 -0
- package/src/provider-plugin-registry.ts +3342 -0
- package/src/provider-plugin-store.ts +2155 -0
- package/src/provider-plugin-test-fixture.ts +5 -0
- package/src/provider-plugin.ts +4907 -0
- package/src/provider-plugins.generated.ts +43 -0
- package/src/provider-plugins.ts +8 -0
- package/src/provider-subject.ts +41 -0
- package/src/provider.ts +315 -0
- package/src/providers/beeper-local-runtime.ts +5426 -0
- package/src/providers/beeper-local.ts +2739 -0
- package/src/providers/beeper-messaging.ts +462 -0
- package/src/providers/beeper-omni.ts +1159 -0
- package/src/providers/bluesky-web-runtime.ts +2895 -0
- package/src/providers/bluesky-web.ts +1614 -0
- package/src/providers/clasificados-web-runtime.ts +255 -0
- package/src/providers/clasificados-web.ts +503 -0
- package/src/providers/contact-projection.ts +301 -0
- package/src/providers/github-read-model.ts +94 -0
- package/src/providers/github-read-platform.ts +165 -0
- package/src/providers/github-read-program.ts +97 -0
- package/src/providers/github-web-runtime.ts +320 -0
- package/src/providers/github-web.ts +403 -0
- package/src/providers/gmail-api.ts +2834 -0
- package/src/providers/gmail-omni.ts +830 -0
- package/src/providers/gmail.ts +1130 -0
- package/src/providers/hacker-news-web-runtime.ts +220 -0
- package/src/providers/hacker-news-web.ts +809 -0
- package/src/providers/imessage-direct-install.ts +354 -0
- package/src/providers/imessage-direct-messaging.ts +532 -0
- package/src/providers/imessage-direct-omni.ts +400 -0
- package/src/providers/imessage-direct-runtime.ts +1498 -0
- package/src/providers/imessage-direct.ts +351 -0
- package/src/providers/instagram-video-foundations.ts +913 -0
- package/src/providers/instagram-web-profile-browser.ts +600 -0
- package/src/providers/iso-bmff.ts +338 -0
- package/src/providers/linkedin-company-platform.ts +42 -0
- package/src/providers/linkedin-company-program.ts +88 -0
- package/src/providers/linkedin-contact-failure.ts +84 -0
- package/src/providers/linkedin-contact-platform.ts +45 -0
- package/src/providers/linkedin-contact-program.ts +91 -0
- package/src/providers/linkedin-profile-activity-failure.ts +76 -0
- package/src/providers/linkedin-profile-activity-platform.ts +41 -0
- package/src/providers/linkedin-profile-activity-program.ts +67 -0
- package/src/providers/linkedin-read-failure.ts +52 -0
- package/src/providers/linkedin-self-platform.ts +53 -0
- package/src/providers/linkedin-self-program.ts +122 -0
- package/src/providers/linkedin-web-article-browser.ts +794 -0
- package/src/providers/linkedin-web-bootstrap.ts +146 -0
- package/src/providers/linkedin-web-contact.ts +830 -0
- package/src/providers/linkedin-web-feed-browser.ts +832 -0
- package/src/providers/linkedin-web-feed.ts +864 -0
- package/src/providers/linkedin-web-post-browser.ts +653 -0
- package/src/providers/linkedin-web-profile-browser.ts +683 -0
- package/src/providers/linkedin-web-runtime.ts +2388 -0
- package/src/providers/linkedin-web.ts +3305 -0
- package/src/providers/linkedin.ts +1885 -0
- package/src/providers/meta-bootstrap.ts +1043 -0
- package/src/providers/meta-facebook-group.ts +690 -0
- package/src/providers/meta-marketplace-relay.ts +747 -0
- package/src/providers/meta-omni.ts +373 -0
- package/src/providers/meta-relay-bundle.ts +1049 -0
- package/src/providers/meta-web-descriptors.ts +1740 -0
- package/src/providers/meta-web-runtime.ts +3388 -0
- package/src/providers/meta-web.ts +2931 -0
- package/src/providers/read-failure.ts +139 -0
- package/src/providers/reddit-omni.ts +462 -0
- package/src/providers/reddit-web-runtime.ts +1929 -0
- package/src/providers/reddit-web.ts +1611 -0
- package/src/providers/rental-listings.ts +460 -0
- package/src/providers/substack-omni.ts +406 -0
- package/src/providers/substack-video-mp4.ts +33 -0
- package/src/providers/substack-web-runtime.ts +2812 -0
- package/src/providers/substack-web.ts +1502 -0
- package/src/providers/tiktok-video-mp4.ts +33 -0
- package/src/providers/tiktok-web-runtime.ts +770 -0
- package/src/providers/tiktok-web.ts +2786 -0
- package/src/providers/twitch-web-runtime.ts +226 -0
- package/src/providers/twitch-web.ts +274 -0
- package/src/providers/whatsapp-account-identity.ts +55 -0
- package/src/providers/whatsapp-contact-projection-helper.ts +874 -0
- package/src/providers/whatsapp-contact-projection-protocol.ts +403 -0
- package/src/providers/whatsapp-interaction-projection-helper.ts +1452 -0
- package/src/providers/whatsapp-interaction-projection-protocol.ts +462 -0
- package/src/providers/whatsapp-message-export-projection-protocol.ts +532 -0
- package/src/providers/whatsapp-messaging.ts +132 -0
- package/src/providers/whatsapp-omni.ts +586 -0
- package/src/providers/whatsapp-web-runtime.ts +3108 -0
- package/src/providers/whatsapp-web.ts +1037 -0
- package/src/providers/x-image-provenance.ts +523 -0
- package/src/providers/x-made-with-ai.ts +106 -0
- package/src/providers/x-transaction-id.ts +413 -0
- package/src/providers/x-web-runtime.ts +3485 -0
- package/src/providers/x-web.ts +2332 -0
- package/src/providers/x.ts +1889 -0
- package/src/providers/youtube-web-runtime.ts +1400 -0
- package/src/providers/youtube-web.ts +1221 -0
- package/src/read-admission-runtime.ts +91 -0
- package/src/read-client.ts +521 -0
- package/src/read-effect-platform.ts +7 -0
- package/src/read-effect-runtime.ts +14 -0
- package/src/read-effect.ts +52 -0
- package/src/read-projection-admission.ts +866 -0
- package/src/read-projections.ts +3151 -0
- package/src/recovery.ts +1492 -0
- package/src/reviewed-template.ts +146 -0
- package/src/run-journal.ts +1272 -0
- package/src/runtime.ts +5116 -0
- package/src/scripts/generate-provider-plugin-catalog.ts +121 -0
- package/src/scripts/install-whatsapp-protocol.sh +232 -0
- package/src/scripts/recover-install-stages.ts +1491 -0
- package/src/scripts/resolve-state-home.ts +30 -0
- package/src/scripts/scaffold-web-provider.ts +973 -0
- package/src/scripts/sync-bundled-adapters.ts +695 -0
- package/src/session-secrets.ts +1183 -0
- package/src/state-helper.bunfig.toml +2 -0
- package/src/state-helper.ts +2914 -0
- package/src/storage.ts +3842 -0
- package/src/transport-policy.ts +2 -0
- package/src/usage.ts +182 -0
- package/src/version.ts +2 -0
- package/src/web-session-authentication-policy.ts +136 -0
- package/src/web-session-cleanup-admission.ts +2344 -0
- package/src/web-session-cleanup-join.ts +99 -0
- package/src/web-session-client.ts +996 -0
- package/src/web-session-contract-definitions.ts +820 -0
- package/src/web-session-contract-planning.ts +42 -0
- package/src/web-session-contracts.ts +228 -0
- package/src/web-session-cookies.ts +500 -0
- package/src/web-session-execution.ts +385 -0
- package/src/web-session-http.ts +373 -0
- package/src/web-session-read-errors.ts +19 -0
- package/src/web-session-read-runtime.ts +124 -0
- package/src/web-session-recovery.ts +818 -0
- package/src/web-session-sites.ts +58 -0
- package/src/web-session-template.ts +994 -0
- package/src/web-session.ts +93 -0
- package/src/whatsapp-client-binding.ts +21 -0
- package/src/whatsapp-client-types.ts +75 -0
- package/src/whatsapp-client.ts +395 -0
- package/src/whatsapp-export-coordinate.ts +17 -0
- package/src/whatsapp-message-bundle-v2.ts +195 -0
- package/src/whatsapp-message-like-me-cli.ts +195 -0
- package/src/whatsapp-message-like-me-export.ts +1072 -0
- package/src/whatsapp-message-like-me-source.ts +1012 -0
- package/tsconfig.json +67 -0
|
@@ -0,0 +1,1242 @@
|
|
|
1
|
+
import { MAX_HAR_BYTES, MAX_HAR_ENTRIES } from "./har";
|
|
2
|
+
import { xWebQueryDescriptorEvidenceSnapshot } from "./providers/x-web";
|
|
3
|
+
import { readRegularFile, type PrivateDirectoryIdentity } from "./storage";
|
|
4
|
+
|
|
5
|
+
type JsonRecord = Record<string, unknown>;
|
|
6
|
+
|
|
7
|
+
export type InternalHarCandidate = {
|
|
8
|
+
readonly method: string;
|
|
9
|
+
readonly origin: string;
|
|
10
|
+
readonly path: string;
|
|
11
|
+
readonly operationType: "query" | "mutation" | "unknown";
|
|
12
|
+
readonly sampleCount: number;
|
|
13
|
+
readonly statuses: readonly number[];
|
|
14
|
+
readonly queryNames: readonly string[];
|
|
15
|
+
readonly headerNames: readonly string[];
|
|
16
|
+
readonly requestFieldPaths: readonly string[];
|
|
17
|
+
readonly responseFieldPaths: readonly string[];
|
|
18
|
+
/** Non-secret registered-operation revisions safe to bind into a contract. */
|
|
19
|
+
readonly revisions: readonly string[];
|
|
20
|
+
readonly reviewRequired: true;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type InternalHarEvidence = {
|
|
24
|
+
readonly schemaVersion: 1;
|
|
25
|
+
readonly adapterId: string;
|
|
26
|
+
readonly targetOrigin: string;
|
|
27
|
+
readonly analyzedAt: string;
|
|
28
|
+
readonly observedEntries: number;
|
|
29
|
+
readonly candidates: readonly InternalHarCandidate[];
|
|
30
|
+
readonly warnings: readonly string[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const isRecord = (value: unknown): value is JsonRecord =>
|
|
34
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
35
|
+
|
|
36
|
+
export const INTERNAL_HAR_REVIEW_BOUNDS = Object.freeze({
|
|
37
|
+
maxArrayItems: 100,
|
|
38
|
+
maxFieldNameCharacters: 4_096,
|
|
39
|
+
maxFieldPaths: 1_000,
|
|
40
|
+
maxFormParameters: 1_000,
|
|
41
|
+
maxHeaderItems: 1_000,
|
|
42
|
+
maxHeaderNames: 200,
|
|
43
|
+
maxCandidates: 2_000,
|
|
44
|
+
maxJsonTextBytes: 2 * 1024 * 1024,
|
|
45
|
+
maxObjectEntries: 300,
|
|
46
|
+
maxPathCharacters: 4_096,
|
|
47
|
+
maxPathSegments: 128,
|
|
48
|
+
maxQueryCharacters: 2 * 1024 * 1024,
|
|
49
|
+
maxQueryIdCharacters: 256,
|
|
50
|
+
maxQueryParameters: 1_000,
|
|
51
|
+
maxRawStringCharacters: 2 * 1024 * 1024,
|
|
52
|
+
maxRevisions: 1_000,
|
|
53
|
+
maxTotalFieldPaths: 20_000,
|
|
54
|
+
maxTotalRevisions: 20_000,
|
|
55
|
+
maxTraversalDepth: 8,
|
|
56
|
+
maxLinkedInArticleTraversalDepth: 12,
|
|
57
|
+
maxUrlCharacters: 4_096 + (2 * 1024 * 1024) + 8_192,
|
|
58
|
+
} as const);
|
|
59
|
+
|
|
60
|
+
const sensitiveName = /(?:authorization|cookie|token|secret|password|passwd|session|credential|private|signature)/iu;
|
|
61
|
+
const safeFieldName = /^[A-Za-z_$][A-Za-z0-9_$.-]{0,127}$/u;
|
|
62
|
+
const safeGraphQlVariableFieldName = /^[_A-Za-z][_0-9A-Za-z]{0,127}$/u;
|
|
63
|
+
const safeHeaderName = /^[a-z0-9!#$%&'*+.^_`|~-]{1,128}$/u;
|
|
64
|
+
|
|
65
|
+
const reviewedHeaderNames: ReadonlySet<string> = new Set([
|
|
66
|
+
"accept",
|
|
67
|
+
"accept-language",
|
|
68
|
+
"authorization",
|
|
69
|
+
"content-type",
|
|
70
|
+
"cookie",
|
|
71
|
+
"csrf-token",
|
|
72
|
+
"origin",
|
|
73
|
+
"referer",
|
|
74
|
+
"user-agent",
|
|
75
|
+
"x-client-transaction-id",
|
|
76
|
+
"x-csrf-token",
|
|
77
|
+
"x-li-lang",
|
|
78
|
+
"x-restli-id",
|
|
79
|
+
"x-restli-method",
|
|
80
|
+
"x-restli-protocol-version",
|
|
81
|
+
"x-twitter-active-user",
|
|
82
|
+
"x-twitter-auth-type",
|
|
83
|
+
"x-twitter-client-language",
|
|
84
|
+
] as const);
|
|
85
|
+
|
|
86
|
+
const reviewedTwitchGraphQlHeaderNames: ReadonlySet<string> = new Set([
|
|
87
|
+
"client-id",
|
|
88
|
+
"client-integrity",
|
|
89
|
+
] as const);
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* HAR object keys are untrusted content too. In particular, normalized API
|
|
93
|
+
* responses commonly use account IDs, URNs, names, and slugs as map keys. A
|
|
94
|
+
* lexical identifier check cannot distinguish those values from schema field
|
|
95
|
+
* names, so only this small reviewed vocabulary may survive as a literal.
|
|
96
|
+
* Unknown keys retain their structural position as `:dynamic`.
|
|
97
|
+
*/
|
|
98
|
+
const reviewedStructuralFieldNames: ReadonlySet<string> = new Set([
|
|
99
|
+
"body",
|
|
100
|
+
"byId",
|
|
101
|
+
"category",
|
|
102
|
+
"conversation_id",
|
|
103
|
+
"conversationUrn",
|
|
104
|
+
"conversations",
|
|
105
|
+
"count",
|
|
106
|
+
"cursor",
|
|
107
|
+
"data",
|
|
108
|
+
"edges",
|
|
109
|
+
"elements",
|
|
110
|
+
"encoded_event",
|
|
111
|
+
"entityUrn",
|
|
112
|
+
"entries",
|
|
113
|
+
"errors",
|
|
114
|
+
"extensions",
|
|
115
|
+
"features",
|
|
116
|
+
"fieldToggles",
|
|
117
|
+
"hasNextPage",
|
|
118
|
+
"hasPreviousPage",
|
|
119
|
+
"id",
|
|
120
|
+
"inbox_initial_state",
|
|
121
|
+
"items",
|
|
122
|
+
"limit",
|
|
123
|
+
"media",
|
|
124
|
+
"message",
|
|
125
|
+
"messages",
|
|
126
|
+
"messengerConversations",
|
|
127
|
+
"messengerMessages",
|
|
128
|
+
"metadata",
|
|
129
|
+
"name",
|
|
130
|
+
"nextCursor",
|
|
131
|
+
"nodes",
|
|
132
|
+
"operationName",
|
|
133
|
+
"pageInfo",
|
|
134
|
+
"paging",
|
|
135
|
+
"payload",
|
|
136
|
+
"previousCursor",
|
|
137
|
+
"public_key",
|
|
138
|
+
"queryId",
|
|
139
|
+
"recipient_ids",
|
|
140
|
+
"response",
|
|
141
|
+
"responsive_web_graphql_enabled",
|
|
142
|
+
"responsive_web_graphql_exclude_directive_enabled",
|
|
143
|
+
"result",
|
|
144
|
+
"results",
|
|
145
|
+
"screen_name",
|
|
146
|
+
"start",
|
|
147
|
+
"subject",
|
|
148
|
+
"text",
|
|
149
|
+
"total",
|
|
150
|
+
"variables",
|
|
151
|
+
"users",
|
|
152
|
+
"withAuxiliaryUserLabels",
|
|
153
|
+
] as const);
|
|
154
|
+
|
|
155
|
+
/** Exact schema vocabulary reviewed only for Twitch's first-party GraphQL route. */
|
|
156
|
+
const reviewedTwitchGraphQlFieldNames: ReadonlySet<string> = new Set([
|
|
157
|
+
"channelLogin",
|
|
158
|
+
"currentUser",
|
|
159
|
+
"displayName",
|
|
160
|
+
"extensions",
|
|
161
|
+
"followers",
|
|
162
|
+
"id",
|
|
163
|
+
"login",
|
|
164
|
+
"operationName",
|
|
165
|
+
"persistedQuery",
|
|
166
|
+
"query",
|
|
167
|
+
"requestInfo",
|
|
168
|
+
"sha256Hash",
|
|
169
|
+
"skipSchedule",
|
|
170
|
+
"totalCount",
|
|
171
|
+
"user",
|
|
172
|
+
"variables",
|
|
173
|
+
"version",
|
|
174
|
+
] as const);
|
|
175
|
+
|
|
176
|
+
const reviewedTwitchGraphQlOperationNames: ReadonlySet<string> = new Set([
|
|
177
|
+
"ChannelRoot_AboutPanel",
|
|
178
|
+
"TopNav_CurrentUser",
|
|
179
|
+
] as const);
|
|
180
|
+
|
|
181
|
+
/** Exact schema vocabulary reviewed only for LinkedIn's native Article route. */
|
|
182
|
+
const reviewedLinkedInArticleFieldNames: ReadonlySet<string> = new Set([
|
|
183
|
+
"$set",
|
|
184
|
+
"$type",
|
|
185
|
+
"activityUrn",
|
|
186
|
+
"article",
|
|
187
|
+
"articleType",
|
|
188
|
+
"articleUrn",
|
|
189
|
+
"attributesV2",
|
|
190
|
+
"author",
|
|
191
|
+
"authors",
|
|
192
|
+
"content",
|
|
193
|
+
"contentHtml",
|
|
194
|
+
"caption",
|
|
195
|
+
"coverImage",
|
|
196
|
+
"coverMedia",
|
|
197
|
+
"coverMediaV2Union",
|
|
198
|
+
"createdAt",
|
|
199
|
+
"detailDataUnion",
|
|
200
|
+
"entity",
|
|
201
|
+
"fileSize",
|
|
202
|
+
"filename",
|
|
203
|
+
"firstPartyArticle",
|
|
204
|
+
"firstPartyArticleUrn",
|
|
205
|
+
"hyperlink",
|
|
206
|
+
"included",
|
|
207
|
+
"length",
|
|
208
|
+
"linkedInArticleUrn",
|
|
209
|
+
"mediaUploadType",
|
|
210
|
+
"originalImage",
|
|
211
|
+
"originalImageUrn",
|
|
212
|
+
"patch",
|
|
213
|
+
"permalink",
|
|
214
|
+
"profileUrn",
|
|
215
|
+
"publishedAt",
|
|
216
|
+
"q",
|
|
217
|
+
"state",
|
|
218
|
+
"textBlock",
|
|
219
|
+
"title",
|
|
220
|
+
"type",
|
|
221
|
+
"ugcPostUrn",
|
|
222
|
+
"updatedAt",
|
|
223
|
+
"version",
|
|
224
|
+
] as const);
|
|
225
|
+
|
|
226
|
+
const reviewedPathSegments: ReadonlySet<string> = new Set([
|
|
227
|
+
"2",
|
|
228
|
+
"api",
|
|
229
|
+
"append",
|
|
230
|
+
"articles",
|
|
231
|
+
"chat",
|
|
232
|
+
"comments",
|
|
233
|
+
"conversation",
|
|
234
|
+
"conversations",
|
|
235
|
+
"events",
|
|
236
|
+
"feed",
|
|
237
|
+
"feeds",
|
|
238
|
+
"finalize",
|
|
239
|
+
"graphql",
|
|
240
|
+
"i",
|
|
241
|
+
"inbox",
|
|
242
|
+
"initialize",
|
|
243
|
+
"low_quality",
|
|
244
|
+
"media",
|
|
245
|
+
"message",
|
|
246
|
+
"messages",
|
|
247
|
+
"messaging",
|
|
248
|
+
"notifications",
|
|
249
|
+
"public_keys",
|
|
250
|
+
"publishing",
|
|
251
|
+
"reactions",
|
|
252
|
+
"replies",
|
|
253
|
+
"rest",
|
|
254
|
+
"timeline",
|
|
255
|
+
"trusted",
|
|
256
|
+
"untrusted",
|
|
257
|
+
"upload",
|
|
258
|
+
"users",
|
|
259
|
+
"voyager",
|
|
260
|
+
"voyagerPublishingDashFirstPartyArticles",
|
|
261
|
+
] as const);
|
|
262
|
+
|
|
263
|
+
/** Fields whose object value is a provider-normalized map keyed by IDs/URNs. */
|
|
264
|
+
const reviewedDynamicMapFieldNames: ReadonlySet<string> = new Set([
|
|
265
|
+
"byId",
|
|
266
|
+
"conversations",
|
|
267
|
+
"entries",
|
|
268
|
+
"messages",
|
|
269
|
+
"objects",
|
|
270
|
+
"tweets",
|
|
271
|
+
"users",
|
|
272
|
+
] as const);
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Structural capture hints only. They let the sanitizer retain known
|
|
276
|
+
* registered-operation names and revisions from a private HAR; they are not
|
|
277
|
+
* request rules and cannot authorize dispatch. Observed LinkedIn web reads
|
|
278
|
+
* still bind their executable request rules in the operation registry.
|
|
279
|
+
*/
|
|
280
|
+
const reviewedLinkedInRegisteredQueries: ReadonlyMap<string, ReadonlySet<string>> = new Map([
|
|
281
|
+
["/voyager/api/graphql", new Set([
|
|
282
|
+
"voyagerFeedDashMainFeed",
|
|
283
|
+
"voyagerFeedDashProfileUpdates",
|
|
284
|
+
])],
|
|
285
|
+
["/voyager/api/voyagerMessagingGraphQL/graphql", new Set([
|
|
286
|
+
"messengerConversations",
|
|
287
|
+
"messengerMailboxCounts",
|
|
288
|
+
"messengerMessages",
|
|
289
|
+
])],
|
|
290
|
+
]);
|
|
291
|
+
|
|
292
|
+
const reviewedXGraphQlDescriptors: ReadonlyMap<
|
|
293
|
+
string,
|
|
294
|
+
{ readonly operationType: "query" | "mutation"; readonly revision: string }
|
|
295
|
+
> = new Map(
|
|
296
|
+
xWebQueryDescriptorEvidenceSnapshot.descriptors.map((descriptor) => [
|
|
297
|
+
descriptor.operationName,
|
|
298
|
+
{ operationType: descriptor.operationType, revision: descriptor.queryId },
|
|
299
|
+
]),
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
const reviewedMetaRequestFieldNames: ReadonlySet<string> = new Set([
|
|
303
|
+
"variables",
|
|
304
|
+
"doc_id",
|
|
305
|
+
"fb_api_req_friendly_name",
|
|
306
|
+
"fb_api_caller_class",
|
|
307
|
+
"av",
|
|
308
|
+
"__user",
|
|
309
|
+
"__a",
|
|
310
|
+
"__req",
|
|
311
|
+
"__hs",
|
|
312
|
+
"dpr",
|
|
313
|
+
"__ccg",
|
|
314
|
+
"__rev",
|
|
315
|
+
"__s",
|
|
316
|
+
"__hsi",
|
|
317
|
+
"__dyn",
|
|
318
|
+
"__csr",
|
|
319
|
+
"__comet_req",
|
|
320
|
+
"fb_dtsg",
|
|
321
|
+
"jazoest",
|
|
322
|
+
"lsd",
|
|
323
|
+
"server_timestamps",
|
|
324
|
+
] as const);
|
|
325
|
+
|
|
326
|
+
function isExactFacebookOrigin(url: URL): boolean {
|
|
327
|
+
return url.origin === "https://www.facebook.com";
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function isReviewedTwitchGraphQlRoute(url: URL): boolean {
|
|
331
|
+
return url.origin === "https://gql.twitch.tv" && url.pathname === "/gql";
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function isTwitchPublicAccountFieldName(value: string): boolean {
|
|
335
|
+
return value === "channelLogin" || value === "login";
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function isTwitchCredentialLikeFieldName(value: string): boolean {
|
|
339
|
+
if (isTwitchPublicAccountFieldName(value)) return false;
|
|
340
|
+
const compact = value.replaceAll(/[^A-Za-z0-9]/gu, "").toLowerCase();
|
|
341
|
+
return /(?:auth|authorization|cookie|credential|password|passwd|private|secret|session|signature|token)/u.test(compact);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function isReviewedTwitchStructuralContainer(value: string): boolean {
|
|
345
|
+
return !isTwitchCredentialLikeFieldName(value);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function isReviewedLinkedInArticleRoute(url: URL): boolean {
|
|
349
|
+
if (url.origin !== "https://www.linkedin.com") return false;
|
|
350
|
+
const segments = url.pathname.split("/").filter(Boolean);
|
|
351
|
+
if (
|
|
352
|
+
segments[0] !== "voyager"
|
|
353
|
+
|| segments[1] !== "api"
|
|
354
|
+
|| segments[2] !== "voyagerPublishingDashFirstPartyArticles"
|
|
355
|
+
|| (segments.length !== 3 && segments.length !== 4)
|
|
356
|
+
) return false;
|
|
357
|
+
if (segments.length === 3) return true;
|
|
358
|
+
try {
|
|
359
|
+
return /^urn:li:fsd_firstPartyArticle:[0-9]{1,32}$/u.test(decodeURIComponent(segments[3] ?? ""));
|
|
360
|
+
} catch {
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function reviewedInternalFieldNameForUrl(url: URL, value: string): string {
|
|
366
|
+
if (
|
|
367
|
+
isReviewedTwitchGraphQlRoute(url)
|
|
368
|
+
&& value.length <= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldNameCharacters
|
|
369
|
+
&& safeFieldName.test(value)
|
|
370
|
+
&& !isTwitchCredentialLikeFieldName(value)
|
|
371
|
+
&& reviewedTwitchGraphQlFieldNames.has(value)
|
|
372
|
+
) return value;
|
|
373
|
+
if (
|
|
374
|
+
isReviewedLinkedInArticleRoute(url)
|
|
375
|
+
&& value.length <= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldNameCharacters
|
|
376
|
+
&& safeFieldName.test(value)
|
|
377
|
+
&& !sensitiveName.test(value)
|
|
378
|
+
&& reviewedLinkedInArticleFieldNames.has(value)
|
|
379
|
+
) return value;
|
|
380
|
+
return reviewedInternalFieldName(value);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function reviewedMetaRequestFieldName(url: URL, value: string): string {
|
|
384
|
+
return isExactFacebookOrigin(url)
|
|
385
|
+
&& value.length <= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldNameCharacters
|
|
386
|
+
&& safeFieldName.test(value)
|
|
387
|
+
&& reviewedMetaRequestFieldNames.has(value)
|
|
388
|
+
? value
|
|
389
|
+
: reviewedInternalFieldNameForUrl(url, value);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function isReviewedMetaStructuralContainer(value: string): boolean {
|
|
393
|
+
return !reviewedMetaRequestFieldNames.has(value) || value === "variables";
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function reviewedInternalFieldName(value: string): string {
|
|
397
|
+
return value.length <= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldNameCharacters
|
|
398
|
+
&& safeFieldName.test(value)
|
|
399
|
+
&& !sensitiveName.test(value)
|
|
400
|
+
&& reviewedStructuralFieldNames.has(value)
|
|
401
|
+
? value
|
|
402
|
+
: ":dynamic";
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* GraphQL top-level variable names are operation schema identifiers rather
|
|
407
|
+
* than caller values. Private review may retain one only for an exact
|
|
408
|
+
* first-party X GraphQL request; values and all nested unknown map keys remain
|
|
409
|
+
* redacted by the ordinary structural reviewer.
|
|
410
|
+
*/
|
|
411
|
+
export function reviewedXGraphQlVariableFieldName(value: string): string {
|
|
412
|
+
return value.length <= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldNameCharacters
|
|
413
|
+
&& safeGraphQlVariableFieldName.test(value)
|
|
414
|
+
&& !sensitiveName.test(value)
|
|
415
|
+
? value
|
|
416
|
+
: ":dynamic";
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function isReviewedInternalDynamicMapField(value: string): boolean {
|
|
420
|
+
return reviewedDynamicMapFieldNames.has(value);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function parseRoot(value: unknown): readonly unknown[] {
|
|
424
|
+
if (!isRecord(value) || !isRecord(value.log) || !Array.isArray(value.log.entries)) {
|
|
425
|
+
throw new Error("HAR must contain log.entries[]");
|
|
426
|
+
}
|
|
427
|
+
if (value.log.entries.length > MAX_HAR_ENTRIES) throw new Error(`HAR contains more than ${MAX_HAR_ENTRIES} entries`);
|
|
428
|
+
return value.log.entries;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export type BoundedInternalHarUrl = {
|
|
432
|
+
readonly url: URL;
|
|
433
|
+
readonly pathTruncated: boolean;
|
|
434
|
+
readonly queryTruncated: boolean;
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
function boundedRawQuery(value: string): { readonly value: string; readonly truncated: boolean } {
|
|
438
|
+
if (value.length > INTERNAL_HAR_REVIEW_BOUNDS.maxQueryCharacters) {
|
|
439
|
+
return { value: "", truncated: true };
|
|
440
|
+
}
|
|
441
|
+
let separators = 0;
|
|
442
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
443
|
+
if (value[index] !== "&") continue;
|
|
444
|
+
separators += 1;
|
|
445
|
+
if (separators === INTERNAL_HAR_REVIEW_BOUNDS.maxQueryParameters) {
|
|
446
|
+
return { value: value.slice(0, index), truncated: true };
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
return { value, truncated: false };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function hasTooManyPathSegments(value: string): boolean {
|
|
453
|
+
let segments = 0;
|
|
454
|
+
let insideSegment = false;
|
|
455
|
+
for (const character of value) {
|
|
456
|
+
if (character === "/") {
|
|
457
|
+
insideSegment = false;
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (insideSegment) continue;
|
|
461
|
+
insideSegment = true;
|
|
462
|
+
segments += 1;
|
|
463
|
+
if (segments > INTERNAL_HAR_REVIEW_BOUNDS.maxPathSegments) return true;
|
|
464
|
+
}
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Bound the raw URL before asking the platform URL parser to normalize or
|
|
470
|
+
* decode it. Oversized path and query components are replaced with inert
|
|
471
|
+
* placeholders, while a query with too many parameters retains only its
|
|
472
|
+
* bounded prefix.
|
|
473
|
+
*/
|
|
474
|
+
export function parseBoundedInternalHarUrl(value: string): BoundedInternalHarUrl | null {
|
|
475
|
+
if (value.length > INTERNAL_HAR_REVIEW_BOUNDS.maxUrlCharacters) return null;
|
|
476
|
+
const fragmentIndex = value.indexOf("#");
|
|
477
|
+
const withoutFragment = fragmentIndex === -1 ? value : value.slice(0, fragmentIndex);
|
|
478
|
+
const queryIndex = withoutFragment.indexOf("?");
|
|
479
|
+
const withoutQuery = queryIndex === -1 ? withoutFragment : withoutFragment.slice(0, queryIndex);
|
|
480
|
+
const rawQuery = queryIndex === -1 ? "" : withoutFragment.slice(queryIndex + 1);
|
|
481
|
+
const schemeIndex = withoutQuery.indexOf("://");
|
|
482
|
+
const pathStart = schemeIndex === -1 ? -1 : withoutQuery.indexOf("/", schemeIndex + 3);
|
|
483
|
+
const rawPath = pathStart === -1 ? "" : withoutQuery.slice(pathStart);
|
|
484
|
+
const pathTruncated = rawPath.length > INTERNAL_HAR_REVIEW_BOUNDS.maxPathCharacters
|
|
485
|
+
|| hasTooManyPathSegments(rawPath);
|
|
486
|
+
const boundedQuery = boundedRawQuery(rawQuery);
|
|
487
|
+
const boundedBase = pathTruncated && pathStart !== -1
|
|
488
|
+
? `${withoutQuery.slice(0, pathStart)}/:oversized-path`
|
|
489
|
+
: withoutQuery;
|
|
490
|
+
const boundedValue = boundedQuery.value === "" ? boundedBase : `${boundedBase}?${boundedQuery.value}`;
|
|
491
|
+
try {
|
|
492
|
+
return {
|
|
493
|
+
url: new URL(boundedValue),
|
|
494
|
+
pathTruncated,
|
|
495
|
+
queryTruncated: boundedQuery.truncated,
|
|
496
|
+
};
|
|
497
|
+
} catch {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function reviewedXGraphQlRoute(url: URL): {
|
|
503
|
+
readonly revision: string;
|
|
504
|
+
readonly operation: string;
|
|
505
|
+
readonly reviewed: boolean;
|
|
506
|
+
readonly operationType: "query" | "mutation" | "unknown";
|
|
507
|
+
} | null {
|
|
508
|
+
if (url.origin !== "https://x.com") return null;
|
|
509
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
510
|
+
if (parts.length !== 5 || parts[0] !== "i" || parts[1] !== "api" || parts[2] !== "graphql") return null;
|
|
511
|
+
const revision = parts[3];
|
|
512
|
+
const operation = parts[4];
|
|
513
|
+
if (revision === undefined || operation === undefined
|
|
514
|
+
|| !/^[A-Za-z0-9_-]{20,64}$/u.test(revision)
|
|
515
|
+
|| !/^[A-Z][A-Za-z0-9_]{2,100}$/u.test(operation)
|
|
516
|
+
|| url.pathname !== `/i/api/graphql/${revision}/${operation}`) return null;
|
|
517
|
+
const descriptor = reviewedXGraphQlDescriptors.get(operation);
|
|
518
|
+
return {
|
|
519
|
+
revision,
|
|
520
|
+
operation,
|
|
521
|
+
reviewed: descriptor?.revision === revision,
|
|
522
|
+
operationType: descriptor?.revision === revision ? descriptor.operationType : "unknown",
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function safePath(url: URL): string {
|
|
527
|
+
const { pathname } = url;
|
|
528
|
+
if (
|
|
529
|
+
pathname.length > INTERNAL_HAR_REVIEW_BOUNDS.maxPathCharacters
|
|
530
|
+
|| hasTooManyPathSegments(pathname)
|
|
531
|
+
) return "/:oversized-path";
|
|
532
|
+
if (
|
|
533
|
+
isExactFacebookOrigin(url)
|
|
534
|
+
&& (
|
|
535
|
+
pathname === "/api/graphql/"
|
|
536
|
+
|| pathname === "/ajax/qm/"
|
|
537
|
+
|| pathname === "/data/manifest/"
|
|
538
|
+
)
|
|
539
|
+
) return pathname;
|
|
540
|
+
if (isReviewedTwitchGraphQlRoute(url)) return "/gql";
|
|
541
|
+
const xGraphQl = reviewedXGraphQlRoute(url);
|
|
542
|
+
if (xGraphQl !== null) {
|
|
543
|
+
return xGraphQl.reviewed
|
|
544
|
+
? `/i/api/graphql/:revision/${xGraphQl.operation}`
|
|
545
|
+
: "/i/api/graphql/:revision/:operation";
|
|
546
|
+
}
|
|
547
|
+
if (url.origin === "https://www.linkedin.com" && reviewedLinkedInRegisteredQueries.has(pathname)) {
|
|
548
|
+
return pathname;
|
|
549
|
+
}
|
|
550
|
+
const rawSegments = pathname.split("/");
|
|
551
|
+
let parameter = 0;
|
|
552
|
+
return rawSegments.map((raw) => {
|
|
553
|
+
let segment: string;
|
|
554
|
+
try {
|
|
555
|
+
segment = decodeURIComponent(raw);
|
|
556
|
+
} catch {
|
|
557
|
+
parameter += 1;
|
|
558
|
+
return `:segment${parameter}`;
|
|
559
|
+
}
|
|
560
|
+
if (segment === "") return "";
|
|
561
|
+
if (reviewedPathSegments.has(segment)) return segment;
|
|
562
|
+
parameter += 1;
|
|
563
|
+
return `:segment${parameter}`;
|
|
564
|
+
}).join("/");
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function headerNames(value: unknown, url: URL): readonly string[] {
|
|
568
|
+
if (!Array.isArray(value)) return [];
|
|
569
|
+
const output = new Set<string>();
|
|
570
|
+
const count = Math.min(value.length, INTERNAL_HAR_REVIEW_BOUNDS.maxHeaderItems);
|
|
571
|
+
for (let index = 0; index < count; index += 1) {
|
|
572
|
+
const candidate: unknown = value[index];
|
|
573
|
+
if (!isRecord(candidate) || typeof candidate.name !== "string") continue;
|
|
574
|
+
if (candidate.name.length > INTERNAL_HAR_REVIEW_BOUNDS.maxFieldNameCharacters) continue;
|
|
575
|
+
const name = candidate.name.toLowerCase();
|
|
576
|
+
if (!safeHeaderName.test(name)) continue;
|
|
577
|
+
output.add(
|
|
578
|
+
reviewedHeaderNames.has(name)
|
|
579
|
+
|| (isReviewedTwitchGraphQlRoute(url) && reviewedTwitchGraphQlHeaderNames.has(name))
|
|
580
|
+
? name
|
|
581
|
+
: ":dynamic",
|
|
582
|
+
);
|
|
583
|
+
if (output.size >= INTERNAL_HAR_REVIEW_BOUNDS.maxHeaderNames) break;
|
|
584
|
+
}
|
|
585
|
+
return [...output].sort();
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function addFieldPaths(
|
|
589
|
+
value: unknown,
|
|
590
|
+
output: Set<string>,
|
|
591
|
+
prefix = "",
|
|
592
|
+
depth = 0,
|
|
593
|
+
redactObjectKeys = false,
|
|
594
|
+
fieldNameReviewer: (value: string) => string = reviewedInternalFieldName,
|
|
595
|
+
shouldTraverseField: (value: string) => boolean = () => true,
|
|
596
|
+
maximumDepth: number = INTERNAL_HAR_REVIEW_BOUNDS.maxTraversalDepth,
|
|
597
|
+
): void {
|
|
598
|
+
if (
|
|
599
|
+
depth >= maximumDepth
|
|
600
|
+
|| output.size >= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldPaths
|
|
601
|
+
) return;
|
|
602
|
+
if (Array.isArray(value)) {
|
|
603
|
+
const count = Math.min(value.length, INTERNAL_HAR_REVIEW_BOUNDS.maxArrayItems);
|
|
604
|
+
for (let index = 0; index < count; index += 1) {
|
|
605
|
+
addFieldPaths(
|
|
606
|
+
value[index],
|
|
607
|
+
output,
|
|
608
|
+
`${prefix}[]`,
|
|
609
|
+
depth + 1,
|
|
610
|
+
false,
|
|
611
|
+
fieldNameReviewer,
|
|
612
|
+
shouldTraverseField,
|
|
613
|
+
maximumDepth,
|
|
614
|
+
);
|
|
615
|
+
if (output.size >= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldPaths) return;
|
|
616
|
+
}
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (!isRecord(value)) return;
|
|
620
|
+
let inspected = 0;
|
|
621
|
+
for (const key in value) {
|
|
622
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) continue;
|
|
623
|
+
if (inspected >= INTERNAL_HAR_REVIEW_BOUNDS.maxObjectEntries) return;
|
|
624
|
+
inspected += 1;
|
|
625
|
+
const child = value[key];
|
|
626
|
+
const renderedKey = redactObjectKeys ? ":dynamic" : fieldNameReviewer(key);
|
|
627
|
+
const path = prefix === "" ? renderedKey : `${prefix}.${renderedKey}`;
|
|
628
|
+
output.add(path);
|
|
629
|
+
if (!redactObjectKeys && !shouldTraverseField(key)) continue;
|
|
630
|
+
addFieldPaths(
|
|
631
|
+
child,
|
|
632
|
+
output,
|
|
633
|
+
path,
|
|
634
|
+
depth + 1,
|
|
635
|
+
!redactObjectKeys && isRecord(child) && reviewedDynamicMapFieldNames.has(key),
|
|
636
|
+
fieldNameReviewer,
|
|
637
|
+
shouldTraverseField,
|
|
638
|
+
maximumDepth,
|
|
639
|
+
);
|
|
640
|
+
if (output.size >= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldPaths) return;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function parsedJson(value: unknown): unknown {
|
|
645
|
+
if (
|
|
646
|
+
typeof value !== "string"
|
|
647
|
+
|| value.length > INTERNAL_HAR_REVIEW_BOUNDS.maxRawStringCharacters
|
|
648
|
+
|| Buffer.byteLength(value, "utf8") > INTERNAL_HAR_REVIEW_BOUNDS.maxJsonTextBytes
|
|
649
|
+
) return null;
|
|
650
|
+
try {
|
|
651
|
+
return JSON.parse(value) as unknown;
|
|
652
|
+
} catch {
|
|
653
|
+
return null;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
type BoundedFormParameters = {
|
|
658
|
+
readonly parameters: readonly JsonRecord[];
|
|
659
|
+
readonly truncated: boolean;
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
function boundedFormParameters(postData: JsonRecord): BoundedFormParameters | null {
|
|
663
|
+
if (Array.isArray(postData.params)) {
|
|
664
|
+
return {
|
|
665
|
+
parameters: postData.params
|
|
666
|
+
.slice(0, INTERNAL_HAR_REVIEW_BOUNDS.maxFormParameters)
|
|
667
|
+
.filter(isRecord),
|
|
668
|
+
truncated: postData.params.length > INTERNAL_HAR_REVIEW_BOUNDS.maxFormParameters,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
const rawMime = postData.mimeType;
|
|
672
|
+
const mime = typeof rawMime === "string"
|
|
673
|
+
&& rawMime.length <= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldNameCharacters
|
|
674
|
+
? rawMime.toLowerCase()
|
|
675
|
+
: "";
|
|
676
|
+
const text = postData.text;
|
|
677
|
+
if (
|
|
678
|
+
!mime.includes("application/x-www-form-urlencoded")
|
|
679
|
+
|| typeof text !== "string"
|
|
680
|
+
|| text.length > INTERNAL_HAR_REVIEW_BOUNDS.maxRawStringCharacters
|
|
681
|
+
|| Buffer.byteLength(text, "utf8") > INTERNAL_HAR_REVIEW_BOUNDS.maxJsonTextBytes
|
|
682
|
+
) return null;
|
|
683
|
+
const output: JsonRecord[] = [];
|
|
684
|
+
for (const [name, value] of new URLSearchParams(text)) {
|
|
685
|
+
if (output.length >= INTERNAL_HAR_REVIEW_BOUNDS.maxFormParameters) {
|
|
686
|
+
return { parameters: output, truncated: true };
|
|
687
|
+
}
|
|
688
|
+
output.push({ name, value });
|
|
689
|
+
}
|
|
690
|
+
return { parameters: output, truncated: false };
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function requestFields(request: JsonRecord, url: URL): readonly string[] {
|
|
694
|
+
if (!isRecord(request.postData)) return [];
|
|
695
|
+
let value: unknown = null;
|
|
696
|
+
const rawMime = request.postData.mimeType;
|
|
697
|
+
const mime = typeof rawMime === "string"
|
|
698
|
+
&& rawMime.length <= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldNameCharacters
|
|
699
|
+
? rawMime.toLowerCase()
|
|
700
|
+
: "";
|
|
701
|
+
const twitchTextPlainJson = request.method === "POST"
|
|
702
|
+
&& isReviewedTwitchGraphQlRoute(url)
|
|
703
|
+
&& mime.split(";", 1)[0]?.trim() === "text/plain";
|
|
704
|
+
if (mime.includes("json") || twitchTextPlainJson) value = parsedJson(request.postData.text);
|
|
705
|
+
else {
|
|
706
|
+
const form = boundedFormParameters(request.postData);
|
|
707
|
+
if (form === null) return [];
|
|
708
|
+
const fields = new Set<string>();
|
|
709
|
+
const count = Math.min(
|
|
710
|
+
form.parameters.length,
|
|
711
|
+
INTERNAL_HAR_REVIEW_BOUNDS.maxFormParameters,
|
|
712
|
+
);
|
|
713
|
+
for (let index = 0; index < count; index += 1) {
|
|
714
|
+
const candidate: unknown = form.parameters[index];
|
|
715
|
+
if (!isRecord(candidate) || typeof candidate.name !== "string") continue;
|
|
716
|
+
const renderedName = reviewedMetaRequestFieldName(url, candidate.name);
|
|
717
|
+
fields.add(renderedName);
|
|
718
|
+
if (
|
|
719
|
+
isExactFacebookOrigin(url)
|
|
720
|
+
&& candidate.name === "variables"
|
|
721
|
+
&& typeof candidate.value === "string"
|
|
722
|
+
) {
|
|
723
|
+
const parsedVariables = parsedJson(candidate.value);
|
|
724
|
+
if (parsedVariables === null) continue;
|
|
725
|
+
const variableFields = new Set<string>();
|
|
726
|
+
addFieldPaths(
|
|
727
|
+
parsedVariables,
|
|
728
|
+
variableFields,
|
|
729
|
+
"",
|
|
730
|
+
0,
|
|
731
|
+
false,
|
|
732
|
+
(fieldName) => reviewedMetaRequestFieldName(url, fieldName),
|
|
733
|
+
isReviewedMetaStructuralContainer,
|
|
734
|
+
);
|
|
735
|
+
for (const field of variableFields) fields.add(`variables.${field}`);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return [...fields].sort();
|
|
739
|
+
}
|
|
740
|
+
const fields = new Set<string>();
|
|
741
|
+
addFieldPaths(
|
|
742
|
+
value,
|
|
743
|
+
fields,
|
|
744
|
+
"",
|
|
745
|
+
0,
|
|
746
|
+
false,
|
|
747
|
+
(fieldName) => reviewedMetaRequestFieldName(url, fieldName),
|
|
748
|
+
isExactFacebookOrigin(url)
|
|
749
|
+
? isReviewedMetaStructuralContainer
|
|
750
|
+
: isReviewedTwitchGraphQlRoute(url)
|
|
751
|
+
? isReviewedTwitchStructuralContainer
|
|
752
|
+
: undefined,
|
|
753
|
+
isReviewedLinkedInArticleRoute(url)
|
|
754
|
+
? INTERNAL_HAR_REVIEW_BOUNDS.maxLinkedInArticleTraversalDepth
|
|
755
|
+
: INTERNAL_HAR_REVIEW_BOUNDS.maxTraversalDepth,
|
|
756
|
+
);
|
|
757
|
+
const xGraphQl = reviewedXGraphQlRoute(url);
|
|
758
|
+
if (
|
|
759
|
+
request.method === "POST"
|
|
760
|
+
&& xGraphQl?.reviewed === true
|
|
761
|
+
&& isRecord(value)
|
|
762
|
+
&& isRecord(value.variables)
|
|
763
|
+
) {
|
|
764
|
+
let inspected = 0;
|
|
765
|
+
for (const key in value.variables) {
|
|
766
|
+
if (!Object.prototype.hasOwnProperty.call(value.variables, key)) continue;
|
|
767
|
+
if (inspected >= INTERNAL_HAR_REVIEW_BOUNDS.maxObjectEntries) break;
|
|
768
|
+
inspected += 1;
|
|
769
|
+
fields.add(`variables.${reviewedXGraphQlVariableFieldName(key)}`);
|
|
770
|
+
if (fields.size >= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldPaths) break;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return [...fields].sort();
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function queryStructuralFields(url: URL): readonly string[] {
|
|
777
|
+
const output = new Set<string>();
|
|
778
|
+
const fieldNameReviewer = (fieldName: string): string =>
|
|
779
|
+
reviewedMetaRequestFieldName(url, fieldName);
|
|
780
|
+
for (const name of ["variables", "features", "fieldToggles"] as const) {
|
|
781
|
+
const raw = url.searchParams.get(name);
|
|
782
|
+
if (raw === null || raw.length > INTERNAL_HAR_REVIEW_BOUNDS.maxRawStringCharacters) continue;
|
|
783
|
+
const parsed = parsedJson(raw);
|
|
784
|
+
if (parsed !== null) {
|
|
785
|
+
const fields = new Set<string>();
|
|
786
|
+
addFieldPaths(
|
|
787
|
+
parsed,
|
|
788
|
+
fields,
|
|
789
|
+
"",
|
|
790
|
+
0,
|
|
791
|
+
false,
|
|
792
|
+
fieldNameReviewer,
|
|
793
|
+
isExactFacebookOrigin(url) ? isReviewedMetaStructuralContainer : undefined,
|
|
794
|
+
);
|
|
795
|
+
for (const field of fields) output.add(`query.${name}.${field}`);
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
if (name !== "variables") continue;
|
|
799
|
+
for (const match of raw.matchAll(/(?:^|[,(])([A-Za-z_$][A-Za-z0-9_$]{0,127}):/gu)) {
|
|
800
|
+
const field = match[1];
|
|
801
|
+
if (field !== undefined) output.add(`query.variables.${fieldNameReviewer(field)}`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return [...output].sort();
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function parsedMetaGraphQlDocuments(value: string): readonly unknown[] {
|
|
808
|
+
if (
|
|
809
|
+
value.length > INTERNAL_HAR_REVIEW_BOUNDS.maxRawStringCharacters
|
|
810
|
+
|| Buffer.byteLength(value, "utf8") > INTERNAL_HAR_REVIEW_BOUNDS.maxJsonTextBytes
|
|
811
|
+
) return [];
|
|
812
|
+
let payload = value.trimStart();
|
|
813
|
+
if (payload.startsWith("for (;;);")) payload = payload.slice("for (;;);".length).trimStart();
|
|
814
|
+
const whole = parsedJson(payload);
|
|
815
|
+
if (whole !== null) return [whole];
|
|
816
|
+
const lines = payload.split(/\r?\n/u);
|
|
817
|
+
if (lines.length > INTERNAL_HAR_REVIEW_BOUNDS.maxArrayItems) return [];
|
|
818
|
+
const documents: unknown[] = [];
|
|
819
|
+
for (const line of lines) {
|
|
820
|
+
const trimmed = line.trim();
|
|
821
|
+
if (trimmed === "") continue;
|
|
822
|
+
const parsed = parsedJson(trimmed);
|
|
823
|
+
if (parsed === null) return [];
|
|
824
|
+
documents.push(parsed);
|
|
825
|
+
}
|
|
826
|
+
return documents;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function responseFields(response: unknown, url: URL): readonly string[] {
|
|
830
|
+
if (!isRecord(response) || !isRecord(response.content)) return [];
|
|
831
|
+
const rawMime = response.content.mimeType;
|
|
832
|
+
const mime = typeof rawMime === "string"
|
|
833
|
+
&& rawMime.length <= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldNameCharacters
|
|
834
|
+
? rawMime.toLowerCase()
|
|
835
|
+
: "";
|
|
836
|
+
const facebookGraphQl = isExactFacebookOrigin(url) && url.pathname === "/api/graphql/";
|
|
837
|
+
if (
|
|
838
|
+
!mime.includes("json")
|
|
839
|
+
&& !(
|
|
840
|
+
facebookGraphQl
|
|
841
|
+
&& (
|
|
842
|
+
mime.includes("text/javascript")
|
|
843
|
+
|| mime.includes("text/plain")
|
|
844
|
+
|| mime.includes("text/html")
|
|
845
|
+
)
|
|
846
|
+
)
|
|
847
|
+
) return [];
|
|
848
|
+
const fields = new Set<string>();
|
|
849
|
+
const documents = facebookGraphQl && typeof response.content.text === "string"
|
|
850
|
+
? parsedMetaGraphQlDocuments(response.content.text)
|
|
851
|
+
: [parsedJson(response.content.text)];
|
|
852
|
+
for (const document of documents) {
|
|
853
|
+
addFieldPaths(
|
|
854
|
+
document,
|
|
855
|
+
fields,
|
|
856
|
+
"",
|
|
857
|
+
0,
|
|
858
|
+
false,
|
|
859
|
+
(fieldName) => reviewedMetaRequestFieldName(url, fieldName),
|
|
860
|
+
isExactFacebookOrigin(url)
|
|
861
|
+
? isReviewedMetaStructuralContainer
|
|
862
|
+
: isReviewedTwitchGraphQlRoute(url)
|
|
863
|
+
? isReviewedTwitchStructuralContainer
|
|
864
|
+
: undefined,
|
|
865
|
+
isReviewedLinkedInArticleRoute(url)
|
|
866
|
+
? INTERNAL_HAR_REVIEW_BOUNDS.maxLinkedInArticleTraversalDepth
|
|
867
|
+
: INTERNAL_HAR_REVIEW_BOUNDS.maxTraversalDepth,
|
|
868
|
+
);
|
|
869
|
+
if (fields.size >= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldPaths) break;
|
|
870
|
+
}
|
|
871
|
+
return [...fields].sort();
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function metaRegisteredOperationRevision(
|
|
875
|
+
url: URL,
|
|
876
|
+
method: string,
|
|
877
|
+
request: JsonRecord,
|
|
878
|
+
): string | null {
|
|
879
|
+
if (
|
|
880
|
+
!isExactFacebookOrigin(url)
|
|
881
|
+
|| method !== "POST"
|
|
882
|
+
|| url.pathname !== "/api/graphql/"
|
|
883
|
+
|| !isRecord(request.postData)
|
|
884
|
+
) return null;
|
|
885
|
+
const form = boundedFormParameters(request.postData);
|
|
886
|
+
if (form === null || form.truncated) return null;
|
|
887
|
+
const friendlyNames: unknown[] = [];
|
|
888
|
+
const documentIds: unknown[] = [];
|
|
889
|
+
for (const candidate of form.parameters) {
|
|
890
|
+
if (!isRecord(candidate) || typeof candidate.name !== "string") continue;
|
|
891
|
+
if (candidate.name === "fb_api_req_friendly_name") friendlyNames.push(candidate.value);
|
|
892
|
+
if (candidate.name === "doc_id") documentIds.push(candidate.value);
|
|
893
|
+
}
|
|
894
|
+
if (friendlyNames.length !== 1 || documentIds.length !== 1) return null;
|
|
895
|
+
const friendlyName = friendlyNames[0];
|
|
896
|
+
const documentId = documentIds[0];
|
|
897
|
+
if (
|
|
898
|
+
typeof friendlyName !== "string"
|
|
899
|
+
|| typeof documentId !== "string"
|
|
900
|
+
|| friendlyName.length > 161
|
|
901
|
+
|| documentId.length > 32
|
|
902
|
+
|| !/^[A-Za-z][A-Za-z0-9_]{2,160}$/u.test(friendlyName)
|
|
903
|
+
|| !/^[0-9]{5,32}$/u.test(documentId)
|
|
904
|
+
) return null;
|
|
905
|
+
return `meta=${friendlyName}.${documentId}`;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function exactOwnKeys(
|
|
909
|
+
value: JsonRecord,
|
|
910
|
+
expected: readonly string[],
|
|
911
|
+
): boolean {
|
|
912
|
+
const keys = Object.keys(value).sort();
|
|
913
|
+
return keys.length === expected.length
|
|
914
|
+
&& keys.every((key, index) => key === expected[index]);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function twitchRegisteredOperationRevisions(
|
|
918
|
+
url: URL,
|
|
919
|
+
method: string,
|
|
920
|
+
request: JsonRecord,
|
|
921
|
+
): readonly string[] {
|
|
922
|
+
if (
|
|
923
|
+
method !== "POST"
|
|
924
|
+
|| !isReviewedTwitchGraphQlRoute(url)
|
|
925
|
+
|| !isRecord(request.postData)
|
|
926
|
+
) return [];
|
|
927
|
+
const rawMime = request.postData.mimeType;
|
|
928
|
+
if (
|
|
929
|
+
typeof rawMime !== "string"
|
|
930
|
+
|| rawMime.length > INTERNAL_HAR_REVIEW_BOUNDS.maxFieldNameCharacters
|
|
931
|
+
|| rawMime.toLowerCase().split(";", 1)[0]?.trim() !== "text/plain"
|
|
932
|
+
) return [];
|
|
933
|
+
const body = parsedJson(request.postData.text);
|
|
934
|
+
if (
|
|
935
|
+
!Array.isArray(body)
|
|
936
|
+
|| body.length !== 1
|
|
937
|
+
) return [];
|
|
938
|
+
const candidate = body[0];
|
|
939
|
+
if (
|
|
940
|
+
!isRecord(candidate)
|
|
941
|
+
|| !exactOwnKeys(candidate, ["extensions", "operationName", "variables"])
|
|
942
|
+
|| typeof candidate.operationName !== "string"
|
|
943
|
+
|| !reviewedTwitchGraphQlOperationNames.has(candidate.operationName)
|
|
944
|
+
|| !isRecord(candidate.variables)
|
|
945
|
+
|| !isRecord(candidate.extensions)
|
|
946
|
+
|| !exactOwnKeys(candidate.extensions, ["persistedQuery"])
|
|
947
|
+
|| !isRecord(candidate.extensions.persistedQuery)
|
|
948
|
+
|| !exactOwnKeys(candidate.extensions.persistedQuery, ["sha256Hash", "version"])
|
|
949
|
+
|| candidate.extensions.persistedQuery.version !== 1
|
|
950
|
+
|| typeof candidate.extensions.persistedQuery.sha256Hash !== "string"
|
|
951
|
+
|| !/^[a-f0-9]{64}$/u.test(candidate.extensions.persistedQuery.sha256Hash)
|
|
952
|
+
) return [];
|
|
953
|
+
if (
|
|
954
|
+
candidate.operationName === "TopNav_CurrentUser"
|
|
955
|
+
&& !exactOwnKeys(candidate.variables, [])
|
|
956
|
+
) return [];
|
|
957
|
+
if (
|
|
958
|
+
candidate.operationName === "ChannelRoot_AboutPanel"
|
|
959
|
+
&& (
|
|
960
|
+
!exactOwnKeys(candidate.variables, ["channelLogin", "skipSchedule"])
|
|
961
|
+
|| typeof candidate.variables.channelLogin !== "string"
|
|
962
|
+
|| !/^[a-z0-9_]{4,25}$/u.test(candidate.variables.channelLogin)
|
|
963
|
+
|| candidate.variables.skipSchedule !== true
|
|
964
|
+
)
|
|
965
|
+
) return [];
|
|
966
|
+
return [
|
|
967
|
+
`twitch=${candidate.operationName}.${candidate.extensions.persistedQuery.sha256Hash}`,
|
|
968
|
+
];
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
function revisionValues(url: URL, method: string, request: JsonRecord): readonly string[] {
|
|
972
|
+
const values: string[] = [];
|
|
973
|
+
if (url.origin === "https://www.linkedin.com" && method === "GET") {
|
|
974
|
+
const prefixes = reviewedLinkedInRegisteredQueries.get(url.pathname);
|
|
975
|
+
if (prefixes !== undefined) {
|
|
976
|
+
for (const value of url.searchParams.getAll("queryId")) {
|
|
977
|
+
if (value.length > INTERNAL_HAR_REVIEW_BOUNDS.maxQueryIdCharacters) continue;
|
|
978
|
+
const separator = value.lastIndexOf(".");
|
|
979
|
+
const prefix = value.slice(0, separator);
|
|
980
|
+
const revision = value.slice(separator + 1);
|
|
981
|
+
if (prefixes.has(prefix) && /^[a-f0-9]{32}$/u.test(revision)) values.push(`queryId=${value}`);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
const xGraphQl = reviewedXGraphQlRoute(url);
|
|
986
|
+
if (xGraphQl?.reviewed === true) values.push(`graphql=${xGraphQl.operation}.${xGraphQl.revision}`);
|
|
987
|
+
for (const revision of twitchRegisteredOperationRevisions(url, method, request)) {
|
|
988
|
+
values.push(revision);
|
|
989
|
+
}
|
|
990
|
+
const metaRevision = metaRegisteredOperationRevision(url, method, request);
|
|
991
|
+
if (metaRevision !== null) values.push(metaRevision);
|
|
992
|
+
return values;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function registeredOperationType(
|
|
996
|
+
url: URL,
|
|
997
|
+
revision: string | null,
|
|
998
|
+
): InternalHarCandidate["operationType"] {
|
|
999
|
+
if (revision === null) return "unknown";
|
|
1000
|
+
const xGraphQl = reviewedXGraphQlRoute(url);
|
|
1001
|
+
if (xGraphQl?.reviewed === true) return xGraphQl.operationType;
|
|
1002
|
+
if (revision.startsWith("queryId=")) return "query";
|
|
1003
|
+
if (revision.startsWith("twitch=")) return "query";
|
|
1004
|
+
// Meta friendly names are labels, not authoritative GraphQL syntax. Keep
|
|
1005
|
+
// their type unknown until a code-owned reviewed descriptor supplies it.
|
|
1006
|
+
return "unknown";
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
type Accumulator = {
|
|
1010
|
+
method: string;
|
|
1011
|
+
origin: string;
|
|
1012
|
+
path: string;
|
|
1013
|
+
operationType: InternalHarCandidate["operationType"];
|
|
1014
|
+
semanticRevision: string | null;
|
|
1015
|
+
sampleCount: number;
|
|
1016
|
+
statuses: Set<number>;
|
|
1017
|
+
queryNames: Set<string>;
|
|
1018
|
+
headerNames: Set<string>;
|
|
1019
|
+
requestFieldPaths: Set<string>;
|
|
1020
|
+
responseFieldPaths: Set<string>;
|
|
1021
|
+
revisions: Set<string>;
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
type CandidateCount = {
|
|
1025
|
+
readonly method: string;
|
|
1026
|
+
readonly origin: string;
|
|
1027
|
+
readonly path: string;
|
|
1028
|
+
readonly operationType: InternalHarCandidate["operationType"];
|
|
1029
|
+
readonly semanticRevision: string | null;
|
|
1030
|
+
sampleCount: number;
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
type ReviewedEntry = {
|
|
1034
|
+
readonly entry: JsonRecord;
|
|
1035
|
+
readonly request: JsonRecord;
|
|
1036
|
+
readonly method: string;
|
|
1037
|
+
readonly url: URL;
|
|
1038
|
+
readonly key: string;
|
|
1039
|
+
readonly path: string;
|
|
1040
|
+
readonly operationType: InternalHarCandidate["operationType"];
|
|
1041
|
+
readonly semanticRevision: string | null;
|
|
1042
|
+
};
|
|
1043
|
+
|
|
1044
|
+
function reviewedEntry(
|
|
1045
|
+
entry: unknown,
|
|
1046
|
+
targetOrigin: string,
|
|
1047
|
+
): ReviewedEntry | null {
|
|
1048
|
+
if (
|
|
1049
|
+
!isRecord(entry)
|
|
1050
|
+
|| !isRecord(entry.request)
|
|
1051
|
+
|| typeof entry.request.url !== "string"
|
|
1052
|
+
|| typeof entry.request.method !== "string"
|
|
1053
|
+
|| entry.request.method.length > 16
|
|
1054
|
+
) return null;
|
|
1055
|
+
const boundedUrl = parseBoundedInternalHarUrl(entry.request.url);
|
|
1056
|
+
if (boundedUrl === null) return null;
|
|
1057
|
+
const { url } = boundedUrl;
|
|
1058
|
+
if (url.origin !== targetOrigin || url.username !== "" || url.password !== "") return null;
|
|
1059
|
+
const method = entry.request.method.toUpperCase();
|
|
1060
|
+
if (!/^(?:GET|HEAD|POST|PUT|PATCH|DELETE)$/u.test(method)) return null;
|
|
1061
|
+
if (/\.(?:css|js|png|jpe?g|gif|svg|woff2?|ico|map)$/iu.test(url.pathname)) return null;
|
|
1062
|
+
const path = boundedUrl.pathTruncated ? "/:oversized-path" : safePath(url);
|
|
1063
|
+
const revisions = [...new Set(revisionValues(url, method, entry.request))];
|
|
1064
|
+
const semanticRevision = revisions.length === 1 ? revisions[0] ?? null : null;
|
|
1065
|
+
const operationType = registeredOperationType(url, semanticRevision);
|
|
1066
|
+
return {
|
|
1067
|
+
entry,
|
|
1068
|
+
request: entry.request,
|
|
1069
|
+
method,
|
|
1070
|
+
url,
|
|
1071
|
+
key: `${method}\0${path}\0${semanticRevision ?? ""}`,
|
|
1072
|
+
path,
|
|
1073
|
+
operationType,
|
|
1074
|
+
semanticRevision,
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
function compareCandidateCounts(left: CandidateCount, right: CandidateCount): number {
|
|
1079
|
+
return right.sampleCount - left.sampleCount
|
|
1080
|
+
|| left.path.localeCompare(right.path)
|
|
1081
|
+
|| left.method.localeCompare(right.method)
|
|
1082
|
+
|| (left.semanticRevision ?? "").localeCompare(right.semanticRevision ?? "");
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* Retain the exact non-secret names needed to review a first-party API
|
|
1087
|
+
* contract. No URL values, headers, cookies, body values, or response values
|
|
1088
|
+
* survive this analyzer.
|
|
1089
|
+
*/
|
|
1090
|
+
export function analyzeInternalHarValue(
|
|
1091
|
+
value: unknown,
|
|
1092
|
+
adapterId: string,
|
|
1093
|
+
targetOrigin: string,
|
|
1094
|
+
now = new Date(),
|
|
1095
|
+
): InternalHarEvidence {
|
|
1096
|
+
const target = new URL(targetOrigin);
|
|
1097
|
+
if (target.protocol !== "https:" || target.origin !== targetOrigin) throw new Error("target origin must be exact HTTPS");
|
|
1098
|
+
const entries = parseRoot(value);
|
|
1099
|
+
const counts = new Map<string, CandidateCount>();
|
|
1100
|
+
const reviewedKeys: (string | null)[] = [];
|
|
1101
|
+
for (const entry of entries) {
|
|
1102
|
+
const reviewed = reviewedEntry(entry, target.origin);
|
|
1103
|
+
reviewedKeys.push(reviewed?.key ?? null);
|
|
1104
|
+
if (reviewed === null) continue;
|
|
1105
|
+
const current = counts.get(reviewed.key) ?? {
|
|
1106
|
+
method: reviewed.method,
|
|
1107
|
+
origin: reviewed.url.origin,
|
|
1108
|
+
path: reviewed.path,
|
|
1109
|
+
operationType: reviewed.operationType,
|
|
1110
|
+
semanticRevision: reviewed.semanticRevision,
|
|
1111
|
+
sampleCount: 0,
|
|
1112
|
+
};
|
|
1113
|
+
current.sampleCount += 1;
|
|
1114
|
+
counts.set(reviewed.key, current);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
const selectedCounts = [...counts.entries()]
|
|
1118
|
+
.sort((left, right) => compareCandidateCounts(left[1], right[1]))
|
|
1119
|
+
.slice(0, INTERNAL_HAR_REVIEW_BOUNDS.maxCandidates);
|
|
1120
|
+
const candidates = new Map<string, Accumulator>(selectedCounts.map(([key, candidate]) => [
|
|
1121
|
+
key,
|
|
1122
|
+
{
|
|
1123
|
+
...candidate,
|
|
1124
|
+
statuses: new Set<number>(),
|
|
1125
|
+
queryNames: new Set<string>(),
|
|
1126
|
+
headerNames: new Set<string>(),
|
|
1127
|
+
requestFieldPaths: new Set<string>(),
|
|
1128
|
+
responseFieldPaths: new Set<string>(),
|
|
1129
|
+
revisions: new Set<string>(),
|
|
1130
|
+
},
|
|
1131
|
+
]));
|
|
1132
|
+
const selectedEntries = new Map<string, unknown[]>(
|
|
1133
|
+
selectedCounts.map(([key]) => [key, []]),
|
|
1134
|
+
);
|
|
1135
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
1136
|
+
const key = reviewedKeys[index];
|
|
1137
|
+
if (key === undefined || key === null) continue;
|
|
1138
|
+
selectedEntries.get(key)?.push(entries[index]);
|
|
1139
|
+
}
|
|
1140
|
+
let retainedFieldPaths = 0;
|
|
1141
|
+
let retainedRevisions = 0;
|
|
1142
|
+
const addFieldPath = (output: Set<string>, value: string): void => {
|
|
1143
|
+
if (
|
|
1144
|
+
output.has(value)
|
|
1145
|
+
|| output.size >= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldPaths
|
|
1146
|
+
|| retainedFieldPaths >= INTERNAL_HAR_REVIEW_BOUNDS.maxTotalFieldPaths
|
|
1147
|
+
) return;
|
|
1148
|
+
output.add(value);
|
|
1149
|
+
retainedFieldPaths += 1;
|
|
1150
|
+
};
|
|
1151
|
+
const addRevision = (output: Set<string>, value: string): void => {
|
|
1152
|
+
if (
|
|
1153
|
+
output.has(value)
|
|
1154
|
+
|| output.size >= INTERNAL_HAR_REVIEW_BOUNDS.maxRevisions
|
|
1155
|
+
|| retainedRevisions >= INTERNAL_HAR_REVIEW_BOUNDS.maxTotalRevisions
|
|
1156
|
+
) return;
|
|
1157
|
+
output.add(value);
|
|
1158
|
+
retainedRevisions += 1;
|
|
1159
|
+
};
|
|
1160
|
+
|
|
1161
|
+
// Enrich in the same deterministic rank order exposed to the reviewer so a
|
|
1162
|
+
// low-ranked prefix of the raw HAR cannot consume the global evidence
|
|
1163
|
+
// budget before a frequently observed candidate is processed.
|
|
1164
|
+
for (const [key] of selectedCounts) {
|
|
1165
|
+
const current = candidates.get(key);
|
|
1166
|
+
if (current === undefined) continue;
|
|
1167
|
+
for (const entry of selectedEntries.get(key) ?? []) {
|
|
1168
|
+
const reviewed = reviewedEntry(entry, target.origin);
|
|
1169
|
+
if (reviewed === null || reviewed.key !== key) continue;
|
|
1170
|
+
if (
|
|
1171
|
+
isRecord(reviewed.entry.response)
|
|
1172
|
+
&& typeof reviewed.entry.response.status === "number"
|
|
1173
|
+
&& Number.isSafeInteger(reviewed.entry.response.status)
|
|
1174
|
+
&& (
|
|
1175
|
+
reviewed.entry.response.status === 0
|
|
1176
|
+
|| (reviewed.entry.response.status >= 100 && reviewed.entry.response.status <= 599)
|
|
1177
|
+
)
|
|
1178
|
+
) {
|
|
1179
|
+
current.statuses.add(reviewed.entry.response.status);
|
|
1180
|
+
}
|
|
1181
|
+
for (const name of reviewed.url.searchParams.keys()) {
|
|
1182
|
+
if (name.length > INTERNAL_HAR_REVIEW_BOUNDS.maxFieldNameCharacters) continue;
|
|
1183
|
+
const renderedName = reviewedMetaRequestFieldName(reviewed.url, name);
|
|
1184
|
+
if (sensitiveName.test(name) && renderedName === ":dynamic") continue;
|
|
1185
|
+
current.queryNames.add(renderedName);
|
|
1186
|
+
}
|
|
1187
|
+
for (const name of headerNames(reviewed.request.headers, reviewed.url)) current.headerNames.add(name);
|
|
1188
|
+
for (const pathName of requestFields(reviewed.request, reviewed.url)) {
|
|
1189
|
+
addFieldPath(current.requestFieldPaths, pathName);
|
|
1190
|
+
}
|
|
1191
|
+
for (const pathName of queryStructuralFields(reviewed.url)) addFieldPath(current.requestFieldPaths, pathName);
|
|
1192
|
+
for (const pathName of responseFields(reviewed.entry.response, reviewed.url)) {
|
|
1193
|
+
addFieldPath(current.responseFieldPaths, pathName);
|
|
1194
|
+
}
|
|
1195
|
+
for (const revision of revisionValues(reviewed.url, reviewed.method, reviewed.request)) {
|
|
1196
|
+
addRevision(current.revisions, revision);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return {
|
|
1201
|
+
schemaVersion: 1,
|
|
1202
|
+
adapterId,
|
|
1203
|
+
targetOrigin,
|
|
1204
|
+
analyzedAt: now.toISOString(),
|
|
1205
|
+
observedEntries: entries.length,
|
|
1206
|
+
candidates: [...candidates.values()]
|
|
1207
|
+
.sort(compareCandidateCounts)
|
|
1208
|
+
.map((candidate) => ({
|
|
1209
|
+
method: candidate.method,
|
|
1210
|
+
origin: candidate.origin,
|
|
1211
|
+
path: candidate.path,
|
|
1212
|
+
operationType: candidate.operationType,
|
|
1213
|
+
sampleCount: candidate.sampleCount,
|
|
1214
|
+
statuses: [...candidate.statuses].sort((left, right) => left - right),
|
|
1215
|
+
queryNames: [...candidate.queryNames].sort(),
|
|
1216
|
+
headerNames: [...candidate.headerNames].sort(),
|
|
1217
|
+
requestFieldPaths: [...candidate.requestFieldPaths].sort(),
|
|
1218
|
+
responseFieldPaths: [...candidate.responseFieldPaths].sort(),
|
|
1219
|
+
revisions: [...candidate.revisions].sort(),
|
|
1220
|
+
reviewRequired: true,
|
|
1221
|
+
})),
|
|
1222
|
+
warnings: [
|
|
1223
|
+
"This evidence is inert: every operation still requires a reviewed semantic contract before execution.",
|
|
1224
|
+
"operationType is authoritative only for reviewed route facts; Meta friendly-name evidence remains unknown until bound to a code-owned descriptor.",
|
|
1225
|
+
"Only first-party paths, names, structural field paths, and registered-operation revisions are retained; all credential and content values are discarded.",
|
|
1226
|
+
"Candidate, structural-field, and registered-revision evidence is bounded; lower-ranked candidates and excess fields or revisions may be omitted.",
|
|
1227
|
+
],
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
export function analyzeInternalHarFile(
|
|
1232
|
+
path: string,
|
|
1233
|
+
adapterId: string,
|
|
1234
|
+
targetOrigin: string,
|
|
1235
|
+
expectedParent?: Readonly<PrivateDirectoryIdentity>,
|
|
1236
|
+
): InternalHarEvidence {
|
|
1237
|
+
return analyzeInternalHarValue(
|
|
1238
|
+
JSON.parse(readRegularFile(path, MAX_HAR_BYTES, "HAR input", expectedParent)) as unknown,
|
|
1239
|
+
adapterId,
|
|
1240
|
+
targetOrigin,
|
|
1241
|
+
);
|
|
1242
|
+
}
|