@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,653 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import type { GhostgetAuth } from "../auth";
|
|
4
|
+
import {
|
|
5
|
+
PreservedBrowserArtifactsError,
|
|
6
|
+
browserResultData,
|
|
7
|
+
createBrowserSession,
|
|
8
|
+
type BrowserSession,
|
|
9
|
+
type CreateBrowserSessionOptions,
|
|
10
|
+
} from "../browser";
|
|
11
|
+
import { canonicalJsonScriptLiteral } from "../canonical-json";
|
|
12
|
+
import type { GhostgetManifest } from "../model";
|
|
13
|
+
import type {
|
|
14
|
+
WebSessionCleanupResourcePublisher,
|
|
15
|
+
WebSessionOperationDeadline,
|
|
16
|
+
} from "../web-session-execution";
|
|
17
|
+
import {
|
|
18
|
+
LINKEDIN_GRAPHQL_PATH,
|
|
19
|
+
LINKEDIN_POST_CREATE_MUTATION_ID,
|
|
20
|
+
LINKEDIN_POST_READBACK_QUERY_ID,
|
|
21
|
+
linkedInPostEntityUrn,
|
|
22
|
+
linkedInPostMediaUrn,
|
|
23
|
+
} from "./linkedin-web";
|
|
24
|
+
|
|
25
|
+
const LINKEDIN_ORIGIN = "https://www.linkedin.com";
|
|
26
|
+
const LINKEDIN_FEED_URL = `${LINKEDIN_ORIGIN}/feed/`;
|
|
27
|
+
const LINKEDIN_IMAGE_REGISTRATION_PATH =
|
|
28
|
+
"/voyager/api/voyagerVideoDashMediaUploadMetadata?action=upload";
|
|
29
|
+
const LINKEDIN_IMAGE_FINALIZATION_PATH =
|
|
30
|
+
"/voyager/api/voyagerVideoDashMediaUploadMetadata?action=completeMultipartUpload";
|
|
31
|
+
const MAX_BROWSER_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
32
|
+
const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
33
|
+
const IMAGE_STAGING_CHUNK_CHARACTERS = 48 * 1024;
|
|
34
|
+
const IMAGE_STAGING_COMMANDS_PER_BATCH = 32;
|
|
35
|
+
const MAX_IMAGE_STAGING_COMMAND_CHARACTERS = 64 * 1024;
|
|
36
|
+
const MAX_IMAGE_BASE64_CHARACTERS = Math.ceil(MAX_IMAGE_BYTES / 3) * 4;
|
|
37
|
+
const MAX_IMAGE_STAGING_CHUNKS = Math.ceil(
|
|
38
|
+
MAX_IMAGE_BASE64_CHARACTERS / IMAGE_STAGING_CHUNK_CHARACTERS,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const postBrowserManifest: GhostgetManifest = Object.freeze({
|
|
42
|
+
schemaVersion: 4,
|
|
43
|
+
id: "linkedin-post-runtime",
|
|
44
|
+
version: "1.0.0",
|
|
45
|
+
displayName: "LinkedIn native post 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 LinkedInPostBrowserTransport = {
|
|
53
|
+
readonly currentIdentityResponse: () => Promise<unknown>;
|
|
54
|
+
readonly uploadImage: (
|
|
55
|
+
expectedSubject: string,
|
|
56
|
+
image: Uint8Array,
|
|
57
|
+
) => Promise<string>;
|
|
58
|
+
readonly createPost: (
|
|
59
|
+
expectedSubject: string,
|
|
60
|
+
expectedProfileUrn: string,
|
|
61
|
+
variables: Readonly<Record<string, unknown>>,
|
|
62
|
+
mediaUrn: string | null,
|
|
63
|
+
) => Promise<string>;
|
|
64
|
+
readonly readPost: (
|
|
65
|
+
expectedSubject: string,
|
|
66
|
+
expectedProfileUrn: string,
|
|
67
|
+
variables: Readonly<Record<string, unknown>>,
|
|
68
|
+
mediaUrn: string | null,
|
|
69
|
+
entityUrn: string,
|
|
70
|
+
) => Promise<unknown>;
|
|
71
|
+
readonly close: () => Promise<void>;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type LinkedInPostBrowserDependencies = {
|
|
75
|
+
readonly createBrowserSession: typeof createBrowserSession;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type LinkedInPostImageFailureStage =
|
|
79
|
+
| "page image staging"
|
|
80
|
+
| "image registration response"
|
|
81
|
+
| "image transfer response"
|
|
82
|
+
| "image finalization response"
|
|
83
|
+
| "image registration or upload response";
|
|
84
|
+
|
|
85
|
+
function linkedInPostImageFailureStage(error: unknown): LinkedInPostImageFailureStage {
|
|
86
|
+
const message = error instanceof Error ? error.message : "";
|
|
87
|
+
if (message.includes("LinkedIn image registration")) return "image registration response";
|
|
88
|
+
if (message.includes("LinkedIn image finalization")) return "image finalization response";
|
|
89
|
+
if (message.includes("LinkedIn image upload")) return "image transfer response";
|
|
90
|
+
return "image registration or upload response";
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Secret-free stage evidence for the preparatory image path. The underlying
|
|
95
|
+
* contained-browser failure remains attached as a cause and is never copied
|
|
96
|
+
* into a public run receipt.
|
|
97
|
+
*/
|
|
98
|
+
export class LinkedInPostImagePreparationError extends Error {
|
|
99
|
+
readonly stage: LinkedInPostImageFailureStage;
|
|
100
|
+
|
|
101
|
+
constructor(stage: LinkedInPostImageFailureStage, cause: unknown) {
|
|
102
|
+
super(`LinkedIn post image preparation failed during ${stage}`, { cause });
|
|
103
|
+
this.name = "LinkedInPostImagePreparationError";
|
|
104
|
+
this.stage = stage;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type LinkedInPostCreateFailureStage =
|
|
109
|
+
| "post create current-member binding"
|
|
110
|
+
| "post create status"
|
|
111
|
+
| "post create content type"
|
|
112
|
+
| "post create response shape"
|
|
113
|
+
| "post create provider errors"
|
|
114
|
+
| "post create entity direct nesting"
|
|
115
|
+
| "post create entity alternate data nesting"
|
|
116
|
+
| "post create normalized included entity"
|
|
117
|
+
| "post create entity absent"
|
|
118
|
+
| "post create entity URN"
|
|
119
|
+
| "post create published lifecycle"
|
|
120
|
+
| "post create response";
|
|
121
|
+
|
|
122
|
+
function linkedInPostCreateFailureStage(error: unknown): LinkedInPostCreateFailureStage {
|
|
123
|
+
const message = error instanceof Error ? error.message : "";
|
|
124
|
+
if (message.includes("LinkedIn current member")) return "post create current-member binding";
|
|
125
|
+
if (message.includes("LinkedIn post create status")) return "post create status";
|
|
126
|
+
if (message.includes("LinkedIn post create content type")) return "post create content type";
|
|
127
|
+
if (message.includes("LinkedIn GraphQL response changed shape")) return "post create response shape";
|
|
128
|
+
if (message.includes("LinkedIn post create returned provider errors")) return "post create provider errors";
|
|
129
|
+
if (message.includes("LinkedIn post create entity used direct nesting")) {
|
|
130
|
+
return "post create entity direct nesting";
|
|
131
|
+
}
|
|
132
|
+
if (message.includes("LinkedIn post create entity used alternate data nesting")) {
|
|
133
|
+
return "post create entity alternate data nesting";
|
|
134
|
+
}
|
|
135
|
+
if (message.includes("LinkedIn post create returned normalized included entities")) {
|
|
136
|
+
return "post create normalized included entity";
|
|
137
|
+
}
|
|
138
|
+
if (message.includes("LinkedIn post create omitted its entity")) {
|
|
139
|
+
return "post create entity absent";
|
|
140
|
+
}
|
|
141
|
+
if (message.includes("LinkedIn post create returned an invalid entity URN")) {
|
|
142
|
+
return "post create entity URN";
|
|
143
|
+
}
|
|
144
|
+
if (message.includes("LinkedIn post create did not report a published lifecycle")) {
|
|
145
|
+
return "post create published lifecycle";
|
|
146
|
+
}
|
|
147
|
+
return "post create response";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export class LinkedInPostCreateResponseError extends Error {
|
|
151
|
+
readonly stage: LinkedInPostCreateFailureStage;
|
|
152
|
+
|
|
153
|
+
constructor(cause: unknown) {
|
|
154
|
+
super("LinkedIn post create response failed strict binding", { cause });
|
|
155
|
+
this.name = "LinkedInPostCreateResponseError";
|
|
156
|
+
this.stage = linkedInPostCreateFailureStage(cause);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
type LinkedInPostPageBindings = {
|
|
161
|
+
readonly pageInstance: string;
|
|
162
|
+
readonly track: string;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const LINKEDIN_PAGE_INSTANCE_PATTERN =
|
|
166
|
+
/^urn:li:page:d_flagship3_[A-Za-z0-9_:-]{1,128};[A-Za-z0-9+/=_-]{1,512}$/u;
|
|
167
|
+
|
|
168
|
+
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
169
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function exactKeys(
|
|
173
|
+
value: Readonly<Record<string, unknown>>,
|
|
174
|
+
expected: readonly string[],
|
|
175
|
+
label: string,
|
|
176
|
+
): void {
|
|
177
|
+
if (Object.keys(value).sort().join(",") !== [...expected].sort().join(",")) {
|
|
178
|
+
throw new Error(`${label} returned an unexpected result shape`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function boundedHeader(value: unknown, label: string, maximum: number): string {
|
|
183
|
+
if (
|
|
184
|
+
typeof value !== "string"
|
|
185
|
+
|| value.length < 1
|
|
186
|
+
|| value.length > maximum
|
|
187
|
+
|| /[\0\r\n]/u.test(value)
|
|
188
|
+
) throw new Error(`${label} changed its reviewed bound`);
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function linkedInPostPageBindings(value: unknown): LinkedInPostPageBindings {
|
|
193
|
+
if (!isRecord(value) || !Array.isArray(value.requests) || value.requests.length > 10_000) {
|
|
194
|
+
throw new Error("LinkedIn post network observation changed shape");
|
|
195
|
+
}
|
|
196
|
+
let selected: LinkedInPostPageBindings | null = null;
|
|
197
|
+
for (const item of value.requests) {
|
|
198
|
+
if (!isRecord(item) || !isRecord(item.headers)) continue;
|
|
199
|
+
if (
|
|
200
|
+
item.method !== "GET"
|
|
201
|
+
|| item.status !== 200
|
|
202
|
+
|| typeof item.url !== "string"
|
|
203
|
+
|| item.url.length > 64 * 1024
|
|
204
|
+
) continue;
|
|
205
|
+
let url: URL;
|
|
206
|
+
try {
|
|
207
|
+
url = new URL(item.url);
|
|
208
|
+
} catch {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (
|
|
212
|
+
url.origin !== LINKEDIN_ORIGIN
|
|
213
|
+
|| url.username !== ""
|
|
214
|
+
|| url.password !== ""
|
|
215
|
+
|| !url.pathname.startsWith("/voyager/api/")
|
|
216
|
+
) continue;
|
|
217
|
+
const pageInstance = item.headers["x-li-page-instance"];
|
|
218
|
+
if (
|
|
219
|
+
typeof pageInstance !== "string"
|
|
220
|
+
|| !LINKEDIN_PAGE_INSTANCE_PATTERN.test(pageInstance)
|
|
221
|
+
) continue;
|
|
222
|
+
const track = boundedHeader(
|
|
223
|
+
item.headers["x-li-track"],
|
|
224
|
+
"LinkedIn post x-li-track binding",
|
|
225
|
+
4_096,
|
|
226
|
+
);
|
|
227
|
+
let parsedTrack: unknown;
|
|
228
|
+
try {
|
|
229
|
+
parsedTrack = JSON.parse(track) as unknown;
|
|
230
|
+
} catch {
|
|
231
|
+
throw new Error("LinkedIn post x-li-track binding changed shape");
|
|
232
|
+
}
|
|
233
|
+
if (!isRecord(parsedTrack) || parsedTrack.mpName !== "voyager-web") {
|
|
234
|
+
throw new Error("LinkedIn post x-li-track binding changed shape");
|
|
235
|
+
}
|
|
236
|
+
// The feed can rotate both tracking fields and its page-instance while it
|
|
237
|
+
// settles. Network observations are ordered, so retain only the newest
|
|
238
|
+
// fully validated same-origin request from this freshly opened page.
|
|
239
|
+
selected = Object.freeze({ pageInstance, track });
|
|
240
|
+
}
|
|
241
|
+
if (selected === null) {
|
|
242
|
+
throw new Error("LinkedIn post page omitted its bounded page-instance binding");
|
|
243
|
+
}
|
|
244
|
+
return selected;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function browserEvaluationResult(
|
|
248
|
+
record: Readonly<Record<string, unknown>>,
|
|
249
|
+
): Readonly<Record<string, unknown>> {
|
|
250
|
+
const data = browserResultData(record as Record<string, unknown>);
|
|
251
|
+
if (!isRecord(data) || typeof data.origin !== "string" || !isRecord(data.result)) {
|
|
252
|
+
throw new Error("LinkedIn post browser returned a malformed evaluation envelope");
|
|
253
|
+
}
|
|
254
|
+
let origin: URL;
|
|
255
|
+
try {
|
|
256
|
+
origin = new URL(data.origin);
|
|
257
|
+
} catch {
|
|
258
|
+
throw new Error("LinkedIn post browser returned a malformed evaluation envelope");
|
|
259
|
+
}
|
|
260
|
+
if (origin.origin !== LINKEDIN_ORIGIN || origin.username !== "" || origin.password !== "") {
|
|
261
|
+
throw new Error("LinkedIn post browser returned a malformed evaluation envelope");
|
|
262
|
+
}
|
|
263
|
+
return data.result;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function commonEvaluationPrelude(input: Readonly<Record<string, unknown>>): string {
|
|
267
|
+
return `const input=${canonicalJsonScriptLiteral(input)};if(location.origin!=="${LINKEDIN_ORIGIN}")throw new Error("unexpected LinkedIn origin");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");const baseHeaders={accept:"application/vnd.linkedin.normalized+json+2.1","csrf-token":csrf,"x-li-lang":"en_US","x-requested-with":"XMLHttpRequest","x-restli-protocol-version":"2.0.0"};const jsonTypes=new Set(["application/graphql","application/json","application/vnd.linkedin.normalized+json+2.1"]);const jsonResponse=async(response,label)=>{const contentType=(response.headers.get("content-type")||"").split(";",1)[0].trim().toLowerCase();if(!jsonTypes.has(contentType))throw new Error(label+" content type changed");if(response.status<200||response.status>=300)throw new Error(label+" status changed");return response.json()};const requestJson=async(path,init,label)=>jsonResponse(await fetch(path,{credentials:"include",redirect:"error",referrer:"${LINKEDIN_FEED_URL}",...init}),label);const identity=async()=>requestJson("/voyager/api/me",{headers:baseHeaders,method:"GET"},"LinkedIn current member");const assertIdentity=(body)=>{if(!body||typeof body!=="object"||Array.isArray(body)||!body.data||typeof body.data!=="object"||Array.isArray(body.data))throw new Error("LinkedIn current member changed shape");const plain=typeof body.data.plainId==="string"?body.data.plainId:Number.isSafeInteger(body.data.plainId)?String(body.data.plainId):"";if("urn:li:fsd_profile:"+plain!==input.expectedSubject)throw new Error("LinkedIn current member changed before dispatch");if(input.expectedProfileUrn!==undefined){const mini=body.data["*miniProfile"]??body.data.miniProfile;const suffix=typeof mini==="string"?/^urn:li:fs_miniProfile:([A-Za-z0-9_-]{1,256})$/.exec(mini)?.[1]:undefined;if("urn:li:fsd_profile:"+suffix!==input.expectedProfileUrn)throw new Error("LinkedIn current profile changed before dispatch")}};`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function identityEvaluationSource(): string {
|
|
271
|
+
const input = Object.freeze({});
|
|
272
|
+
return `(async()=>{${commonEvaluationPrelude(input)}const body=await identity();return{body,contentType:"application/vnd.linkedin.normalized+json+2.1",status:200}})()`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
type LinkedInPostImageStaging = {
|
|
276
|
+
readonly key: string;
|
|
277
|
+
readonly byteLength: number;
|
|
278
|
+
readonly base64Length: number;
|
|
279
|
+
readonly chunkCount: number;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
function imageStagingInitializationSource(staging: LinkedInPostImageStaging): string {
|
|
283
|
+
const input = Object.freeze({
|
|
284
|
+
stagingKey: staging.key,
|
|
285
|
+
expectedChunkCount: staging.chunkCount,
|
|
286
|
+
});
|
|
287
|
+
return `(async()=>{const input=${canonicalJsonScriptLiteral(input)};if(location.origin!=="${LINKEDIN_ORIGIN}")throw new Error("unexpected LinkedIn origin");if(!/^__ghostgetLinkedInPostImage_[a-f0-9]{32}$/.test(input.stagingKey)||!Number.isSafeInteger(input.expectedChunkCount)||input.expectedChunkCount<1||input.expectedChunkCount>${MAX_IMAGE_STAGING_CHUNKS})throw new Error("LinkedIn image staging input changed shape");if(Object.hasOwn(globalThis,input.stagingKey))throw new Error("LinkedIn image staging key collision");Object.defineProperty(globalThis,input.stagingKey,{configurable:true,enumerable:false,value:[],writable:false});return{ready:true}})()`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function imageStagingChunkSource(
|
|
291
|
+
staging: LinkedInPostImageStaging,
|
|
292
|
+
index: number,
|
|
293
|
+
chunk: string,
|
|
294
|
+
): string {
|
|
295
|
+
const input = Object.freeze({
|
|
296
|
+
chunk,
|
|
297
|
+
expectedChunkCount: staging.chunkCount,
|
|
298
|
+
index,
|
|
299
|
+
stagingKey: staging.key,
|
|
300
|
+
});
|
|
301
|
+
const source = `(async()=>{const input=${canonicalJsonScriptLiteral(input)};if(location.origin!=="${LINKEDIN_ORIGIN}")throw new Error("unexpected LinkedIn origin");const chunks=globalThis[input.stagingKey];if(!Array.isArray(chunks)||!Number.isSafeInteger(input.index)||input.index<0||input.index>=input.expectedChunkCount||input.expectedChunkCount<1||input.expectedChunkCount>${MAX_IMAGE_STAGING_CHUNKS}||chunks.length!==input.index)throw new Error("LinkedIn image staging order changed");if(typeof input.chunk!=="string"||input.chunk.length<1||input.chunk.length>${IMAGE_STAGING_CHUNK_CHARACTERS}||!/^[A-Za-z0-9+/]*={0,2}$/.test(input.chunk))throw new Error("LinkedIn image staging chunk changed shape");chunks.push(input.chunk);return{staged:chunks.length}})()`;
|
|
302
|
+
if (source.length > MAX_IMAGE_STAGING_COMMAND_CHARACTERS) {
|
|
303
|
+
throw new Error("LinkedIn image staging command exceeded its reviewed bound");
|
|
304
|
+
}
|
|
305
|
+
return source;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function imageStagingCleanupSource(stagingKey: string): string {
|
|
309
|
+
const input = Object.freeze({ stagingKey });
|
|
310
|
+
return `(async()=>{const input=${canonicalJsonScriptLiteral(input)};if(location.origin!=="${LINKEDIN_ORIGIN}")throw new Error("unexpected LinkedIn origin");const removed=Object.hasOwn(globalThis,input.stagingKey);delete globalThis[input.stagingKey];return{removed}})()`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function baseUploadEvaluationSource(
|
|
314
|
+
bindings: LinkedInPostPageBindings,
|
|
315
|
+
expectedSubject: string,
|
|
316
|
+
staging: LinkedInPostImageStaging,
|
|
317
|
+
): string {
|
|
318
|
+
if (
|
|
319
|
+
staging.byteLength < 24
|
|
320
|
+
|| staging.byteLength > MAX_IMAGE_BYTES
|
|
321
|
+
|| staging.base64Length < 32
|
|
322
|
+
|| staging.base64Length > MAX_IMAGE_BASE64_CHARACTERS
|
|
323
|
+
|| staging.chunkCount < 1
|
|
324
|
+
|| staging.chunkCount > MAX_IMAGE_STAGING_CHUNKS
|
|
325
|
+
) {
|
|
326
|
+
throw new Error("LinkedIn post image is outside the reviewed byte bound");
|
|
327
|
+
}
|
|
328
|
+
const input = Object.freeze({
|
|
329
|
+
expectedSubject,
|
|
330
|
+
expectedBase64Length: staging.base64Length,
|
|
331
|
+
expectedByteLength: staging.byteLength,
|
|
332
|
+
expectedChunkCount: staging.chunkCount,
|
|
333
|
+
pageInstance: bindings.pageInstance,
|
|
334
|
+
stagingKey: staging.key,
|
|
335
|
+
track: bindings.track,
|
|
336
|
+
});
|
|
337
|
+
return `(async()=>{${commonEvaluationPrelude(input)}const chunks=globalThis[input.stagingKey];if(!Array.isArray(chunks)||chunks.length!==input.expectedChunkCount||chunks.length<1||chunks.length>${MAX_IMAGE_STAGING_CHUNKS})throw new Error("missing bounded LinkedIn image bytes");delete globalThis[input.stagingKey];if(Object.hasOwn(globalThis,input.stagingKey))throw new Error("LinkedIn image staging cleanup failed");let encoded="";for(let index=0;index<chunks.length;index+=1){const chunk=chunks[index];if(typeof chunk!=="string"||chunk.length<1||chunk.length>${IMAGE_STAGING_CHUNK_CHARACTERS}||!/^[A-Za-z0-9+/]*={0,2}$/.test(chunk))throw new Error("invalid LinkedIn image bytes");encoded+=chunk;chunks[index]=""}if(encoded.length!==input.expectedBase64Length||encoded.length>${MAX_IMAGE_BASE64_CHARACTERS})throw new Error("LinkedIn image changed encoded size");const binary=atob(encoded);encoded="";if(binary.length!==input.expectedByteLength||binary.length<24||binary.length>${MAX_IMAGE_BYTES})throw new Error("LinkedIn image changed size");const bytes=new Uint8Array(binary.length);for(let index=0;index<binary.length;index+=1)bytes[index]=binary.charCodeAt(index);const firstIdentity=await identity();assertIdentity(firstIdentity);const image=new Blob([bytes],{type:"image/png"});const mutationHeaders={...baseHeaders,"content-type":"application/json; charset=UTF-8","x-li-page-instance":input.pageInstance,"x-li-pem-metadata":"Voyager - Feed Images=register-vector-upload","x-li-track":input.track};const registrationBody=await requestJson("${LINKEDIN_IMAGE_REGISTRATION_PATH}",{body:JSON.stringify({fileSize:image.size,filename:"image.png",mediaUploadType:"IMAGE_SHARING"}),headers:mutationHeaders,method:"POST"},"LinkedIn image registration");const registrationEnvelope=registrationBody&&typeof registrationBody==="object"&&!Array.isArray(registrationBody)?registrationBody:null;if(registrationEnvelope===null)throw new Error("LinkedIn image registration changed shape");const registration=registrationEnvelope.data&&typeof registrationEnvelope.data==="object"&&!Array.isArray(registrationEnvelope.data)&®istrationEnvelope.data.value!==undefined?registrationEnvelope.data.value:registrationEnvelope.value!==undefined?registrationEnvelope.value:null;if(!registration||typeof registration!=="object"||Array.isArray(registration))throw new Error("LinkedIn image registration omitted its value");const allowedKeys=new Set(["mediaArtifactUrn","multipartMetadata","partUploadRequests","recipes","singleUploadHeaders","singleUploadUrl","type","urn"]);for(const key of Object.keys(registration))if(!allowedKeys.has(key))throw new Error("LinkedIn image registration returned an unreviewed field");if(!/^urn:li:(?:digitalmediaAsset|fsd_image):[A-Za-z0-9_(),.:%=-]{1,448}$/.test(registration.urn||""))throw new Error("LinkedIn image registration omitted its media URN");if(typeof registration.mediaArtifactUrn!=="string"||registration.mediaArtifactUrn.length<1||registration.mediaArtifactUrn.length>1024)throw new Error("LinkedIn image registration omitted its artifact URN");const checkedUrl=(value)=>{if(typeof value!=="string"||value.length<1||value.length>16384)throw new Error("LinkedIn image upload target changed shape");const url=new URL(value);if(url.protocol!=="https:"||url.username!==""||url.password!==""||url.hash!==""||url.port!==""||!(url.hostname==="linkedin.com"||url.hostname.endsWith(".linkedin.com")||url.hostname==="licdn.com"||url.hostname.endsWith(".licdn.com")))throw new Error("LinkedIn image upload target escaped its reviewed host family");return url};const checkedHeaders=(value,formData)=>{if(value===undefined)return {"csrf-token":csrf};if(!value||typeof value!=="object"||Array.isArray(value)||Object.keys(value).length>32)throw new Error("LinkedIn image upload headers changed shape");const output={};for(const [name,headerValue] of Object.entries(value)){const lower=name.toLowerCase();if(!/^[a-z0-9!#$%&'*+.^_|~-]{1,128}$/.test(lower)||typeof headerValue!=="string"||headerValue.length>8192||/[\\0\\r\\n]/.test(headerValue)||["cookie","host","content-length","origin","referer"].includes(lower)||lower.startsWith("sec-"))throw new Error("LinkedIn image upload headers changed shape");if(formData&&lower==="content-type")continue;output[lower]=headerValue}output["csrf-token"]=csrf;return output};const upload=async(urlValue,body,headers,formData)=>{const url=checkedUrl(urlValue);const response=await fetch(url.href,{body,credentials:url.origin===location.origin?"include":"omit",headers:checkedHeaders(headers,formData),method:"PUT",redirect:"error",referrer:"${LINKEDIN_FEED_URL}"});if(response.status<200||response.status>=300)throw new Error("LinkedIn image upload status changed");return{headers:Object.fromEntries(response.headers.entries()),httpStatusCode:response.status}};if(registration.type==="SINGLE"||registration.type==="MULTIPART_FORMDATA"){if(registration.partUploadRequests!==undefined||registration.multipartMetadata!==undefined)throw new Error("LinkedIn single image upload returned multipart fields");await upload(registration.singleUploadUrl,image,registration.singleUploadHeaders,registration.type==="MULTIPART_FORMDATA")}else if(registration.type==="MULTIPART"){if(registration.singleUploadUrl!==undefined||registration.singleUploadHeaders!==undefined||!Array.isArray(registration.partUploadRequests)||registration.partUploadRequests.length<1||registration.partUploadRequests.length>20||!registration.multipartMetadata||typeof registration.multipartMetadata!=="object"||Array.isArray(registration.multipartMetadata)||JSON.stringify(registration.multipartMetadata).length>65536)throw new Error("LinkedIn multipart image registration changed shape");let next=0;const parts=[];for(const part of registration.partUploadRequests){if(!part||typeof part!=="object"||Array.isArray(part)||Object.keys(part).sort().join(",")!=="firstByte,headers,lastByte,uploadUrl"||!Number.isSafeInteger(part.firstByte)||!Number.isSafeInteger(part.lastByte)||part.firstByte!==next||part.lastByte<part.firstByte||part.lastByte>=image.size)throw new Error("LinkedIn multipart image offsets changed shape");parts.push(await upload(part.uploadUrl,image.slice(part.firstByte,part.lastByte+1,"image/png"),part.headers,false));next=part.lastByte+1}if(next!==image.size)throw new Error("LinkedIn multipart image registration did not cover the file");await requestJson("${LINKEDIN_IMAGE_FINALIZATION_PATH}",{body:JSON.stringify({completeUploadRequest:{mediaArtifactUrn:registration.mediaArtifactUrn,multipartMetadata:registration.multipartMetadata,partUploadResponses:parts}}),headers:mutationHeaders,method:"POST"},"LinkedIn image finalization")}else throw new Error("LinkedIn image upload mechanism changed");return{mediaUrn:registration.urn}})()`;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function replaceRequiredUploadSource(
|
|
341
|
+
source: string,
|
|
342
|
+
from: string,
|
|
343
|
+
to: string,
|
|
344
|
+
): string {
|
|
345
|
+
const first = source.indexOf(from);
|
|
346
|
+
if (first < 0 || source.indexOf(from, first + from.length) >= 0) {
|
|
347
|
+
throw new Error("LinkedIn image upload source rewrite changed shape");
|
|
348
|
+
}
|
|
349
|
+
return source.replace(from, to);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function uploadEvaluationSource(
|
|
353
|
+
bindings: LinkedInPostPageBindings,
|
|
354
|
+
expectedSubject: string,
|
|
355
|
+
staging: LinkedInPostImageStaging,
|
|
356
|
+
): string {
|
|
357
|
+
let source = baseUploadEvaluationSource(bindings, expectedSubject, staging);
|
|
358
|
+
source = replaceRequiredUploadSource(
|
|
359
|
+
source,
|
|
360
|
+
'const allowedKeys=new Set(["mediaArtifactUrn","multipartMetadata","partUploadRequests","recipes","singleUploadHeaders","singleUploadUrl","type","urn"]);',
|
|
361
|
+
'const allowedKeys=new Set(["$type","assetRealtimeTopic","mediaArtifactUrn","multipartMetadata","partUploadRequests","pollingUrl","recipes","singleUploadHeaders","singleUploadUrl","type","urn"]);if(registration.$type!==undefined&®istration.$type!=="com.linkedin.mediauploader.MediaUploadMetadata")throw new Error("LinkedIn image registration changed response type");if(registration.assetRealtimeTopic!==undefined&&(typeof registration.assetRealtimeTopic!=="string"||registration.assetRealtimeTopic.length<1||registration.assetRealtimeTopic.length>4096))throw new Error("LinkedIn image registration changed realtime topic");if(registration.recipes!==undefined&&(!Array.isArray(registration.recipes)||registration.recipes.length<1||registration.recipes.length>20||registration.recipes.some((recipe)=>typeof recipe!=="string"||!/^urn:li:[A-Za-z0-9_(),.:%=-]{1,448}$/.test(recipe))||new Set(registration.recipes).size!==registration.recipes.length))throw new Error("LinkedIn image registration changed recipes");',
|
|
362
|
+
);
|
|
363
|
+
source = replaceRequiredUploadSource(
|
|
364
|
+
source,
|
|
365
|
+
"const checkedHeaders=(value,formData)=>{",
|
|
366
|
+
"if(registration.pollingUrl!==undefined)checkedUrl(registration.pollingUrl);const checkedHeaders=(value,formData)=>{",
|
|
367
|
+
);
|
|
368
|
+
source = replaceRequiredUploadSource(
|
|
369
|
+
source,
|
|
370
|
+
'if(registration.type==="SINGLE"||registration.type==="MULTIPART_FORMDATA"){',
|
|
371
|
+
'if(registration.type==="SINGLE"||registration.type==="MULTIPART_FORMDATA"||registration.type==="VECTOR"){if(registration.type==="VECTOR"&&(!registration.singleUploadHeaders||typeof registration.singleUploadHeaders!=="object"||Array.isArray(registration.singleUploadHeaders)||Object.keys(registration.singleUploadHeaders).sort().join(",")!=="media-type-family"||registration.singleUploadHeaders["media-type-family"]!=="STILLIMAGE"))throw new Error("LinkedIn vector image upload headers changed shape");',
|
|
372
|
+
);
|
|
373
|
+
return source;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function baseCreateEvaluationSource(
|
|
377
|
+
bindings: LinkedInPostPageBindings,
|
|
378
|
+
expectedSubject: string,
|
|
379
|
+
expectedProfileUrn: string,
|
|
380
|
+
variables: Readonly<Record<string, unknown>>,
|
|
381
|
+
mediaUrn: string | null,
|
|
382
|
+
): string {
|
|
383
|
+
const input = Object.freeze({
|
|
384
|
+
expectedProfileUrn,
|
|
385
|
+
expectedSubject,
|
|
386
|
+
mediaUrn,
|
|
387
|
+
pageInstance: bindings.pageInstance,
|
|
388
|
+
track: bindings.track,
|
|
389
|
+
variables,
|
|
390
|
+
});
|
|
391
|
+
return `(async()=>{${commonEvaluationPrelude(input)}const firstIdentity=await identity();assertIdentity(firstIdentity);const mutationHeaders={...baseHeaders,"content-type":"application/json; charset=UTF-8","x-li-page-instance":input.pageInstance,"x-li-pem-metadata":"Voyager - Sharing - CreateShare=sharing-create-content","x-li-track":input.track};const createPath="${LINKEDIN_GRAPHQL_PATH}?action=execute&queryId=${encodeURIComponent(LINKEDIN_POST_CREATE_MUTATION_ID)}";const createBody=await requestJson(createPath,{body:JSON.stringify({includeWebMetadata:true,queryId:"${LINKEDIN_POST_CREATE_MUTATION_ID}",variables:input.variables}),headers:mutationHeaders,method:"POST"},"LinkedIn post create");const createPayload=createBody&&typeof createBody==="object"&&!Array.isArray(createBody)&&createBody.data&&typeof createBody.data==="object"&&!Array.isArray(createBody.data)&&createBody.data.value&&typeof createBody.data.value==="object"&&!Array.isArray(createBody.data.value)?createBody.data.value:createBody&&typeof createBody==="object"&&!Array.isArray(createBody)&&createBody.value&&typeof createBody.value==="object"&&!Array.isArray(createBody.value)?createBody.value:createBody;if(!createPayload||typeof createPayload!=="object"||Array.isArray(createPayload))throw new Error("LinkedIn GraphQL response changed shape");if(Array.isArray(createPayload.errors)&&createPayload.errors.length>0)throw new Error("LinkedIn post create returned provider errors");const entity=createPayload.data?.createContentcreationDashShares?.entity;if(!entity||typeof entity!=="object"||Array.isArray(entity))throw new Error("LinkedIn post create omitted its entity");const entityUrn=entity.entityUrn;if(!/^urn:li:(?:fsd_share|share|ugcPost):[A-Za-z0-9_(),.:%=-]{1,448}$/.test(entityUrn||""))throw new Error("LinkedIn post create returned an invalid entity URN");if(!entity.status||typeof entity.status!=="object"||Array.isArray(entity.status)||!entity.status.lifecycleState||typeof entity.status.lifecycleState!=="object"||Array.isArray(entity.status.lifecycleState)||!entity.status.lifecycleState.PublishedState||typeof entity.status.lifecycleState.PublishedState!=="object"||Array.isArray(entity.status.lifecycleState.PublishedState))throw new Error("LinkedIn post create did not report a published lifecycle");return{entityUrn}})()`;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function createEvaluationSource(
|
|
395
|
+
bindings: LinkedInPostPageBindings,
|
|
396
|
+
expectedSubject: string,
|
|
397
|
+
expectedProfileUrn: string,
|
|
398
|
+
variables: Readonly<Record<string, unknown>>,
|
|
399
|
+
mediaUrn: string | null,
|
|
400
|
+
): string {
|
|
401
|
+
return replaceRequiredUploadSource(
|
|
402
|
+
baseCreateEvaluationSource(
|
|
403
|
+
bindings,
|
|
404
|
+
expectedSubject,
|
|
405
|
+
expectedProfileUrn,
|
|
406
|
+
variables,
|
|
407
|
+
mediaUrn,
|
|
408
|
+
),
|
|
409
|
+
'const entity=createPayload.data?.createContentcreationDashShares?.entity;if(!entity||typeof entity!=="object"||Array.isArray(entity))throw new Error("LinkedIn post create omitted its entity");',
|
|
410
|
+
'const entity=createPayload.data?.createContentcreationDashShares?.entity;if(!entity||typeof entity!=="object"||Array.isArray(entity)){const direct=createPayload.createContentcreationDashShares?.entity;const alternate=createBody&&typeof createBody==="object"&&!Array.isArray(createBody)?createBody.data?.createContentcreationDashShares?.entity:null;if(direct&&typeof direct==="object"&&!Array.isArray(direct))throw new Error("LinkedIn post create entity used direct nesting");if(alternate&&typeof alternate==="object"&&!Array.isArray(alternate))throw new Error("LinkedIn post create entity used alternate data nesting");if(createBody&&typeof createBody==="object"&&!Array.isArray(createBody)&&Array.isArray(createBody.included)&&createBody.included.length>0)throw new Error("LinkedIn post create returned normalized included entities");throw new Error("LinkedIn post create omitted its entity")}',
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function readbackEvaluationSource(
|
|
415
|
+
bindings: LinkedInPostPageBindings,
|
|
416
|
+
expectedSubject: string,
|
|
417
|
+
expectedProfileUrn: string,
|
|
418
|
+
variables: Readonly<Record<string, unknown>>,
|
|
419
|
+
mediaUrn: string | null,
|
|
420
|
+
entityUrn: string,
|
|
421
|
+
): string {
|
|
422
|
+
const input = Object.freeze({
|
|
423
|
+
entityUrn,
|
|
424
|
+
expectedProfileUrn,
|
|
425
|
+
expectedSubject,
|
|
426
|
+
mediaUrn,
|
|
427
|
+
pageInstance: bindings.pageInstance,
|
|
428
|
+
track: bindings.track,
|
|
429
|
+
variables,
|
|
430
|
+
});
|
|
431
|
+
return `(async()=>{${commonEvaluationPrelude(input)}const graphqlEnvelope=(body)=>{if(!body||typeof body!=="object"||Array.isArray(body))throw new Error("LinkedIn GraphQL response changed shape");const value=body.data&&typeof body.data==="object"&&!Array.isArray(body.data)&&body.data.value&&typeof body.data.value==="object"&&!Array.isArray(body.data.value)?body.data.value:body.value&&typeof body.value==="object"&&!Array.isArray(body.value)?body.value:body;return value};const variablesText="(moduleKey:feed-item:desktop,urnOrNss:"+input.entityUrn+")";const readbackPath="${LINKEDIN_GRAPHQL_PATH}?includeWebMetadata=true&queryId=${encodeURIComponent(LINKEDIN_POST_READBACK_QUERY_ID)}&variables="+encodeURIComponent(variablesText);const readbackBody=await requestJson(readbackPath,{headers:baseHeaders,method:"GET"},"LinkedIn post readback");const readbackPayload=graphqlEnvelope(readbackBody);if(Array.isArray(readbackPayload.errors)&&readbackPayload.errors.length>0)throw new Error("LinkedIn post readback returned provider errors");const elements=readbackPayload.data?.feedDashUpdatesByBackendUrnOrNss?.elements;if(!Array.isArray(elements)||elements.length!==1||!elements[0]||typeof elements[0]!=="object"||Array.isArray(elements[0]))throw new Error("LinkedIn post readback omitted its exact update");const update=elements[0];let visited=0;const contains=(value,expected,depth=0)=>{if(++visited>50000||depth>32)throw new Error("LinkedIn post readback exceeded its reviewed shape");if(value===expected)return true;if(Array.isArray(value))return value.some((item)=>contains(item,expected,depth+1));if(value&&typeof value==="object")return Object.values(value).some((item)=>contains(item,expected,depth+1));return false};const withinNamed=(value,name,expected,depth=0)=>{if(depth>32||!value||typeof value!=="object")return false;if(Array.isArray(value))return value.some((item)=>withinNamed(item,name,expected,depth+1));for(const [key,item] of Object.entries(value)){if(key===name&&contains(item,expected,depth+1))return true;if(withinNamed(item,name,expected,depth+1))return true}return false};visited=0;const entityMatched=contains(update,input.entityUrn);visited=0;const actorMatched=withinNamed(update,"actor",input.expectedProfileUrn);visited=0;const textMatched=withinNamed(update,"commentary",input.variables.post.commentary.text);visited=0;const mediaMatched=input.mediaUrn===null?false:contains(update,input.mediaUrn);if(!entityMatched||!actorMatched||!textMatched||(input.mediaUrn!==null&&!mediaMatched))throw new Error("LinkedIn independent post readback did not bind the confirmed post");const id=input.entityUrn.slice(input.entityUrn.lastIndexOf(":")+1);if(!/^[A-Za-z0-9_(),.=%-]{1,448}$/.test(id))throw new Error("LinkedIn post entity ID changed shape");const url="${LINKEDIN_ORIGIN}/feed/update/urn:li:activity:"+id+"/";return{actorMatched,entityMatched,entityUrn:input.entityUrn,lifecycle:"PUBLISHED",mediaMatched,mediaUrn:input.mediaUrn,textMatched,url}})()`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
async function finalizeBrowserSession(session: BrowserSession): Promise<void> {
|
|
435
|
+
const failures: unknown[] = [];
|
|
436
|
+
let closeVerified = false;
|
|
437
|
+
try {
|
|
438
|
+
await session.close();
|
|
439
|
+
closeVerified = true;
|
|
440
|
+
} catch (error) {
|
|
441
|
+
failures.push(error);
|
|
442
|
+
}
|
|
443
|
+
let cleanupVerified = false;
|
|
444
|
+
try {
|
|
445
|
+
await session.cleanup();
|
|
446
|
+
cleanupVerified = true;
|
|
447
|
+
} catch (error) {
|
|
448
|
+
failures.push(error);
|
|
449
|
+
}
|
|
450
|
+
if (closeVerified && cleanupVerified) return;
|
|
451
|
+
const cleanupEvidence = (
|
|
452
|
+
closeVerified
|
|
453
|
+
&& !cleanupVerified
|
|
454
|
+
&& session.cleanupResourceIdentity !== undefined
|
|
455
|
+
)
|
|
456
|
+
? Object.freeze({
|
|
457
|
+
kind: "agent-browser-closed-artifacts-v1" as const,
|
|
458
|
+
resource: session.cleanupResourceIdentity,
|
|
459
|
+
})
|
|
460
|
+
: undefined;
|
|
461
|
+
throw new PreservedBrowserArtifactsError(
|
|
462
|
+
"LinkedIn post browser finalization failed; private artifacts were preserved",
|
|
463
|
+
session.recoveryHandle ?? "session=linkedin-post-runtime;artifacts=unknown",
|
|
464
|
+
new AggregateError(failures, "LinkedIn post browser finalization failed"),
|
|
465
|
+
cleanupEvidence,
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export async function createLinkedInPostBrowserTransport(
|
|
470
|
+
auth: GhostgetAuth,
|
|
471
|
+
options: {
|
|
472
|
+
readonly timeoutMs: number;
|
|
473
|
+
readonly operationDeadline?: WebSessionOperationDeadline;
|
|
474
|
+
readonly publishCleanupResource?: WebSessionCleanupResourcePublisher;
|
|
475
|
+
readonly dependencies?: Partial<LinkedInPostBrowserDependencies>;
|
|
476
|
+
},
|
|
477
|
+
): Promise<LinkedInPostBrowserTransport> {
|
|
478
|
+
const createSession = options.dependencies?.createBrowserSession ?? createBrowserSession;
|
|
479
|
+
const sessionOptions: CreateBrowserSessionOptions = {
|
|
480
|
+
allowCodeOwnedEvaluation: true,
|
|
481
|
+
allowCodeOwnedNetworkObservation: true,
|
|
482
|
+
headed: true,
|
|
483
|
+
maxOutputBytes: MAX_BROWSER_OUTPUT_BYTES,
|
|
484
|
+
timeoutMs: options.timeoutMs,
|
|
485
|
+
...(options.operationDeadline === undefined
|
|
486
|
+
? {}
|
|
487
|
+
: { operationDeadline: options.operationDeadline }),
|
|
488
|
+
...(options.publishCleanupResource === undefined
|
|
489
|
+
? {}
|
|
490
|
+
: { publishCleanupResource: options.publishCleanupResource }),
|
|
491
|
+
};
|
|
492
|
+
const session = await createSession(postBrowserManifest, auth, sessionOptions);
|
|
493
|
+
let closed = false;
|
|
494
|
+
const remaining = (): number => options.operationDeadline?.remainingTimeMs() ?? options.timeoutMs;
|
|
495
|
+
const runEvaluations = async (
|
|
496
|
+
sources: readonly string[],
|
|
497
|
+
): Promise<readonly Readonly<Record<string, unknown>>[]> => {
|
|
498
|
+
if (closed) throw new Error("LinkedIn post browser transport is closed");
|
|
499
|
+
const records = await session.runBatch(
|
|
500
|
+
sources.map((source) => ["eval", source] as const),
|
|
501
|
+
remaining(),
|
|
502
|
+
MAX_BROWSER_OUTPUT_BYTES,
|
|
503
|
+
);
|
|
504
|
+
if (records.length !== sources.length) {
|
|
505
|
+
throw new Error("LinkedIn post browser omitted an evaluation response");
|
|
506
|
+
}
|
|
507
|
+
return Object.freeze(records.map((record) => browserEvaluationResult(record)));
|
|
508
|
+
};
|
|
509
|
+
const run = async (source: string): Promise<Readonly<Record<string, unknown>>> => {
|
|
510
|
+
const first = (await runEvaluations([source]))[0];
|
|
511
|
+
if (first === undefined) throw new Error("LinkedIn post browser omitted its response");
|
|
512
|
+
return first;
|
|
513
|
+
};
|
|
514
|
+
|
|
515
|
+
let bindings: LinkedInPostPageBindings;
|
|
516
|
+
try {
|
|
517
|
+
await session.runBatch(
|
|
518
|
+
[["open", LINKEDIN_FEED_URL], ["wait", "5000"]],
|
|
519
|
+
remaining(),
|
|
520
|
+
MAX_BROWSER_OUTPUT_BYTES,
|
|
521
|
+
);
|
|
522
|
+
const records = await session.runBatch(
|
|
523
|
+
[["network", "requests", "--filter", "/voyager/api/"]],
|
|
524
|
+
Math.min(remaining(), 30_000),
|
|
525
|
+
MAX_BROWSER_OUTPUT_BYTES,
|
|
526
|
+
);
|
|
527
|
+
const first = records[0];
|
|
528
|
+
if (first === undefined) throw new Error("LinkedIn post page-binding observation omitted its response");
|
|
529
|
+
bindings = linkedInPostPageBindings(browserResultData(first));
|
|
530
|
+
} catch (error) {
|
|
531
|
+
try {
|
|
532
|
+
await finalizeBrowserSession(session);
|
|
533
|
+
} catch (cleanupError) {
|
|
534
|
+
throw cleanupError;
|
|
535
|
+
}
|
|
536
|
+
throw error;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
return Object.freeze({
|
|
540
|
+
currentIdentityResponse: async () => {
|
|
541
|
+
const result = await run(identityEvaluationSource());
|
|
542
|
+
exactKeys(result, ["body", "contentType", "status"], "LinkedIn current-member browser request");
|
|
543
|
+
if (result.status !== 200) {
|
|
544
|
+
throw new Error("LinkedIn current-member browser request returned an unreviewed response");
|
|
545
|
+
}
|
|
546
|
+
return result.body;
|
|
547
|
+
},
|
|
548
|
+
uploadImage: async (expectedSubject: string, image: Uint8Array) => {
|
|
549
|
+
if (image.byteLength < 24 || image.byteLength > MAX_IMAGE_BYTES) {
|
|
550
|
+
throw new Error("LinkedIn post image is outside the reviewed byte bound");
|
|
551
|
+
}
|
|
552
|
+
const encoded = Buffer.from(image).toString("base64");
|
|
553
|
+
const chunkCount = Math.ceil(encoded.length / IMAGE_STAGING_CHUNK_CHARACTERS);
|
|
554
|
+
const staging = Object.freeze({
|
|
555
|
+
key: `__ghostgetLinkedInPostImage_${randomUUID().replaceAll("-", "")}`,
|
|
556
|
+
byteLength: image.byteLength,
|
|
557
|
+
base64Length: encoded.length,
|
|
558
|
+
chunkCount,
|
|
559
|
+
});
|
|
560
|
+
let failureStage: LinkedInPostImageFailureStage = "page image staging";
|
|
561
|
+
try {
|
|
562
|
+
const initialized = await run(imageStagingInitializationSource(staging));
|
|
563
|
+
exactKeys(initialized, ["ready"], "LinkedIn image staging initialization");
|
|
564
|
+
if (initialized.ready !== true) {
|
|
565
|
+
throw new Error("LinkedIn image staging initialization changed shape");
|
|
566
|
+
}
|
|
567
|
+
const sources: string[] = [];
|
|
568
|
+
for (let index = 0; index < chunkCount; index += 1) {
|
|
569
|
+
const offset = index * IMAGE_STAGING_CHUNK_CHARACTERS;
|
|
570
|
+
sources.push(imageStagingChunkSource(
|
|
571
|
+
staging,
|
|
572
|
+
index,
|
|
573
|
+
encoded.slice(offset, offset + IMAGE_STAGING_CHUNK_CHARACTERS),
|
|
574
|
+
));
|
|
575
|
+
}
|
|
576
|
+
for (
|
|
577
|
+
let offset = 0;
|
|
578
|
+
offset < sources.length;
|
|
579
|
+
offset += IMAGE_STAGING_COMMANDS_PER_BATCH
|
|
580
|
+
) {
|
|
581
|
+
const batch = sources.slice(offset, offset + IMAGE_STAGING_COMMANDS_PER_BATCH);
|
|
582
|
+
const staged = await runEvaluations(batch);
|
|
583
|
+
for (let index = 0; index < staged.length; index += 1) {
|
|
584
|
+
const result = staged[index]!;
|
|
585
|
+
exactKeys(result, ["staged"], "LinkedIn image staging command");
|
|
586
|
+
if (result.staged !== offset + index + 1) {
|
|
587
|
+
throw new Error("LinkedIn image staging command changed order");
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
failureStage = "image registration or upload response";
|
|
592
|
+
const result = await run(uploadEvaluationSource(bindings, expectedSubject, staging));
|
|
593
|
+
exactKeys(result, ["mediaUrn"], "LinkedIn image upload browser request");
|
|
594
|
+
return linkedInPostMediaUrn(result.mediaUrn);
|
|
595
|
+
} catch (error) {
|
|
596
|
+
throw new LinkedInPostImagePreparationError(
|
|
597
|
+
failureStage === "page image staging"
|
|
598
|
+
? failureStage
|
|
599
|
+
: linkedInPostImageFailureStage(error),
|
|
600
|
+
error,
|
|
601
|
+
);
|
|
602
|
+
} finally {
|
|
603
|
+
try {
|
|
604
|
+
await run(imageStagingCleanupSource(staging.key));
|
|
605
|
+
} catch {
|
|
606
|
+
// Browser finalization remains the authoritative private-artifact cleanup.
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
},
|
|
610
|
+
createPost: async (
|
|
611
|
+
expectedSubject: string,
|
|
612
|
+
expectedProfileUrn: string,
|
|
613
|
+
variables: Readonly<Record<string, unknown>>,
|
|
614
|
+
mediaUrn: string | null,
|
|
615
|
+
) => {
|
|
616
|
+
try {
|
|
617
|
+
const result = await run(createEvaluationSource(
|
|
618
|
+
bindings,
|
|
619
|
+
expectedSubject,
|
|
620
|
+
expectedProfileUrn,
|
|
621
|
+
variables,
|
|
622
|
+
mediaUrn,
|
|
623
|
+
));
|
|
624
|
+
exactKeys(result, ["entityUrn"], "LinkedIn post create browser request");
|
|
625
|
+
return linkedInPostEntityUrn(result.entityUrn);
|
|
626
|
+
} catch (error) {
|
|
627
|
+
throw new LinkedInPostCreateResponseError(error);
|
|
628
|
+
}
|
|
629
|
+
},
|
|
630
|
+
readPost: async (
|
|
631
|
+
expectedSubject: string,
|
|
632
|
+
expectedProfileUrn: string,
|
|
633
|
+
variables: Readonly<Record<string, unknown>>,
|
|
634
|
+
mediaUrn: string | null,
|
|
635
|
+
entityUrn: string,
|
|
636
|
+
) => {
|
|
637
|
+
const result = await run(readbackEvaluationSource(
|
|
638
|
+
bindings,
|
|
639
|
+
expectedSubject,
|
|
640
|
+
expectedProfileUrn,
|
|
641
|
+
variables,
|
|
642
|
+
mediaUrn,
|
|
643
|
+
linkedInPostEntityUrn(entityUrn),
|
|
644
|
+
));
|
|
645
|
+
return result;
|
|
646
|
+
},
|
|
647
|
+
close: async () => {
|
|
648
|
+
if (closed) return;
|
|
649
|
+
closed = true;
|
|
650
|
+
await finalizeBrowserSession(session);
|
|
651
|
+
},
|
|
652
|
+
});
|
|
653
|
+
}
|