@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,832 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import type { GhostgetAuth } from "../auth";
|
|
4
|
+
import {
|
|
5
|
+
PreservedBrowserArtifactsError,
|
|
6
|
+
browserResultData,
|
|
7
|
+
createBrowserSession,
|
|
8
|
+
runCommand,
|
|
9
|
+
type BrowserSession,
|
|
10
|
+
type CommandResult,
|
|
11
|
+
type CreateBrowserSessionOptions,
|
|
12
|
+
} from "../browser";
|
|
13
|
+
import type { GhostgetManifest } from "../model";
|
|
14
|
+
import type {
|
|
15
|
+
WebSessionCleanupResourcePublisher,
|
|
16
|
+
WebSessionOperationDeadline,
|
|
17
|
+
} from "../web-session-execution";
|
|
18
|
+
import {
|
|
19
|
+
LINKEDIN_GRAPHQL_PATH,
|
|
20
|
+
LINKEDIN_PROFILE_ACTIVITY_QUERY_PREFIX,
|
|
21
|
+
assertLinkedInProfileActivityRequest,
|
|
22
|
+
linkedInProfileActivityPageUrl,
|
|
23
|
+
linkedInProfileActivityTargetFromVanity,
|
|
24
|
+
resolveLinkedInProfileActivityBinding,
|
|
25
|
+
type LinkedInProfileActivityBinding,
|
|
26
|
+
} from "./linkedin-web-feed";
|
|
27
|
+
import { jsonScriptLiteral } from "../canonical-json";
|
|
28
|
+
|
|
29
|
+
const LINKEDIN_ORIGIN = "https://www.linkedin.com";
|
|
30
|
+
const LINKEDIN_FEED_URL = `${LINKEDIN_ORIGIN}/feed/`;
|
|
31
|
+
const LINKEDIN_PRE_COOKIE_REALM_URL = `${LINKEDIN_ORIGIN}/robots.txt`;
|
|
32
|
+
const LINKEDIN_INITIAL_ROOT_BATCH = JSON.stringify([["open", LINKEDIN_ORIGIN]]);
|
|
33
|
+
const LINKEDIN_INITIAL_BLANK_BATCH = JSON.stringify([["open", "about:blank"]]);
|
|
34
|
+
const LINKEDIN_INITIAL_REALM_BATCH = JSON.stringify([["open", LINKEDIN_PRE_COOKIE_REALM_URL]]);
|
|
35
|
+
const MAX_IDENTITY_BYTES = 2 * 1024 * 1024;
|
|
36
|
+
const MAX_FEED_PAGE_BYTES = 8 * 1024 * 1024;
|
|
37
|
+
const BROWSER_ENVELOPE_BYTES = 64 * 1024;
|
|
38
|
+
const MAX_NETWORK_REQUESTS = 10_000;
|
|
39
|
+
const MAX_REQUEST_URL_CHARACTERS = 64 * 1_024;
|
|
40
|
+
|
|
41
|
+
const feedBrowserManifest: GhostgetManifest = Object.freeze({
|
|
42
|
+
schemaVersion: 4,
|
|
43
|
+
id: "linkedin-profile-activity-runtime",
|
|
44
|
+
version: "1.0.0",
|
|
45
|
+
displayName: "LinkedIn profile-activity runtime",
|
|
46
|
+
surfaceId: "linkedin",
|
|
47
|
+
origins: Object.freeze([LINKEDIN_ORIGIN]),
|
|
48
|
+
browserDomains: Object.freeze(["www.linkedin.com", "static.licdn.com"]),
|
|
49
|
+
operations: Object.freeze({}),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export type LinkedInFeedBrowserTransport = {
|
|
53
|
+
readonly currentIdentityResponse: () => Promise<unknown>;
|
|
54
|
+
readonly resolveProfileActivityBinding: (vanity: string) => Promise<LinkedInProfileActivityBinding>;
|
|
55
|
+
readonly readProfileActivityPage: (input: {
|
|
56
|
+
readonly count: number;
|
|
57
|
+
readonly start: number;
|
|
58
|
+
}) => Promise<unknown>;
|
|
59
|
+
readonly close: () => Promise<void>;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type LinkedInFeedBrowserDependencies = {
|
|
63
|
+
readonly createBrowserSession: typeof createBrowserSession;
|
|
64
|
+
readonly runCommand: typeof runCommand;
|
|
65
|
+
readonly settleContext: () => Promise<void>;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type BrowserReadBinding = {
|
|
69
|
+
readonly documentUrl: string | null;
|
|
70
|
+
readonly kind: "json";
|
|
71
|
+
readonly maxBytes: number;
|
|
72
|
+
readonly path: string;
|
|
73
|
+
readonly referrer: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
type BrowserReadState = "ready" | "identity" | "bound";
|
|
77
|
+
|
|
78
|
+
type ObservedBrowserRequest = Readonly<Record<string, unknown>> & {
|
|
79
|
+
readonly requestId: string;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
type BoundProfileActivityPage = {
|
|
83
|
+
readonly binding: LinkedInProfileActivityBinding;
|
|
84
|
+
readonly targetUrl: string;
|
|
85
|
+
readonly vanity: string;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const LINKEDIN_RESPONSE_MEDIA_TYPE =
|
|
89
|
+
/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/u;
|
|
90
|
+
|
|
91
|
+
export type LinkedInFeedBrowserFailureCategory =
|
|
92
|
+
| "authwall"
|
|
93
|
+
| "body-envelope"
|
|
94
|
+
| "bootstrap"
|
|
95
|
+
| "browser-envelope"
|
|
96
|
+
| "browser-command"
|
|
97
|
+
| "execution-context"
|
|
98
|
+
| "identity-json"
|
|
99
|
+
| "output-bound"
|
|
100
|
+
| "provider-fetch"
|
|
101
|
+
| "response-envelope"
|
|
102
|
+
| "response-rejected"
|
|
103
|
+
| "session-cookie"
|
|
104
|
+
| "startup";
|
|
105
|
+
|
|
106
|
+
export class LinkedInFeedBrowserFailure extends Error {
|
|
107
|
+
readonly category: LinkedInFeedBrowserFailureCategory;
|
|
108
|
+
|
|
109
|
+
constructor(category: LinkedInFeedBrowserFailureCategory, message: string) {
|
|
110
|
+
super(message);
|
|
111
|
+
this.name = "LinkedInFeedBrowserFailure";
|
|
112
|
+
this.category = category;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export class LinkedInFeedBrowserResponseRejectedError extends LinkedInFeedBrowserFailure {
|
|
117
|
+
readonly status: number;
|
|
118
|
+
readonly contentType: string;
|
|
119
|
+
|
|
120
|
+
constructor(status: number, contentType: string) {
|
|
121
|
+
super(
|
|
122
|
+
"response-rejected",
|
|
123
|
+
"LinkedIn profile-activity browser request returned a reviewed rejection",
|
|
124
|
+
);
|
|
125
|
+
this.name = "LinkedInFeedBrowserResponseRejectedError";
|
|
126
|
+
if (
|
|
127
|
+
!Number.isSafeInteger(status)
|
|
128
|
+
|| status < 100
|
|
129
|
+
|| status > 599
|
|
130
|
+
|| contentType.length > 128
|
|
131
|
+
|| (contentType !== "" && !LINKEDIN_RESPONSE_MEDIA_TYPE.test(contentType))
|
|
132
|
+
) throw new Error("LinkedIn profile-activity browser returned a malformed response category");
|
|
133
|
+
this.status = status;
|
|
134
|
+
this.contentType = contentType === "" ? "missing" : contentType;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
139
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function exactKeys(
|
|
143
|
+
value: Readonly<Record<string, unknown>>,
|
|
144
|
+
expected: readonly string[],
|
|
145
|
+
label: string,
|
|
146
|
+
): void {
|
|
147
|
+
if (Object.keys(value).sort().join(",") !== [...expected].sort().join(",")) {
|
|
148
|
+
throw new Error(`${label} returned an unexpected result shape`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function browserReadEvaluationSource(binding: BrowserReadBinding): string {
|
|
153
|
+
const bound = jsonScriptLiteral(binding);
|
|
154
|
+
return `(async()=>{const input=${bound};if(location.origin!=="${LINKEDIN_ORIGIN}")throw new Error("unexpected LinkedIn origin");if(input.kind!=="json"||!Number.isSafeInteger(input.maxBytes)||input.maxBytes<1||input.maxBytes>${MAX_FEED_PAGE_BYTES}||(input.documentUrl!==null&&typeof input.documentUrl!=="string"))throw new Error("invalid LinkedIn profile-activity browser request binding");if(input.documentUrl!==null&&location.href!==input.documentUrl){if(location.origin==="${LINKEDIN_ORIGIN}"&&/^\/(?:authwall|checkpoint|login|uas\/login(?:-submit)?)(?:\/|$)/u.test(location.pathname))throw new Error("LinkedIn profile-activity browser reached its signed-out authwall");throw new Error("LinkedIn profile-activity browser left its bound document")}const expected=new URL(input.path,"${LINKEDIN_ORIGIN}");if(expected.origin!=="${LINKEDIN_ORIGIN}"||expected.username!==""||expected.password!==""||expected.hash!==""||expected.href!=="${LINKEDIN_ORIGIN}"+input.path)throw new Error("invalid LinkedIn profile-activity browser path binding");if(expected.pathname!=="/voyager/api/me"&&expected.pathname!=="${LINKEDIN_GRAPHQL_PATH}")throw new Error("invalid LinkedIn profile-activity browser path binding");const headers={accept:"application/vnd.linkedin.normalized+json+2.1","x-li-lang":"en_US","x-requested-with":"XMLHttpRequest","x-restli-protocol-version":"2.0.0"};const raw=document.cookie.split("; ").find((part)=>part.startsWith("JSESSIONID="));if(typeof raw!=="string")throw new Error("missing LinkedIn browser CSRF cookie");const csrf=decodeURIComponent(raw.slice("JSESSIONID=".length)).replace(/^\"|\"$/g,"");if(!/^ajax:[A-Za-z0-9_-]{1,512}$/.test(csrf))throw new Error("invalid LinkedIn browser CSRF cookie");headers["csrf-token"]=csrf;const response=await fetch(input.path,{credentials:"include",headers,method:"GET",redirect:"manual",referrer:input.referrer});if(response.type==="opaqueredirect"){response.body?.cancel();return{authWall:true,bodyBase64:null,bodyBytes:0,bodySha256:null,contentType:"",status:0}}const responseUrl=new URL(response.url);if(responseUrl.origin!=="${LINKEDIN_ORIGIN}"||responseUrl.username!==""||responseUrl.password!==""||responseUrl.hash!==""||responseUrl.href!==expected.href)throw new Error("LinkedIn profile-activity browser response escaped its exact route");if(response.status>=300&&response.status<=399){response.body?.cancel();return{authWall:true,bodyBase64:null,bodyBytes:0,bodySha256:null,contentType:"",status:response.status}}const contentType=(response.headers.get("content-type")||"").split(";",1)[0].trim().toLowerCase();if(response.status!==200||(contentType!=="application/vnd.linkedin.normalized+json+2.1"&&contentType!=="application/json")){response.body?.cancel();return{authWall:false,bodyBase64:null,bodyBytes:0,bodySha256:null,contentType,status:response.status}}if(response.body===null)throw new Error("LinkedIn profile-activity browser response omitted its body");const reader=response.body.getReader();const chunks=[];let bytes=0;while(true){const part=await reader.read();if(part.done)break;bytes+=part.value.byteLength;if(bytes>input.maxBytes){await reader.cancel();throw new Error("LinkedIn profile-activity browser response exceeded its reviewed byte bound")}chunks.push(part.value)}const body=new Uint8Array(bytes);let cursor=0;for(const chunk of chunks){body.set(chunk,cursor);cursor+=chunk.byteLength}const digest=Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256",body)),(value)=>value.toString(16).padStart(2,"0")).join("");let binary="";for(let offset=0;offset<body.length;offset+=32768)binary+=String.fromCharCode(...body.subarray(offset,Math.min(offset+32768,body.length)));return{authWall:false,bodyBase64:btoa(binary),bodyBytes:body.byteLength,bodySha256:digest,contentType,status:response.status}})()`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function encodedBodyBound(bytes: number): number {
|
|
158
|
+
return Math.ceil(bytes / 3) * 4;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function decodedBody(
|
|
162
|
+
result: Readonly<Record<string, unknown>>,
|
|
163
|
+
maximumBytes: number,
|
|
164
|
+
): string {
|
|
165
|
+
if (
|
|
166
|
+
typeof result.bodyBase64 !== "string"
|
|
167
|
+
|| result.bodyBase64.length > encodedBodyBound(maximumBytes)
|
|
168
|
+
|| !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(
|
|
169
|
+
result.bodyBase64,
|
|
170
|
+
)
|
|
171
|
+
|| !Number.isSafeInteger(result.bodyBytes)
|
|
172
|
+
|| (result.bodyBytes as number) < 0
|
|
173
|
+
|| (result.bodyBytes as number) > maximumBytes
|
|
174
|
+
|| typeof result.bodySha256 !== "string"
|
|
175
|
+
|| !/^[a-f0-9]{64}$/u.test(result.bodySha256)
|
|
176
|
+
) throw new LinkedInFeedBrowserFailure(
|
|
177
|
+
"body-envelope",
|
|
178
|
+
"LinkedIn profile-activity browser body envelope changed shape",
|
|
179
|
+
);
|
|
180
|
+
const bytes = Buffer.from(result.bodyBase64, "base64");
|
|
181
|
+
if (
|
|
182
|
+
bytes.byteLength !== result.bodyBytes
|
|
183
|
+
|| bytes.toString("base64") !== result.bodyBase64
|
|
184
|
+
|| createHash("sha256").update(bytes).digest("hex") !== result.bodySha256
|
|
185
|
+
) throw new LinkedInFeedBrowserFailure(
|
|
186
|
+
"body-envelope",
|
|
187
|
+
"LinkedIn profile-activity browser body envelope failed integrity verification",
|
|
188
|
+
);
|
|
189
|
+
try {
|
|
190
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
191
|
+
} catch {
|
|
192
|
+
throw new LinkedInFeedBrowserFailure(
|
|
193
|
+
"body-envelope",
|
|
194
|
+
"LinkedIn profile-activity browser body was not valid UTF-8",
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function browserEvaluationResult(
|
|
200
|
+
value: Readonly<Record<string, unknown>>,
|
|
201
|
+
): Readonly<Record<string, unknown>> {
|
|
202
|
+
const data = browserResultData(value as Record<string, unknown>);
|
|
203
|
+
if (!isRecord(data) || typeof data.origin !== "string" || !isRecord(data.result)) {
|
|
204
|
+
throw new LinkedInFeedBrowserFailure(
|
|
205
|
+
"response-envelope",
|
|
206
|
+
"LinkedIn profile-activity browser returned a malformed evaluation envelope",
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
let origin: URL;
|
|
210
|
+
try {
|
|
211
|
+
origin = new URL(data.origin);
|
|
212
|
+
} catch {
|
|
213
|
+
throw new LinkedInFeedBrowserFailure(
|
|
214
|
+
"response-envelope",
|
|
215
|
+
"LinkedIn profile-activity browser returned a malformed evaluation envelope",
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
if (
|
|
219
|
+
origin.origin !== LINKEDIN_ORIGIN
|
|
220
|
+
|| origin.username !== ""
|
|
221
|
+
|| origin.password !== ""
|
|
222
|
+
) throw new LinkedInFeedBrowserFailure(
|
|
223
|
+
"response-envelope",
|
|
224
|
+
"LinkedIn profile-activity browser returned a malformed evaluation envelope",
|
|
225
|
+
);
|
|
226
|
+
return data.result;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function hasNoDefaultExecutionContext(error: unknown): boolean {
|
|
230
|
+
let current: unknown = error;
|
|
231
|
+
for (let depth = 0; depth < 8 && current !== undefined; depth += 1) {
|
|
232
|
+
if (
|
|
233
|
+
current instanceof Error
|
|
234
|
+
&& /(?:cannot find|no) default execution context/iu.test(current.message)
|
|
235
|
+
) return true;
|
|
236
|
+
current = current instanceof Error ? current.cause : undefined;
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function hasUnexpectedLinkedInOrigin(error: unknown): boolean {
|
|
242
|
+
let current: unknown = error;
|
|
243
|
+
for (let depth = 0; depth < 8 && current !== undefined; depth += 1) {
|
|
244
|
+
if (
|
|
245
|
+
current instanceof Error
|
|
246
|
+
&& /(?:^|: )unexpected LinkedIn origin(?:$|[\r\n])/u.test(current.message)
|
|
247
|
+
) return true;
|
|
248
|
+
current = current instanceof Error ? current.cause : undefined;
|
|
249
|
+
}
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function classifiedBrowserCommandFailure(error: unknown): LinkedInFeedBrowserFailure {
|
|
254
|
+
const message = error instanceof Error && error.message.length <= 1_500
|
|
255
|
+
? error.message
|
|
256
|
+
: "";
|
|
257
|
+
if (/(?:^|: )(?:missing|invalid) LinkedIn browser CSRF cookie$/u.test(message)) {
|
|
258
|
+
return new LinkedInFeedBrowserFailure(
|
|
259
|
+
"session-cookie",
|
|
260
|
+
"LinkedIn profile-activity browser could not establish its reviewed CSRF cookie",
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
if (message.includes("Failed to fetch")) {
|
|
264
|
+
return new LinkedInFeedBrowserFailure(
|
|
265
|
+
"provider-fetch",
|
|
266
|
+
"LinkedIn profile-activity browser could not complete its first-party fetch",
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
if (message.endsWith("LinkedIn profile-activity browser response escaped its exact route")) {
|
|
270
|
+
return new LinkedInFeedBrowserFailure(
|
|
271
|
+
"response-envelope",
|
|
272
|
+
"LinkedIn profile-activity browser response escaped its exact route",
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
if (message.endsWith("LinkedIn profile-activity browser left its bound document")) {
|
|
276
|
+
return new LinkedInFeedBrowserFailure(
|
|
277
|
+
"response-envelope",
|
|
278
|
+
"LinkedIn profile-activity browser left its exact target page",
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
if (message.endsWith("LinkedIn profile-activity browser reached its signed-out authwall")) {
|
|
282
|
+
return new LinkedInFeedBrowserFailure(
|
|
283
|
+
"authwall",
|
|
284
|
+
"LinkedIn profile-activity browser reached the signed-out authwall",
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
if (
|
|
288
|
+
message.includes("process output exceeded")
|
|
289
|
+
|| message.includes("response exceeded its reviewed byte bound")
|
|
290
|
+
) {
|
|
291
|
+
return new LinkedInFeedBrowserFailure(
|
|
292
|
+
"output-bound",
|
|
293
|
+
"LinkedIn profile-activity browser exceeded a reviewed output bound",
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
if (
|
|
297
|
+
message.includes("malformed batch")
|
|
298
|
+
|| message.includes("malformed batch entry")
|
|
299
|
+
|| message.includes("did not return JSON")
|
|
300
|
+
|| message.includes("command omitted its result")
|
|
301
|
+
) {
|
|
302
|
+
return new LinkedInFeedBrowserFailure(
|
|
303
|
+
"browser-envelope",
|
|
304
|
+
"LinkedIn profile-activity browser command returned a malformed envelope",
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
return new LinkedInFeedBrowserFailure(
|
|
308
|
+
"browser-command",
|
|
309
|
+
"LinkedIn profile-activity browser command failed before a reviewed response",
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function contextSettlementRejected(result: CommandResult): boolean {
|
|
314
|
+
return result.exitCode !== 0
|
|
315
|
+
&& /Failed to install browser network controls:[^\r\n]{0,256}Cannot find default execution context/u.test(
|
|
316
|
+
`${result.stderr}\n${result.stdout}`,
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function commandWasAborted(options: Parameters<typeof runCommand>[1]): boolean {
|
|
321
|
+
return options.signal?.aborted === true;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function linkedInFeedBrowserCommandRunner(
|
|
325
|
+
execute: typeof runCommand,
|
|
326
|
+
settleContext: () => Promise<void>,
|
|
327
|
+
authKind: GhostgetAuth["kind"],
|
|
328
|
+
): typeof runCommand {
|
|
329
|
+
let initialBatchPending = true;
|
|
330
|
+
return async (command, options) => {
|
|
331
|
+
const rewroteInitialRoot = initialBatchPending
|
|
332
|
+
&& options.stdin === LINKEDIN_INITIAL_ROOT_BATCH;
|
|
333
|
+
const rewroteInitialBlank = initialBatchPending
|
|
334
|
+
&& authKind === "browser-profile"
|
|
335
|
+
&& options.stdin === LINKEDIN_INITIAL_BLANK_BATCH;
|
|
336
|
+
const executionOptions = rewroteInitialRoot || rewroteInitialBlank
|
|
337
|
+
? { ...options, stdin: LINKEDIN_INITIAL_REALM_BATCH }
|
|
338
|
+
: options;
|
|
339
|
+
initialBatchPending = false;
|
|
340
|
+
const first = await execute(command, executionOptions);
|
|
341
|
+
if (
|
|
342
|
+
!rewroteInitialRoot
|
|
343
|
+
|| authKind === "browser-profile"
|
|
344
|
+
|| !contextSettlementRejected(first)
|
|
345
|
+
|| commandWasAborted(executionOptions)
|
|
346
|
+
) return first;
|
|
347
|
+
await settleContext();
|
|
348
|
+
if (commandWasAborted(executionOptions)) return first;
|
|
349
|
+
return execute(command, executionOptions);
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function settleLinkedInBrowserContext(): Promise<void> {
|
|
354
|
+
return new Promise((resolve) => {
|
|
355
|
+
setTimeout(resolve, 500);
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function finalizeBrowserSession(session: BrowserSession): Promise<void> {
|
|
360
|
+
const failures: unknown[] = [];
|
|
361
|
+
let closeVerified = false;
|
|
362
|
+
try {
|
|
363
|
+
await session.close();
|
|
364
|
+
closeVerified = true;
|
|
365
|
+
} catch (error) {
|
|
366
|
+
failures.push(error);
|
|
367
|
+
}
|
|
368
|
+
let cleanupVerified = false;
|
|
369
|
+
try {
|
|
370
|
+
await session.cleanup();
|
|
371
|
+
cleanupVerified = true;
|
|
372
|
+
} catch (error) {
|
|
373
|
+
failures.push(error);
|
|
374
|
+
}
|
|
375
|
+
if (closeVerified && cleanupVerified) return;
|
|
376
|
+
const cleanupEvidence = (
|
|
377
|
+
closeVerified
|
|
378
|
+
&& !cleanupVerified
|
|
379
|
+
&& session.cleanupResourceIdentity !== undefined
|
|
380
|
+
)
|
|
381
|
+
? Object.freeze({
|
|
382
|
+
kind: "agent-browser-closed-artifacts-v1" as const,
|
|
383
|
+
resource: session.cleanupResourceIdentity,
|
|
384
|
+
})
|
|
385
|
+
: undefined;
|
|
386
|
+
throw new PreservedBrowserArtifactsError(
|
|
387
|
+
"LinkedIn profile-activity browser finalization failed; private artifacts were preserved",
|
|
388
|
+
session.recoveryHandle ?? "session=linkedin-profile-activity-runtime;artifacts=unknown",
|
|
389
|
+
new AggregateError(failures, "LinkedIn profile-activity browser finalization failed"),
|
|
390
|
+
cleanupEvidence,
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function observedRequests(value: unknown): readonly ObservedBrowserRequest[] {
|
|
395
|
+
if (!isRecord(value) || !Array.isArray(value.requests) || value.requests.length > MAX_NETWORK_REQUESTS) {
|
|
396
|
+
throw new LinkedInFeedBrowserFailure(
|
|
397
|
+
"browser-envelope",
|
|
398
|
+
"LinkedIn profile-activity network observation returned a malformed bounded request list",
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
const requests: ObservedBrowserRequest[] = [];
|
|
402
|
+
for (const item of value.requests) {
|
|
403
|
+
if (
|
|
404
|
+
!isRecord(item)
|
|
405
|
+
|| typeof item.requestId !== "string"
|
|
406
|
+
|| item.requestId.length < 1
|
|
407
|
+
|| item.requestId.length > 512
|
|
408
|
+
|| /[\0\r\n]/u.test(item.requestId)
|
|
409
|
+
) {
|
|
410
|
+
throw new LinkedInFeedBrowserFailure(
|
|
411
|
+
"browser-envelope",
|
|
412
|
+
"LinkedIn profile-activity network observation returned a malformed request",
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
requests.push(Object.freeze({
|
|
416
|
+
requestId: item.requestId,
|
|
417
|
+
method: item.method,
|
|
418
|
+
status: item.status,
|
|
419
|
+
url: item.url,
|
|
420
|
+
}));
|
|
421
|
+
}
|
|
422
|
+
return requests;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function observedRequestIds(
|
|
426
|
+
requests: readonly ObservedBrowserRequest[],
|
|
427
|
+
): ReadonlySet<string> {
|
|
428
|
+
const ids = new Set<string>();
|
|
429
|
+
for (const request of requests) {
|
|
430
|
+
if (ids.has(request.requestId)) {
|
|
431
|
+
throw new LinkedInFeedBrowserFailure(
|
|
432
|
+
"browser-envelope",
|
|
433
|
+
"LinkedIn profile-activity network observation returned duplicate request identities",
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
ids.add(request.requestId);
|
|
437
|
+
}
|
|
438
|
+
return ids;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function currentProfileActivityUrl(
|
|
442
|
+
value: unknown,
|
|
443
|
+
expected: string,
|
|
444
|
+
): void {
|
|
445
|
+
if (!isRecord(value) || typeof value.url !== "string" || value.url.length > 2_048) {
|
|
446
|
+
throw new LinkedInFeedBrowserFailure(
|
|
447
|
+
"browser-envelope",
|
|
448
|
+
"LinkedIn profile-activity browser omitted its current URL",
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
let current: URL;
|
|
452
|
+
try {
|
|
453
|
+
current = new URL(value.url);
|
|
454
|
+
} catch {
|
|
455
|
+
throw new LinkedInFeedBrowserFailure(
|
|
456
|
+
"browser-envelope",
|
|
457
|
+
"LinkedIn profile-activity browser returned a malformed current URL",
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
if (
|
|
461
|
+
current.origin === LINKEDIN_ORIGIN
|
|
462
|
+
&& /^\/(?:authwall|checkpoint|login|uas\/login(?:-submit)?)(?:\/|$)/u.test(current.pathname)
|
|
463
|
+
) {
|
|
464
|
+
throw new LinkedInFeedBrowserFailure(
|
|
465
|
+
"authwall",
|
|
466
|
+
"LinkedIn profile-activity browser reached the signed-out authwall",
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
if (
|
|
470
|
+
current.username !== ""
|
|
471
|
+
|| current.password !== ""
|
|
472
|
+
|| current.hash !== ""
|
|
473
|
+
|| current.href !== expected
|
|
474
|
+
) throw new LinkedInFeedBrowserFailure(
|
|
475
|
+
"response-envelope",
|
|
476
|
+
"LinkedIn profile-activity browser left its exact target page",
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export async function createLinkedInFeedBrowserTransport(
|
|
481
|
+
auth: GhostgetAuth,
|
|
482
|
+
options: {
|
|
483
|
+
readonly timeoutMs: number;
|
|
484
|
+
readonly maxOutputBytes: number;
|
|
485
|
+
readonly operationDeadline?: WebSessionOperationDeadline;
|
|
486
|
+
readonly publishCleanupResource?: WebSessionCleanupResourcePublisher;
|
|
487
|
+
readonly dependencies?: Partial<LinkedInFeedBrowserDependencies>;
|
|
488
|
+
},
|
|
489
|
+
): Promise<LinkedInFeedBrowserTransport> {
|
|
490
|
+
if (
|
|
491
|
+
!Number.isSafeInteger(options.maxOutputBytes)
|
|
492
|
+
|| options.maxOutputBytes < 1
|
|
493
|
+
|| options.maxOutputBytes > MAX_FEED_PAGE_BYTES
|
|
494
|
+
) throw new Error("LinkedIn profile-activity browser output bound is invalid");
|
|
495
|
+
const createSession = options.dependencies?.createBrowserSession
|
|
496
|
+
?? createBrowserSession;
|
|
497
|
+
const browserOutputBytes = encodedBodyBound(options.maxOutputBytes)
|
|
498
|
+
+ BROWSER_ENVELOPE_BYTES;
|
|
499
|
+
const sessionOptions: CreateBrowserSessionOptions = {
|
|
500
|
+
allowCodeOwnedEvaluation: true,
|
|
501
|
+
allowCodeOwnedNetworkObservation: true,
|
|
502
|
+
headed: true,
|
|
503
|
+
maxOutputBytes: browserOutputBytes,
|
|
504
|
+
timeoutMs: options.timeoutMs,
|
|
505
|
+
dependencies: {
|
|
506
|
+
runCommand: linkedInFeedBrowserCommandRunner(
|
|
507
|
+
options.dependencies?.runCommand ?? runCommand,
|
|
508
|
+
options.dependencies?.settleContext ?? settleLinkedInBrowserContext,
|
|
509
|
+
auth.kind,
|
|
510
|
+
),
|
|
511
|
+
},
|
|
512
|
+
...(options.operationDeadline === undefined
|
|
513
|
+
? {}
|
|
514
|
+
: { operationDeadline: options.operationDeadline }),
|
|
515
|
+
...(options.publishCleanupResource === undefined
|
|
516
|
+
? {}
|
|
517
|
+
: { publishCleanupResource: options.publishCleanupResource }),
|
|
518
|
+
};
|
|
519
|
+
let session: BrowserSession;
|
|
520
|
+
try {
|
|
521
|
+
session = await createSession(feedBrowserManifest, auth, sessionOptions);
|
|
522
|
+
} catch (error) {
|
|
523
|
+
if (error instanceof PreservedBrowserArtifactsError) throw error;
|
|
524
|
+
options.operationDeadline?.throwIfUnavailable(
|
|
525
|
+
"LinkedIn profile-activity browser startup",
|
|
526
|
+
);
|
|
527
|
+
throw new LinkedInFeedBrowserFailure(
|
|
528
|
+
"startup",
|
|
529
|
+
"LinkedIn profile-activity browser could not start its contained session",
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
let closed = false;
|
|
533
|
+
let state: BrowserReadState = "ready";
|
|
534
|
+
let boundPage: BoundProfileActivityPage | null = null;
|
|
535
|
+
|
|
536
|
+
const remainingTimeMs = (): number =>
|
|
537
|
+
options.operationDeadline?.remainingTimeMs() ?? options.timeoutMs;
|
|
538
|
+
|
|
539
|
+
const observeProfileActivityRequests = async (): Promise<readonly ObservedBrowserRequest[]> => {
|
|
540
|
+
const entries = await session.runBatch(
|
|
541
|
+
[["network", "requests", "--filter", LINKEDIN_PROFILE_ACTIVITY_QUERY_PREFIX]],
|
|
542
|
+
Math.min(remainingTimeMs(), 30_000),
|
|
543
|
+
8 * 1024 * 1024,
|
|
544
|
+
);
|
|
545
|
+
const first = entries[0];
|
|
546
|
+
if (first === undefined) {
|
|
547
|
+
throw new LinkedInFeedBrowserFailure(
|
|
548
|
+
"browser-envelope",
|
|
549
|
+
"LinkedIn profile-activity browser omitted its network observation",
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
return observedRequests(browserResultData(first));
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
const run = async (
|
|
556
|
+
binding: BrowserReadBinding,
|
|
557
|
+
): Promise<Readonly<Record<string, unknown>>> => {
|
|
558
|
+
if (closed) throw new Error("LinkedIn profile-activity browser transport is closed");
|
|
559
|
+
const source = browserReadEvaluationSource(binding);
|
|
560
|
+
let records: readonly Readonly<Record<string, unknown>>[];
|
|
561
|
+
try {
|
|
562
|
+
records = await session.runBatch(
|
|
563
|
+
[["eval", source]],
|
|
564
|
+
remainingTimeMs(),
|
|
565
|
+
Math.min(
|
|
566
|
+
encodedBodyBound(binding.maxBytes) + BROWSER_ENVELOPE_BYTES,
|
|
567
|
+
browserOutputBytes,
|
|
568
|
+
),
|
|
569
|
+
);
|
|
570
|
+
} catch (error) {
|
|
571
|
+
if (error instanceof PreservedBrowserArtifactsError) throw error;
|
|
572
|
+
if (error instanceof LinkedInFeedBrowserFailure) throw error;
|
|
573
|
+
options.operationDeadline?.throwIfUnavailable(
|
|
574
|
+
"LinkedIn profile-activity browser operation",
|
|
575
|
+
);
|
|
576
|
+
if (hasNoDefaultExecutionContext(error)) {
|
|
577
|
+
throw new LinkedInFeedBrowserFailure(
|
|
578
|
+
"execution-context",
|
|
579
|
+
"LinkedIn profile-activity browser lost its reviewed execution context",
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
if (hasUnexpectedLinkedInOrigin(error)) {
|
|
583
|
+
throw new LinkedInFeedBrowserFailure(
|
|
584
|
+
"bootstrap",
|
|
585
|
+
"LinkedIn profile-activity browser was not on its reviewed signed-in origin",
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
throw classifiedBrowserCommandFailure(error);
|
|
589
|
+
}
|
|
590
|
+
const first = records[0];
|
|
591
|
+
if (first === undefined) {
|
|
592
|
+
throw new LinkedInFeedBrowserFailure(
|
|
593
|
+
"response-envelope",
|
|
594
|
+
"LinkedIn profile-activity browser omitted its response",
|
|
595
|
+
);
|
|
596
|
+
}
|
|
597
|
+
const result = browserEvaluationResult(first);
|
|
598
|
+
try {
|
|
599
|
+
exactKeys(
|
|
600
|
+
result,
|
|
601
|
+
[
|
|
602
|
+
"authWall",
|
|
603
|
+
"bodyBase64",
|
|
604
|
+
"bodyBytes",
|
|
605
|
+
"bodySha256",
|
|
606
|
+
"contentType",
|
|
607
|
+
"status",
|
|
608
|
+
],
|
|
609
|
+
"LinkedIn profile-activity browser request",
|
|
610
|
+
);
|
|
611
|
+
} catch {
|
|
612
|
+
throw new LinkedInFeedBrowserFailure(
|
|
613
|
+
"response-envelope",
|
|
614
|
+
"LinkedIn profile-activity browser request returned an unexpected result shape",
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
if (
|
|
618
|
+
typeof result.authWall !== "boolean"
|
|
619
|
+
|| typeof result.status !== "number"
|
|
620
|
+
|| !Number.isSafeInteger(result.status)
|
|
621
|
+
|| typeof result.contentType !== "string"
|
|
622
|
+
|| result.contentType.length > 128
|
|
623
|
+
|| (result.contentType !== "" && !LINKEDIN_RESPONSE_MEDIA_TYPE.test(result.contentType))
|
|
624
|
+
) throw new LinkedInFeedBrowserFailure(
|
|
625
|
+
"response-envelope",
|
|
626
|
+
"LinkedIn profile-activity browser returned a malformed response category",
|
|
627
|
+
);
|
|
628
|
+
if (result.authWall) {
|
|
629
|
+
if (
|
|
630
|
+
!(
|
|
631
|
+
result.status === 0
|
|
632
|
+
|| (result.status >= 300 && result.status <= 399)
|
|
633
|
+
)
|
|
634
|
+
|| result.contentType !== ""
|
|
635
|
+
|| result.bodyBase64 !== null
|
|
636
|
+
|| result.bodyBytes !== 0
|
|
637
|
+
|| result.bodySha256 !== null
|
|
638
|
+
) throw new LinkedInFeedBrowserFailure(
|
|
639
|
+
"response-envelope",
|
|
640
|
+
"LinkedIn profile-activity browser returned a malformed authwall envelope",
|
|
641
|
+
);
|
|
642
|
+
throw new LinkedInFeedBrowserFailure(
|
|
643
|
+
"authwall",
|
|
644
|
+
"LinkedIn profile-activity browser reached the signed-out authwall",
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
if (result.status < 100 || result.status > 599) {
|
|
648
|
+
throw new LinkedInFeedBrowserFailure(
|
|
649
|
+
"response-envelope",
|
|
650
|
+
"LinkedIn profile-activity browser returned a malformed response category",
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
if (result.status >= 300 && result.status <= 399) {
|
|
654
|
+
throw new LinkedInFeedBrowserFailure(
|
|
655
|
+
"response-envelope",
|
|
656
|
+
"LinkedIn profile-activity browser returned a malformed redirect envelope",
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
if (result.status === 401 || result.status === 403) {
|
|
660
|
+
if (
|
|
661
|
+
result.bodyBase64 !== null
|
|
662
|
+
|| result.bodyBytes !== 0
|
|
663
|
+
|| result.bodySha256 !== null
|
|
664
|
+
) throw new LinkedInFeedBrowserFailure(
|
|
665
|
+
"response-envelope",
|
|
666
|
+
"LinkedIn profile-activity browser returned a malformed rejection envelope",
|
|
667
|
+
);
|
|
668
|
+
throw new LinkedInFeedBrowserFailure(
|
|
669
|
+
"authwall",
|
|
670
|
+
"LinkedIn profile-activity browser reached the signed-out authwall",
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
if (
|
|
674
|
+
result.status !== 200
|
|
675
|
+
|| (
|
|
676
|
+
result.contentType !== "application/vnd.linkedin.normalized+json+2.1"
|
|
677
|
+
&& result.contentType !== "application/json"
|
|
678
|
+
)
|
|
679
|
+
) {
|
|
680
|
+
if (
|
|
681
|
+
result.bodyBase64 !== null
|
|
682
|
+
|| result.bodyBytes !== 0
|
|
683
|
+
|| result.bodySha256 !== null
|
|
684
|
+
) throw new LinkedInFeedBrowserFailure(
|
|
685
|
+
"response-envelope",
|
|
686
|
+
"LinkedIn profile-activity browser returned a malformed rejection envelope",
|
|
687
|
+
);
|
|
688
|
+
throw new LinkedInFeedBrowserResponseRejectedError(
|
|
689
|
+
result.status,
|
|
690
|
+
result.contentType,
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
return result;
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
const parseJsonBody = (
|
|
697
|
+
result: Readonly<Record<string, unknown>>,
|
|
698
|
+
maximumBytes: number,
|
|
699
|
+
label: string,
|
|
700
|
+
): unknown => {
|
|
701
|
+
const text = decodedBody(result, maximumBytes);
|
|
702
|
+
try {
|
|
703
|
+
return JSON.parse(text) as unknown;
|
|
704
|
+
} catch {
|
|
705
|
+
throw new LinkedInFeedBrowserFailure(
|
|
706
|
+
"identity-json",
|
|
707
|
+
`${label} was not valid JSON`,
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
return Object.freeze({
|
|
713
|
+
currentIdentityResponse: async () => {
|
|
714
|
+
if (state !== "ready") {
|
|
715
|
+
throw new Error("LinkedIn profile-activity browser identity read is out of order");
|
|
716
|
+
}
|
|
717
|
+
const result = await run({
|
|
718
|
+
documentUrl: null,
|
|
719
|
+
kind: "json",
|
|
720
|
+
maxBytes: Math.min(MAX_IDENTITY_BYTES, options.maxOutputBytes),
|
|
721
|
+
path: "/voyager/api/me",
|
|
722
|
+
referrer: LINKEDIN_FEED_URL,
|
|
723
|
+
});
|
|
724
|
+
state = "identity";
|
|
725
|
+
return parseJsonBody(
|
|
726
|
+
result,
|
|
727
|
+
Math.min(MAX_IDENTITY_BYTES, options.maxOutputBytes),
|
|
728
|
+
"LinkedIn profile-activity browser identity response",
|
|
729
|
+
);
|
|
730
|
+
},
|
|
731
|
+
resolveProfileActivityBinding: async (vanity: string) => {
|
|
732
|
+
if (state !== "identity") {
|
|
733
|
+
throw new Error("LinkedIn profile-activity browser query observation is out of order");
|
|
734
|
+
}
|
|
735
|
+
const target = linkedInProfileActivityTargetFromVanity(vanity);
|
|
736
|
+
try {
|
|
737
|
+
const baselineRequestIds = observedRequestIds(
|
|
738
|
+
await observeProfileActivityRequests(),
|
|
739
|
+
);
|
|
740
|
+
await session.runBatch(
|
|
741
|
+
[["open", target.activityUrl]],
|
|
742
|
+
remainingTimeMs(),
|
|
743
|
+
2 * 1024 * 1024,
|
|
744
|
+
);
|
|
745
|
+
await session.runBatch(
|
|
746
|
+
[["wait", "8000"]],
|
|
747
|
+
Math.min(remainingTimeMs(), 20_000),
|
|
748
|
+
1024 * 1024,
|
|
749
|
+
);
|
|
750
|
+
const requests = await observeProfileActivityRequests();
|
|
751
|
+
observedRequestIds(requests);
|
|
752
|
+
const currentUrlEntries = await session.runBatch(
|
|
753
|
+
[["get", "url"]],
|
|
754
|
+
Math.min(remainingTimeMs(), 10_000),
|
|
755
|
+
1024 * 1024,
|
|
756
|
+
);
|
|
757
|
+
const currentUrl = currentUrlEntries[0];
|
|
758
|
+
if (currentUrl === undefined) {
|
|
759
|
+
throw new LinkedInFeedBrowserFailure(
|
|
760
|
+
"browser-envelope",
|
|
761
|
+
"LinkedIn profile-activity browser omitted its current URL",
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
currentProfileActivityUrl(browserResultData(currentUrl), target.activityUrl);
|
|
765
|
+
const bounded = requests.flatMap((request) => {
|
|
766
|
+
if (baselineRequestIds.has(request.requestId)) return [];
|
|
767
|
+
if (typeof request.url !== "string" || request.url.length > MAX_REQUEST_URL_CHARACTERS) {
|
|
768
|
+
return [];
|
|
769
|
+
}
|
|
770
|
+
return [{
|
|
771
|
+
method: request.method,
|
|
772
|
+
status: request.status,
|
|
773
|
+
url: request.url,
|
|
774
|
+
}];
|
|
775
|
+
});
|
|
776
|
+
const binding = resolveLinkedInProfileActivityBinding(bounded);
|
|
777
|
+
boundPage = Object.freeze({
|
|
778
|
+
binding,
|
|
779
|
+
targetUrl: target.activityUrl,
|
|
780
|
+
vanity: target.slug,
|
|
781
|
+
});
|
|
782
|
+
state = "bound";
|
|
783
|
+
return binding;
|
|
784
|
+
} catch (error) {
|
|
785
|
+
if (error instanceof PreservedBrowserArtifactsError) throw error;
|
|
786
|
+
if (error instanceof LinkedInFeedBrowserFailure) throw error;
|
|
787
|
+
options.operationDeadline?.throwIfUnavailable(
|
|
788
|
+
"LinkedIn profile-activity query observation",
|
|
789
|
+
);
|
|
790
|
+
throw new LinkedInFeedBrowserFailure(
|
|
791
|
+
"browser-command",
|
|
792
|
+
"LinkedIn current registered query was not observed",
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
},
|
|
796
|
+
readProfileActivityPage: async (input: {
|
|
797
|
+
readonly count: number;
|
|
798
|
+
readonly start: number;
|
|
799
|
+
}) => {
|
|
800
|
+
if (state !== "bound" || boundPage === null) {
|
|
801
|
+
throw new Error("LinkedIn profile-activity browser page read is out of order");
|
|
802
|
+
}
|
|
803
|
+
const request = {
|
|
804
|
+
queryId: boundPage.binding.queryId,
|
|
805
|
+
profileUrn: boundPage.binding.profileUrn,
|
|
806
|
+
count: input.count,
|
|
807
|
+
start: input.start,
|
|
808
|
+
} as const;
|
|
809
|
+
const url = linkedInProfileActivityPageUrl(request);
|
|
810
|
+
assertLinkedInProfileActivityRequest({ method: "GET", url }, request);
|
|
811
|
+
const path = `${url.pathname}${url.search}`;
|
|
812
|
+
const result = await run({
|
|
813
|
+
documentUrl: boundPage.targetUrl,
|
|
814
|
+
kind: "json",
|
|
815
|
+
maxBytes: options.maxOutputBytes,
|
|
816
|
+
path,
|
|
817
|
+
referrer: boundPage.targetUrl,
|
|
818
|
+
});
|
|
819
|
+
state = "bound";
|
|
820
|
+
return parseJsonBody(
|
|
821
|
+
result,
|
|
822
|
+
options.maxOutputBytes,
|
|
823
|
+
"LinkedIn profile-activity browser page response",
|
|
824
|
+
);
|
|
825
|
+
},
|
|
826
|
+
close: async () => {
|
|
827
|
+
if (closed) return;
|
|
828
|
+
closed = true;
|
|
829
|
+
await finalizeBrowserSession(session);
|
|
830
|
+
},
|
|
831
|
+
});
|
|
832
|
+
}
|