@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,794 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import type {
|
|
4
|
+
ArticleDraftDocument,
|
|
5
|
+
ArticleDraftDocumentV2,
|
|
6
|
+
} from "../article-draft-document";
|
|
7
|
+
import type { BoundArticleDraftImage } from "../article-draft-images";
|
|
8
|
+
import type { GhostgetAuth } from "../auth";
|
|
9
|
+
import {
|
|
10
|
+
PreservedBrowserArtifactsError,
|
|
11
|
+
browserResultData,
|
|
12
|
+
createBrowserSession,
|
|
13
|
+
type BrowserSession,
|
|
14
|
+
type CreateBrowserSessionOptions,
|
|
15
|
+
} from "../browser";
|
|
16
|
+
import { canonicalJson, jsonScriptLiteral } from "../canonical-json";
|
|
17
|
+
import type { GhostgetManifest } from "../model";
|
|
18
|
+
import type {
|
|
19
|
+
WebSessionCleanupResourcePublisher,
|
|
20
|
+
WebSessionOperationDeadline,
|
|
21
|
+
} from "../web-session-execution";
|
|
22
|
+
import {
|
|
23
|
+
LINKEDIN_ARTICLE_PAGE_MAX_CHARACTERS,
|
|
24
|
+
LINKEDIN_ARTICLE_INLINE_IMAGE_UPLOAD_PATH,
|
|
25
|
+
LINKEDIN_FIRST_PARTY_ARTICLES_PATH,
|
|
26
|
+
buildLinkedInArticleCoverPatch,
|
|
27
|
+
buildLinkedInArticleContentPatch,
|
|
28
|
+
buildLinkedInArticleContentPatchV2,
|
|
29
|
+
buildLinkedInArticleCreateBody,
|
|
30
|
+
buildLinkedInArticleTitlePatch,
|
|
31
|
+
linkedInArticleDraftEditUrl,
|
|
32
|
+
linkedInArticleDraftEnvelopeFromCodePayloads,
|
|
33
|
+
linkedInArticleDraftEntityUrl,
|
|
34
|
+
normalizeLinkedInArticleImageUploadRegistration,
|
|
35
|
+
} from "./linkedin-web";
|
|
36
|
+
|
|
37
|
+
const LINKEDIN_ORIGIN = "https://www.linkedin.com";
|
|
38
|
+
const LINKEDIN_ARTICLE_NEW_URL = `${LINKEDIN_ORIGIN}/article/new/`;
|
|
39
|
+
const LINKEDIN_ARTICLE_AUTOSAVE_PEM_METADATA =
|
|
40
|
+
"Voyager - Article Creator=autosave-article";
|
|
41
|
+
const MAX_BROWSER_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
42
|
+
const LINKEDIN_IMAGE_STAGING_COMMANDS_PER_BATCH = 16;
|
|
43
|
+
|
|
44
|
+
const articleBrowserManifest: GhostgetManifest = Object.freeze({
|
|
45
|
+
schemaVersion: 4,
|
|
46
|
+
id: "linkedin-article-runtime",
|
|
47
|
+
version: "1.0.0",
|
|
48
|
+
displayName: "LinkedIn native Article runtime",
|
|
49
|
+
surfaceId: "linkedin",
|
|
50
|
+
origins: Object.freeze([LINKEDIN_ORIGIN]),
|
|
51
|
+
browserDomains: Object.freeze(["www.linkedin.com", "static.licdn.com"]),
|
|
52
|
+
operations: Object.freeze({}),
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export type LinkedInArticleBrowserTransport = {
|
|
56
|
+
readonly currentIdentityResponse: () => Promise<unknown>;
|
|
57
|
+
readonly prepareCreateDraft: () => Promise<void>;
|
|
58
|
+
readonly createDraft: (profileUrn: string, title: string) => Promise<string | null>;
|
|
59
|
+
readonly readDraftResponse: (draftId: string) => Promise<unknown>;
|
|
60
|
+
readonly updateTitle: (draftId: string, title: string) => Promise<void>;
|
|
61
|
+
readonly updateContent: (
|
|
62
|
+
draftId: string,
|
|
63
|
+
document: ArticleDraftDocument,
|
|
64
|
+
) => Promise<void>;
|
|
65
|
+
readonly uploadInlineImage?: (
|
|
66
|
+
draftId: string,
|
|
67
|
+
image: BoundArticleDraftImage,
|
|
68
|
+
) => Promise<string>;
|
|
69
|
+
readonly uploadCoverImage?: (
|
|
70
|
+
draftId: string,
|
|
71
|
+
image: BoundArticleDraftImage,
|
|
72
|
+
) => Promise<string>;
|
|
73
|
+
readonly updateCover?: (
|
|
74
|
+
draftId: string,
|
|
75
|
+
assetUrn: string,
|
|
76
|
+
) => Promise<void>;
|
|
77
|
+
readonly updateContentV2?: (
|
|
78
|
+
draftId: string,
|
|
79
|
+
document: ArticleDraftDocumentV2,
|
|
80
|
+
imageAssetUrns: readonly string[],
|
|
81
|
+
) => Promise<void>;
|
|
82
|
+
readonly close: () => Promise<void>;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export type LinkedInArticleBrowserDependencies = {
|
|
86
|
+
readonly createBrowserSession: typeof createBrowserSession;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
type RequestBinding = {
|
|
90
|
+
readonly method: "GET" | "POST";
|
|
91
|
+
readonly path: string;
|
|
92
|
+
readonly referrer: string;
|
|
93
|
+
readonly body: string | null;
|
|
94
|
+
readonly pageInstance: string | null;
|
|
95
|
+
readonly pemMetadata: "article-autosave" | null;
|
|
96
|
+
readonly track: string | null;
|
|
97
|
+
readonly response: "json" | "status" | "created" | "page";
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
type LinkedInArticlePageBindings = {
|
|
101
|
+
readonly pageInstance: string;
|
|
102
|
+
readonly track: string;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const LINKEDIN_PAGE_INSTANCE_PATTERN =
|
|
106
|
+
/^urn:li:page:[A-Za-z0-9_:-]{1,128};[A-Za-z0-9+/=_-]{1,512}$/u;
|
|
107
|
+
|
|
108
|
+
function linkedInPageInstance(value: unknown): string {
|
|
109
|
+
if (typeof value !== "string" || !LINKEDIN_PAGE_INSTANCE_PATTERN.test(value)) {
|
|
110
|
+
throw new Error("LinkedIn Article page omitted its bounded page-instance binding");
|
|
111
|
+
}
|
|
112
|
+
return value;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function boundedString(value: unknown, label: string, maximum: number): string {
|
|
116
|
+
if (
|
|
117
|
+
typeof value !== "string"
|
|
118
|
+
|| value.length < 1
|
|
119
|
+
|| value.length > maximum
|
|
120
|
+
|| /[\0\r\n]/u.test(value)
|
|
121
|
+
) throw new Error(`${label} changed its reviewed bound`);
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
126
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function exactKeys(
|
|
130
|
+
value: Readonly<Record<string, unknown>>,
|
|
131
|
+
expected: readonly string[],
|
|
132
|
+
label: string,
|
|
133
|
+
): void {
|
|
134
|
+
if (Object.keys(value).sort().join(",") !== [...expected].sort().join(",")) {
|
|
135
|
+
throw new Error(`${label} returned an unexpected result shape`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function linkedInArticleTrack(value: unknown): string {
|
|
140
|
+
const track = boundedString(value, "LinkedIn Article x-li-track binding", 4_096);
|
|
141
|
+
let parsed: unknown;
|
|
142
|
+
try {
|
|
143
|
+
parsed = JSON.parse(track) as unknown;
|
|
144
|
+
} catch {
|
|
145
|
+
throw new Error("LinkedIn Article x-li-track binding changed shape");
|
|
146
|
+
}
|
|
147
|
+
if (!isRecord(parsed)) {
|
|
148
|
+
throw new Error("LinkedIn Article x-li-track binding changed shape");
|
|
149
|
+
}
|
|
150
|
+
exactKeys(parsed, [
|
|
151
|
+
"clientVersion",
|
|
152
|
+
"deviceFormFactor",
|
|
153
|
+
"displayDensity",
|
|
154
|
+
"displayHeight",
|
|
155
|
+
"displayWidth",
|
|
156
|
+
"mpName",
|
|
157
|
+
"mpVersion",
|
|
158
|
+
"osName",
|
|
159
|
+
"timezone",
|
|
160
|
+
"timezoneOffset",
|
|
161
|
+
], "LinkedIn Article x-li-track binding");
|
|
162
|
+
const clientVersion = boundedString(
|
|
163
|
+
parsed.clientVersion,
|
|
164
|
+
"LinkedIn Article x-li-track clientVersion",
|
|
165
|
+
64,
|
|
166
|
+
);
|
|
167
|
+
if (!/^[0-9]+(?:\.[0-9]+){1,7}$/u.test(clientVersion) || parsed.mpVersion !== clientVersion) {
|
|
168
|
+
throw new Error("LinkedIn Article x-li-track version changed shape");
|
|
169
|
+
}
|
|
170
|
+
boundedString(parsed.osName, "LinkedIn Article x-li-track osName", 32);
|
|
171
|
+
const timezone = boundedString(
|
|
172
|
+
parsed.timezone,
|
|
173
|
+
"LinkedIn Article x-li-track timezone",
|
|
174
|
+
128,
|
|
175
|
+
);
|
|
176
|
+
if (!/^[A-Za-z0-9_+.-]+(?:\/[A-Za-z0-9_+.-]+)+$/u.test(timezone)) {
|
|
177
|
+
throw new Error("LinkedIn Article x-li-track timezone changed shape");
|
|
178
|
+
}
|
|
179
|
+
if (parsed.deviceFormFactor !== "DESKTOP" || parsed.mpName !== "voyager-web") {
|
|
180
|
+
throw new Error("LinkedIn Article x-li-track client binding changed shape");
|
|
181
|
+
}
|
|
182
|
+
if (
|
|
183
|
+
!Number.isInteger(parsed.timezoneOffset)
|
|
184
|
+
|| (parsed.timezoneOffset as number) < -24
|
|
185
|
+
|| (parsed.timezoneOffset as number) > 24
|
|
186
|
+
|| typeof parsed.displayDensity !== "number"
|
|
187
|
+
|| !Number.isFinite(parsed.displayDensity)
|
|
188
|
+
|| parsed.displayDensity < 0.5
|
|
189
|
+
|| parsed.displayDensity > 10
|
|
190
|
+
|| !Number.isInteger(parsed.displayWidth)
|
|
191
|
+
|| (parsed.displayWidth as number) < 1
|
|
192
|
+
|| (parsed.displayWidth as number) > 20_000
|
|
193
|
+
|| !Number.isInteger(parsed.displayHeight)
|
|
194
|
+
|| (parsed.displayHeight as number) < 1
|
|
195
|
+
|| (parsed.displayHeight as number) > 20_000
|
|
196
|
+
) throw new Error("LinkedIn Article x-li-track display binding changed shape");
|
|
197
|
+
return track;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function linkedInArticlePageBindings(
|
|
201
|
+
value: unknown,
|
|
202
|
+
expectedPageName: "d_flagship3_publishing_post_new" | "d_flagship3_publishing_post_edit",
|
|
203
|
+
): LinkedInArticlePageBindings {
|
|
204
|
+
if (!isRecord(value) || !Array.isArray(value.requests) || value.requests.length > 10_000) {
|
|
205
|
+
throw new Error("LinkedIn Article network observation changed shape");
|
|
206
|
+
}
|
|
207
|
+
let selected: LinkedInArticlePageBindings | null = null;
|
|
208
|
+
for (const item of value.requests) {
|
|
209
|
+
if (!isRecord(item) || !isRecord(item.headers)) continue;
|
|
210
|
+
if (
|
|
211
|
+
item.method !== "GET"
|
|
212
|
+
|| item.status !== 200
|
|
213
|
+
|| typeof item.url !== "string"
|
|
214
|
+
|| item.url.length > 64 * 1_024
|
|
215
|
+
) continue;
|
|
216
|
+
let url: URL;
|
|
217
|
+
try {
|
|
218
|
+
url = new URL(item.url);
|
|
219
|
+
} catch {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (
|
|
223
|
+
url.origin !== LINKEDIN_ORIGIN
|
|
224
|
+
|| url.username !== ""
|
|
225
|
+
|| url.password !== ""
|
|
226
|
+
|| !url.pathname.startsWith("/voyager/api/")
|
|
227
|
+
) continue;
|
|
228
|
+
const pageInstanceValue = item.headers["x-li-page-instance"];
|
|
229
|
+
if (typeof pageInstanceValue !== "string") continue;
|
|
230
|
+
const name = /^urn:li:page:([A-Za-z0-9_:-]{1,128});/u.exec(pageInstanceValue)?.[1];
|
|
231
|
+
if (name !== expectedPageName) continue;
|
|
232
|
+
const candidate = Object.freeze({
|
|
233
|
+
pageInstance: linkedInPageInstance(pageInstanceValue),
|
|
234
|
+
track: linkedInArticleTrack(item.headers["x-li-track"]),
|
|
235
|
+
});
|
|
236
|
+
// Agent-browser's bounded request inventory is chronological and may
|
|
237
|
+
// retain an earlier instance of this same editor after a navigation retry.
|
|
238
|
+
// The final matching request is the binding for the currently visible
|
|
239
|
+
// editor; older matching instances must not make an otherwise exact
|
|
240
|
+
// current-page binding ambiguous.
|
|
241
|
+
selected = candidate;
|
|
242
|
+
}
|
|
243
|
+
if (selected === null) {
|
|
244
|
+
throw new Error("LinkedIn Article page omitted its bounded page-instance binding");
|
|
245
|
+
}
|
|
246
|
+
return selected;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function articleReadbackResponseCategory(
|
|
250
|
+
status: unknown,
|
|
251
|
+
contentType: unknown,
|
|
252
|
+
body: unknown,
|
|
253
|
+
): string {
|
|
254
|
+
const bodyShape = isRecord(body)
|
|
255
|
+
? `:${Object.keys(body).sort().join("+") || "empty"}`
|
|
256
|
+
: typeof contentType === "string" && contentType.includes("json")
|
|
257
|
+
? ":json-error"
|
|
258
|
+
: typeof contentType === "string" && contentType.startsWith("text/")
|
|
259
|
+
? ":text-error"
|
|
260
|
+
: ":opaque-error";
|
|
261
|
+
if (status === 401 || status === 403) return "session-rejected";
|
|
262
|
+
if (status === 400 || status === 422) return `request-rejected-${status}${bodyShape}`;
|
|
263
|
+
if (status === 404) return "contract-route-not-found";
|
|
264
|
+
if (status === 410) return "contract-route-retired";
|
|
265
|
+
if (status === 409) return "provider-conflict";
|
|
266
|
+
if (status === 429) return "provider-throttled";
|
|
267
|
+
if (typeof status === "number" && status >= 500) return "provider-unavailable";
|
|
268
|
+
if (status !== 200) return "status-drift";
|
|
269
|
+
if (
|
|
270
|
+
contentType !== "application/vnd.linkedin.normalized+json+2.1"
|
|
271
|
+
&& contentType !== "application/json"
|
|
272
|
+
) return "content-type-drift";
|
|
273
|
+
return "response-drift";
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function articleEditUrl(draftId: string): string {
|
|
277
|
+
return linkedInArticleDraftEditUrl(draftId).href;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function exactRequestPath(url: URL): string {
|
|
281
|
+
if (
|
|
282
|
+
url.origin !== LINKEDIN_ORIGIN
|
|
283
|
+
|| url.username !== ""
|
|
284
|
+
|| url.password !== ""
|
|
285
|
+
|| url.hash !== ""
|
|
286
|
+
) throw new Error("LinkedIn Article browser request escaped its reviewed origin");
|
|
287
|
+
return `${url.pathname}${url.search}`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function requestEvaluationSource(binding: RequestBinding): string {
|
|
291
|
+
const bound = jsonScriptLiteral(binding);
|
|
292
|
+
return `(async()=>{const input=${bound};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 headers=input.response==="page"?{accept:"text/html"}:{accept:"application/vnd.linkedin.normalized+json+2.1","csrf-token":csrf,"x-li-lang":"en_US","x-restli-protocol-version":"2.0.0"};if(input.body!==null){if(!/^urn:li:page:[A-Za-z0-9_:-]{1,128};[A-Za-z0-9+/=_-]{1,512}$/.test(input.pageInstance||""))throw new Error("missing LinkedIn browser page instance");if(typeof input.track!=="string"||input.track.length<1||input.track.length>4096||/[\\0\\r\\n]/u.test(input.track))throw new Error("missing LinkedIn browser track binding");if(input.pemMetadata!==null&&input.pemMetadata!=="article-autosave")throw new Error("invalid LinkedIn browser PEM binding");headers["x-li-page-instance"]=input.pageInstance;if(input.pemMetadata==="article-autosave")headers["x-li-pem-metadata"]="${LINKEDIN_ARTICLE_AUTOSAVE_PEM_METADATA}";headers["x-li-track"]=input.track;headers["content-type"]="application/json; charset=UTF-8"}else if(input.pemMetadata!==null)throw new Error("invalid LinkedIn browser PEM binding");const response=await fetch(input.path,{body:input.body===null?undefined:input.body,credentials:"include",headers,method:input.method,redirect:"error",referrer:input.referrer});const contentType=(response.headers.get("content-type")||"").split(";",1)[0].trim().toLowerCase();if(input.response==="page"){const payloads=[];if(response.status===200&&contentType==="text/html"){if(response.body===null)throw new Error("missing LinkedIn Article page body");const reader=response.body.getReader();const decoder=new TextDecoder();const chunks=[];let bytes=0;while(true){const part=await reader.read();if(part.done)break;bytes+=part.value.byteLength;if(bytes>${LINKEDIN_ARTICLE_PAGE_MAX_CHARACTERS}){await reader.cancel();throw new Error("LinkedIn Article page exceeded its reviewed bound")}chunks.push(decoder.decode(part.value,{stream:true}))}chunks.push(decoder.decode());const html=chunks.join("");const id=new RegExp("^/article/edit/([0-9]{1,32})/$","u").exec(input.path)?.[1];if(typeof id!=="string")throw new Error("invalid LinkedIn Article page path");const urn="urn:li:fsd_firstPartyArticle:"+id;for(const match of html.matchAll(/<code\\b([^>]*)>([\\s\\S]*?)<\\/code>/giu)){const attributes=match[1]||"";const body=match[2]||"";if(!body.includes(urn))continue;payloads.push({attributes,body});if(payloads.length>20)throw new Error("LinkedIn Article page returned too many matching payloads")}}return{contentType,payloads,status:response.status}}if(input.response==="json"){let body=null;if(contentType==="application/vnd.linkedin.normalized+json+2.1"||contentType==="application/json")body=await response.json();return{body,contentType,status:response.status}}if(input.response==="created")return{contentType,responseId:response.headers.get("x-restli-id"),status:response.status};return{contentType,status:response.status}})()`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function evaluationResult(
|
|
296
|
+
record: Readonly<Record<string, unknown>>,
|
|
297
|
+
): Readonly<Record<string, unknown>> {
|
|
298
|
+
const data = browserResultData(record);
|
|
299
|
+
if (!isRecord(data) || typeof data.origin !== "string" || !isRecord(data.result)) {
|
|
300
|
+
throw new Error("LinkedIn Article browser returned a malformed evaluation envelope");
|
|
301
|
+
}
|
|
302
|
+
let origin: URL;
|
|
303
|
+
try {
|
|
304
|
+
origin = new URL(data.origin);
|
|
305
|
+
} catch {
|
|
306
|
+
throw new Error("LinkedIn Article browser returned a malformed evaluation envelope");
|
|
307
|
+
}
|
|
308
|
+
if (
|
|
309
|
+
origin.origin !== LINKEDIN_ORIGIN
|
|
310
|
+
|| origin.username !== ""
|
|
311
|
+
|| origin.password !== ""
|
|
312
|
+
) throw new Error("LinkedIn Article browser returned a malformed evaluation envelope");
|
|
313
|
+
return data.result;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function linkedInArticleImageUploadEvaluationSource(input: {
|
|
317
|
+
readonly expectedBase64Length: number;
|
|
318
|
+
readonly expectedByteLength: number;
|
|
319
|
+
readonly expectedChunkCount: number;
|
|
320
|
+
readonly key: string;
|
|
321
|
+
readonly mediaType: string;
|
|
322
|
+
readonly referrer: string;
|
|
323
|
+
readonly uploadHeaders: Readonly<Record<string, string>>;
|
|
324
|
+
readonly uploadUrl: string;
|
|
325
|
+
}): string {
|
|
326
|
+
const bound = jsonScriptLiteral(input);
|
|
327
|
+
return `(async()=>{const input=${bound};if(location.origin!=="${LINKEDIN_ORIGIN}")throw new Error("unexpected LinkedIn origin");if(!Number.isSafeInteger(input.expectedBase64Length)||input.expectedBase64Length<1||!Number.isSafeInteger(input.expectedByteLength)||input.expectedByteLength<1||!Number.isSafeInteger(input.expectedChunkCount)||input.expectedChunkCount<1||input.expectedChunkCount>256)throw new Error("invalid LinkedIn image byte binding");const chunks=globalThis[input.key];delete globalThis[input.key];if(!Array.isArray(chunks)||chunks.length!==input.expectedChunkCount)throw new Error("missing bounded LinkedIn image bytes");let encoded="";for(const chunk of chunks){if(typeof chunk!=="string"||chunk.length<1||chunk.length>49152||!/^[A-Za-z0-9+/]*={0,2}$/.test(chunk))throw new Error("invalid LinkedIn image bytes");encoded+=chunk}if(encoded.length!==input.expectedBase64Length)throw new Error("LinkedIn image changed encoded size");const binary=atob(encoded);encoded="";if(binary.length!==input.expectedByteLength)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 image=new Blob([bytes],{type:input.mediaType});if(image.size!==input.expectedByteLength||image.type!==input.mediaType)throw new Error("LinkedIn image blob changed shape");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 upload=await fetch(input.uploadUrl,{body:image,credentials:"include",headers:{...input.uploadHeaders,"content-type":input.mediaType,"csrf-token":csrf},method:"PUT",redirect:"error",referrer:input.referrer});return{uploadStatus:upload.status}})()`;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function stageLinkedInArticleImageBytes(
|
|
331
|
+
session: BrowserSession,
|
|
332
|
+
key: string,
|
|
333
|
+
image: BoundArticleDraftImage,
|
|
334
|
+
timeoutMs: number,
|
|
335
|
+
): Promise<void> {
|
|
336
|
+
const init = `(async()=>{const key=${jsonScriptLiteral(key)};if(Object.hasOwn(globalThis,key))throw new Error("LinkedIn image staging key collision");globalThis[key]=[];return true})()`;
|
|
337
|
+
await session.runBatch([["eval", init]], timeoutMs, MAX_BROWSER_OUTPUT_BYTES);
|
|
338
|
+
const encoded = Buffer.from(image.bytes).toString("base64");
|
|
339
|
+
try {
|
|
340
|
+
const commands: (readonly string[])[] = [];
|
|
341
|
+
for (let offset = 0; offset < encoded.length; offset += 48 * 1_024) {
|
|
342
|
+
const chunk = encoded.slice(offset, offset + 48 * 1_024);
|
|
343
|
+
const source = `(async()=>{const key=${jsonScriptLiteral(key)};const chunks=globalThis[key];if(!Array.isArray(chunks)||chunks.length>=256)throw new Error("LinkedIn image staging changed shape");chunks.push(${jsonScriptLiteral(chunk)});return true})()`;
|
|
344
|
+
commands.push(["eval", source]);
|
|
345
|
+
}
|
|
346
|
+
for (
|
|
347
|
+
let offset = 0;
|
|
348
|
+
offset < commands.length;
|
|
349
|
+
offset += LINKEDIN_IMAGE_STAGING_COMMANDS_PER_BATCH
|
|
350
|
+
) {
|
|
351
|
+
await session.runBatch(
|
|
352
|
+
commands.slice(offset, offset + LINKEDIN_IMAGE_STAGING_COMMANDS_PER_BATCH),
|
|
353
|
+
timeoutMs,
|
|
354
|
+
MAX_BROWSER_OUTPUT_BYTES,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
} catch (error) {
|
|
358
|
+
try {
|
|
359
|
+
await session.runBatch(
|
|
360
|
+
[["eval", `(async()=>{delete globalThis[${jsonScriptLiteral(key)}];return true})()`]],
|
|
361
|
+
timeoutMs,
|
|
362
|
+
MAX_BROWSER_OUTPUT_BYTES,
|
|
363
|
+
);
|
|
364
|
+
} catch {
|
|
365
|
+
// Browser finalization remains the authoritative private-artifact cleanup.
|
|
366
|
+
}
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
async function finalizeBrowserSession(session: BrowserSession): Promise<void> {
|
|
372
|
+
const failures: unknown[] = [];
|
|
373
|
+
let closeVerified = false;
|
|
374
|
+
try {
|
|
375
|
+
await session.close();
|
|
376
|
+
closeVerified = true;
|
|
377
|
+
} catch (error) {
|
|
378
|
+
failures.push(error);
|
|
379
|
+
}
|
|
380
|
+
let cleanupVerified = false;
|
|
381
|
+
try {
|
|
382
|
+
await session.cleanup();
|
|
383
|
+
cleanupVerified = true;
|
|
384
|
+
} catch (error) {
|
|
385
|
+
failures.push(error);
|
|
386
|
+
}
|
|
387
|
+
if (closeVerified && cleanupVerified) return;
|
|
388
|
+
const cleanupEvidence = (
|
|
389
|
+
closeVerified
|
|
390
|
+
&& !cleanupVerified
|
|
391
|
+
&& session.cleanupResourceIdentity !== undefined
|
|
392
|
+
)
|
|
393
|
+
? Object.freeze({
|
|
394
|
+
kind: "agent-browser-closed-artifacts-v1" as const,
|
|
395
|
+
resource: session.cleanupResourceIdentity,
|
|
396
|
+
})
|
|
397
|
+
: undefined;
|
|
398
|
+
throw new PreservedBrowserArtifactsError(
|
|
399
|
+
"LinkedIn Article browser finalization failed; private artifacts were preserved",
|
|
400
|
+
session.recoveryHandle ?? "session=linkedin-article-runtime;artifacts=unknown",
|
|
401
|
+
new AggregateError(failures, "LinkedIn Article browser finalization failed"),
|
|
402
|
+
cleanupEvidence,
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export async function createLinkedInArticleBrowserTransport(
|
|
407
|
+
auth: GhostgetAuth,
|
|
408
|
+
options: {
|
|
409
|
+
readonly timeoutMs: number;
|
|
410
|
+
readonly operationDeadline?: WebSessionOperationDeadline;
|
|
411
|
+
readonly publishCleanupResource?: WebSessionCleanupResourcePublisher;
|
|
412
|
+
readonly dependencies?: Partial<LinkedInArticleBrowserDependencies>;
|
|
413
|
+
},
|
|
414
|
+
): Promise<LinkedInArticleBrowserTransport> {
|
|
415
|
+
const createSession = options.dependencies?.createBrowserSession ?? createBrowserSession;
|
|
416
|
+
const sessionOptions: CreateBrowserSessionOptions = {
|
|
417
|
+
allowCodeOwnedEvaluation: true,
|
|
418
|
+
allowCodeOwnedNetworkObservation: true,
|
|
419
|
+
headed: true,
|
|
420
|
+
maxOutputBytes: MAX_BROWSER_OUTPUT_BYTES,
|
|
421
|
+
timeoutMs: options.timeoutMs,
|
|
422
|
+
...(options.operationDeadline === undefined
|
|
423
|
+
? {}
|
|
424
|
+
: { operationDeadline: options.operationDeadline }),
|
|
425
|
+
...(options.publishCleanupResource === undefined
|
|
426
|
+
? {}
|
|
427
|
+
: { publishCleanupResource: options.publishCleanupResource }),
|
|
428
|
+
};
|
|
429
|
+
const session = await createSession(articleBrowserManifest, auth, sessionOptions);
|
|
430
|
+
let closed = false;
|
|
431
|
+
let articleBindings: LinkedInArticlePageBindings | null = null;
|
|
432
|
+
let activeEditor: { readonly kind: "new" } | { readonly kind: "edit"; readonly draftId: string } | null = null;
|
|
433
|
+
const run = async (binding: RequestBinding): Promise<Readonly<Record<string, unknown>>> => {
|
|
434
|
+
if (closed) throw new Error("LinkedIn Article browser transport is closed");
|
|
435
|
+
const records = await session.runBatch(
|
|
436
|
+
[["eval", requestEvaluationSource(binding)]],
|
|
437
|
+
options.operationDeadline?.remainingTimeMs() ?? options.timeoutMs,
|
|
438
|
+
MAX_BROWSER_OUTPUT_BYTES,
|
|
439
|
+
);
|
|
440
|
+
const first = records[0];
|
|
441
|
+
if (first === undefined) throw new Error("LinkedIn Article browser omitted its response");
|
|
442
|
+
return evaluationResult(first);
|
|
443
|
+
};
|
|
444
|
+
const open = async (url: URL): Promise<void> => {
|
|
445
|
+
if (
|
|
446
|
+
url.origin !== LINKEDIN_ORIGIN
|
|
447
|
+
|| url.username !== ""
|
|
448
|
+
|| url.password !== ""
|
|
449
|
+
|| url.hash !== ""
|
|
450
|
+
) throw new Error("LinkedIn Article browser navigation escaped its reviewed origin");
|
|
451
|
+
await session.runBatch(
|
|
452
|
+
[["open", url.href]],
|
|
453
|
+
options.operationDeadline?.remainingTimeMs() ?? options.timeoutMs,
|
|
454
|
+
MAX_BROWSER_OUTPUT_BYTES,
|
|
455
|
+
);
|
|
456
|
+
};
|
|
457
|
+
const openEditor = async (
|
|
458
|
+
url: URL,
|
|
459
|
+
expectedPageName: "d_flagship3_publishing_post_new" | "d_flagship3_publishing_post_edit",
|
|
460
|
+
editor: { readonly kind: "new" } | { readonly kind: "edit"; readonly draftId: string },
|
|
461
|
+
): Promise<void> => {
|
|
462
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
463
|
+
try {
|
|
464
|
+
await open(url);
|
|
465
|
+
await session.runBatch(
|
|
466
|
+
[["wait", "5000"]],
|
|
467
|
+
Math.min(options.operationDeadline?.remainingTimeMs() ?? options.timeoutMs, 20_000),
|
|
468
|
+
MAX_BROWSER_OUTPUT_BYTES,
|
|
469
|
+
);
|
|
470
|
+
const records = await session.runBatch(
|
|
471
|
+
[["network", "requests", "--filter", "/voyager/api/"]],
|
|
472
|
+
Math.min(options.operationDeadline?.remainingTimeMs() ?? options.timeoutMs, 30_000),
|
|
473
|
+
MAX_BROWSER_OUTPUT_BYTES,
|
|
474
|
+
);
|
|
475
|
+
const first = records[0];
|
|
476
|
+
if (first === undefined) {
|
|
477
|
+
throw new Error("LinkedIn Article page-binding observation omitted its response");
|
|
478
|
+
}
|
|
479
|
+
articleBindings = linkedInArticlePageBindings(
|
|
480
|
+
browserResultData(first),
|
|
481
|
+
expectedPageName,
|
|
482
|
+
);
|
|
483
|
+
activeEditor = editor;
|
|
484
|
+
return;
|
|
485
|
+
} catch (error) {
|
|
486
|
+
articleBindings = null;
|
|
487
|
+
activeEditor = null;
|
|
488
|
+
if (attempt === 1) throw error;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
await session.runBatch(
|
|
495
|
+
[["open", `${LINKEDIN_ORIGIN}/feed/`]],
|
|
496
|
+
options.operationDeadline?.remainingTimeMs() ?? options.timeoutMs,
|
|
497
|
+
MAX_BROWSER_OUTPUT_BYTES,
|
|
498
|
+
);
|
|
499
|
+
// A freshly cloned browser profile can return from navigation before the
|
|
500
|
+
// signed-in feed has installed its CSRF cookie in the page context. Wait
|
|
501
|
+
// once before the exact current-member probe; this remains wholly before
|
|
502
|
+
// the durable dispatch boundary and never retries a mutation.
|
|
503
|
+
await session.runBatch(
|
|
504
|
+
[["wait", "2000"]],
|
|
505
|
+
Math.min(options.operationDeadline?.remainingTimeMs() ?? options.timeoutMs, 10_000),
|
|
506
|
+
MAX_BROWSER_OUTPUT_BYTES,
|
|
507
|
+
);
|
|
508
|
+
} catch (error) {
|
|
509
|
+
try {
|
|
510
|
+
await finalizeBrowserSession(session);
|
|
511
|
+
} catch (cleanupError) {
|
|
512
|
+
throw cleanupError;
|
|
513
|
+
}
|
|
514
|
+
throw error;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const uploadArticleImage = async (
|
|
518
|
+
draftId: string,
|
|
519
|
+
image: BoundArticleDraftImage,
|
|
520
|
+
mediaUploadType: "PUBLISHING_COVER_IMAGE" | "PUBLISHING_INLINE_IMAGE",
|
|
521
|
+
label: string,
|
|
522
|
+
): Promise<string> => {
|
|
523
|
+
if (
|
|
524
|
+
activeEditor?.kind !== "edit"
|
|
525
|
+
|| activeEditor.draftId !== draftId
|
|
526
|
+
|| articleBindings === null
|
|
527
|
+
) throw new Error(`${label} upload omitted its exact editor binding`);
|
|
528
|
+
const registrationResult = await run({
|
|
529
|
+
method: "POST",
|
|
530
|
+
path: LINKEDIN_ARTICLE_INLINE_IMAGE_UPLOAD_PATH,
|
|
531
|
+
referrer: articleEditUrl(draftId),
|
|
532
|
+
body: canonicalJson({
|
|
533
|
+
fileSize: image.bytes.byteLength,
|
|
534
|
+
filename: image.filename,
|
|
535
|
+
mediaUploadType,
|
|
536
|
+
}),
|
|
537
|
+
pageInstance: articleBindings.pageInstance,
|
|
538
|
+
pemMetadata: null,
|
|
539
|
+
track: articleBindings.track,
|
|
540
|
+
response: "json",
|
|
541
|
+
});
|
|
542
|
+
exactKeys(
|
|
543
|
+
registrationResult,
|
|
544
|
+
["body", "contentType", "status"],
|
|
545
|
+
`${label} registration request`,
|
|
546
|
+
);
|
|
547
|
+
if (
|
|
548
|
+
registrationResult.status !== 200
|
|
549
|
+
|| (registrationResult.contentType !== "application/vnd.linkedin.normalized+json+2.1"
|
|
550
|
+
&& registrationResult.contentType !== "application/json")
|
|
551
|
+
) throw new Error(`${label} registration returned an unreviewed response`);
|
|
552
|
+
const registration = normalizeLinkedInArticleImageUploadRegistration(
|
|
553
|
+
registrationResult.body,
|
|
554
|
+
);
|
|
555
|
+
const key = `__ghostgetLinkedInArticleImage_${randomUUID().replaceAll("-", "")}`;
|
|
556
|
+
const timeoutMs = options.operationDeadline?.remainingTimeMs() ?? options.timeoutMs;
|
|
557
|
+
const encodedLength = Buffer.from(image.bytes).toString("base64").length;
|
|
558
|
+
const chunkCount = Math.ceil(encodedLength / (48 * 1_024));
|
|
559
|
+
await stageLinkedInArticleImageBytes(session, key, image, timeoutMs);
|
|
560
|
+
let transfer: Readonly<Record<string, unknown>>;
|
|
561
|
+
try {
|
|
562
|
+
const records = await session.runBatch(
|
|
563
|
+
[["eval", linkedInArticleImageUploadEvaluationSource({
|
|
564
|
+
expectedBase64Length: encodedLength,
|
|
565
|
+
expectedByteLength: image.bytes.byteLength,
|
|
566
|
+
expectedChunkCount: chunkCount,
|
|
567
|
+
key,
|
|
568
|
+
mediaType: image.mediaType,
|
|
569
|
+
referrer: articleEditUrl(draftId),
|
|
570
|
+
uploadHeaders: registration.uploadHeaders,
|
|
571
|
+
uploadUrl: registration.uploadUrl,
|
|
572
|
+
})]],
|
|
573
|
+
options.operationDeadline?.remainingTimeMs() ?? options.timeoutMs,
|
|
574
|
+
MAX_BROWSER_OUTPUT_BYTES,
|
|
575
|
+
);
|
|
576
|
+
const first = records[0];
|
|
577
|
+
if (first === undefined) throw new Error(`${label} upload omitted its response`);
|
|
578
|
+
transfer = evaluationResult(first);
|
|
579
|
+
} catch (error) {
|
|
580
|
+
try {
|
|
581
|
+
await session.runBatch(
|
|
582
|
+
[["eval", `(async()=>{delete globalThis[${jsonScriptLiteral(key)}];return true})()`]],
|
|
583
|
+
options.operationDeadline?.remainingTimeMs() ?? options.timeoutMs,
|
|
584
|
+
MAX_BROWSER_OUTPUT_BYTES,
|
|
585
|
+
);
|
|
586
|
+
} catch {
|
|
587
|
+
// Browser finalization remains the authoritative private-artifact cleanup.
|
|
588
|
+
}
|
|
589
|
+
throw error;
|
|
590
|
+
}
|
|
591
|
+
exactKeys(transfer, ["uploadStatus"], `${label} upload`);
|
|
592
|
+
// The reviewed editor proceeds from the signed transfer's 201 response
|
|
593
|
+
// directly to the Article autosave. Exact acceptance remains gated by the
|
|
594
|
+
// later private Article response and independent readback.
|
|
595
|
+
if (transfer.uploadStatus !== 201) {
|
|
596
|
+
throw new Error(`${label} upload returned an unreviewed response`);
|
|
597
|
+
}
|
|
598
|
+
return registration.assetUrn;
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
const transport: LinkedInArticleBrowserTransport = {
|
|
602
|
+
currentIdentityResponse: async () => {
|
|
603
|
+
const result = await run({
|
|
604
|
+
method: "GET",
|
|
605
|
+
path: "/voyager/api/me",
|
|
606
|
+
referrer: `${LINKEDIN_ORIGIN}/feed/`,
|
|
607
|
+
body: null,
|
|
608
|
+
pageInstance: null,
|
|
609
|
+
pemMetadata: null,
|
|
610
|
+
track: null,
|
|
611
|
+
response: "json",
|
|
612
|
+
});
|
|
613
|
+
exactKeys(result, ["body", "contentType", "status"], "LinkedIn current-member browser request");
|
|
614
|
+
if (
|
|
615
|
+
result.status !== 200
|
|
616
|
+
|| (result.contentType !== "application/vnd.linkedin.normalized+json+2.1"
|
|
617
|
+
&& result.contentType !== "application/json")
|
|
618
|
+
) throw new Error("LinkedIn current-member browser request returned an unreviewed response");
|
|
619
|
+
return result.body;
|
|
620
|
+
},
|
|
621
|
+
prepareCreateDraft: () => openEditor(
|
|
622
|
+
new URL(LINKEDIN_ARTICLE_NEW_URL),
|
|
623
|
+
"d_flagship3_publishing_post_new",
|
|
624
|
+
Object.freeze({ kind: "new" }),
|
|
625
|
+
),
|
|
626
|
+
createDraft: async (profileUrn, title) => {
|
|
627
|
+
if (activeEditor?.kind !== "new" || articleBindings === null) {
|
|
628
|
+
throw new Error("LinkedIn Article create omitted its exact editor binding");
|
|
629
|
+
}
|
|
630
|
+
const result = await run({
|
|
631
|
+
method: "POST",
|
|
632
|
+
path: `${LINKEDIN_FIRST_PARTY_ARTICLES_PATH}/`,
|
|
633
|
+
referrer: LINKEDIN_ARTICLE_NEW_URL,
|
|
634
|
+
body: canonicalJson(buildLinkedInArticleCreateBody(profileUrn, title)),
|
|
635
|
+
pageInstance: articleBindings.pageInstance,
|
|
636
|
+
pemMetadata: "article-autosave",
|
|
637
|
+
track: articleBindings.track,
|
|
638
|
+
response: "created",
|
|
639
|
+
});
|
|
640
|
+
exactKeys(result, ["contentType", "responseId", "status"], "LinkedIn Article create browser request");
|
|
641
|
+
if (result.status !== 201) {
|
|
642
|
+
throw new Error("LinkedIn Article create browser request returned an unreviewed response");
|
|
643
|
+
}
|
|
644
|
+
if (result.responseId !== null && typeof result.responseId !== "string") {
|
|
645
|
+
throw new Error("LinkedIn Article create browser response returned an invalid identifier");
|
|
646
|
+
}
|
|
647
|
+
return result.responseId as string | null;
|
|
648
|
+
},
|
|
649
|
+
readDraftResponse: async (draftId) => {
|
|
650
|
+
const editUrl = linkedInArticleDraftEditUrl(draftId);
|
|
651
|
+
if (activeEditor?.kind !== "edit" || activeEditor.draftId !== draftId) {
|
|
652
|
+
await openEditor(
|
|
653
|
+
editUrl,
|
|
654
|
+
"d_flagship3_publishing_post_edit",
|
|
655
|
+
Object.freeze({ kind: "edit", draftId }),
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
const result = await run({
|
|
659
|
+
method: "GET",
|
|
660
|
+
path: exactRequestPath(editUrl),
|
|
661
|
+
referrer: editUrl.href,
|
|
662
|
+
body: null,
|
|
663
|
+
pageInstance: null,
|
|
664
|
+
pemMetadata: null,
|
|
665
|
+
track: null,
|
|
666
|
+
response: "page",
|
|
667
|
+
});
|
|
668
|
+
exactKeys(
|
|
669
|
+
result,
|
|
670
|
+
["contentType", "payloads", "status"],
|
|
671
|
+
"LinkedIn Article readback browser request",
|
|
672
|
+
);
|
|
673
|
+
if (result.status !== 200 || result.contentType !== "text/html") {
|
|
674
|
+
throw new Error(
|
|
675
|
+
`LinkedIn Article readback browser request returned an unreviewed response (${articleReadbackResponseCategory(result.status, result.contentType, null)})`,
|
|
676
|
+
);
|
|
677
|
+
}
|
|
678
|
+
return linkedInArticleDraftEnvelopeFromCodePayloads(
|
|
679
|
+
result.payloads,
|
|
680
|
+
draftId,
|
|
681
|
+
);
|
|
682
|
+
},
|
|
683
|
+
updateTitle: async (draftId, title) => {
|
|
684
|
+
if (
|
|
685
|
+
activeEditor?.kind !== "edit"
|
|
686
|
+
|| activeEditor.draftId !== draftId
|
|
687
|
+
|| articleBindings === null
|
|
688
|
+
) throw new Error("LinkedIn Article title update omitted its exact editor binding");
|
|
689
|
+
const result = await run({
|
|
690
|
+
method: "POST",
|
|
691
|
+
path: exactRequestPath(linkedInArticleDraftEntityUrl(draftId)),
|
|
692
|
+
referrer: articleEditUrl(draftId),
|
|
693
|
+
body: canonicalJson(buildLinkedInArticleTitlePatch(title)),
|
|
694
|
+
pageInstance: articleBindings.pageInstance,
|
|
695
|
+
pemMetadata: "article-autosave",
|
|
696
|
+
track: articleBindings.track,
|
|
697
|
+
response: "status",
|
|
698
|
+
});
|
|
699
|
+
exactKeys(result, ["contentType", "status"], "LinkedIn Article title browser request");
|
|
700
|
+
if (result.status !== 200) {
|
|
701
|
+
throw new Error(
|
|
702
|
+
`LinkedIn Article title browser request returned an unreviewed response (${articleReadbackResponseCategory(result.status, result.contentType, null)})`,
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
},
|
|
706
|
+
updateContent: async (draftId, document) => {
|
|
707
|
+
if (
|
|
708
|
+
activeEditor?.kind !== "edit"
|
|
709
|
+
|| activeEditor.draftId !== draftId
|
|
710
|
+
|| articleBindings === null
|
|
711
|
+
) throw new Error("LinkedIn Article content update omitted its exact editor binding");
|
|
712
|
+
const result = await run({
|
|
713
|
+
method: "POST",
|
|
714
|
+
path: exactRequestPath(linkedInArticleDraftEntityUrl(draftId)),
|
|
715
|
+
referrer: articleEditUrl(draftId),
|
|
716
|
+
body: canonicalJson(buildLinkedInArticleContentPatch(document)),
|
|
717
|
+
pageInstance: articleBindings.pageInstance,
|
|
718
|
+
pemMetadata: "article-autosave",
|
|
719
|
+
track: articleBindings.track,
|
|
720
|
+
response: "status",
|
|
721
|
+
});
|
|
722
|
+
exactKeys(result, ["contentType", "status"], "LinkedIn Article content browser request");
|
|
723
|
+
if (result.status !== 200) {
|
|
724
|
+
throw new Error(
|
|
725
|
+
`LinkedIn Article content browser request returned an unreviewed response (${articleReadbackResponseCategory(result.status, result.contentType, null)})`,
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
},
|
|
729
|
+
uploadInlineImage: (draftId, image) => uploadArticleImage(
|
|
730
|
+
draftId,
|
|
731
|
+
image,
|
|
732
|
+
"PUBLISHING_INLINE_IMAGE",
|
|
733
|
+
"LinkedIn Article inline image",
|
|
734
|
+
),
|
|
735
|
+
uploadCoverImage: (draftId, image) => uploadArticleImage(
|
|
736
|
+
draftId,
|
|
737
|
+
image,
|
|
738
|
+
"PUBLISHING_COVER_IMAGE",
|
|
739
|
+
"LinkedIn Article cover image",
|
|
740
|
+
),
|
|
741
|
+
updateCover: async (draftId, assetUrn) => {
|
|
742
|
+
if (
|
|
743
|
+
activeEditor?.kind !== "edit"
|
|
744
|
+
|| activeEditor.draftId !== draftId
|
|
745
|
+
|| articleBindings === null
|
|
746
|
+
) throw new Error("LinkedIn Article cover update omitted its exact editor binding");
|
|
747
|
+
const result = await run({
|
|
748
|
+
method: "POST",
|
|
749
|
+
path: exactRequestPath(linkedInArticleDraftEntityUrl(draftId)),
|
|
750
|
+
referrer: articleEditUrl(draftId),
|
|
751
|
+
body: canonicalJson(buildLinkedInArticleCoverPatch(assetUrn)),
|
|
752
|
+
pageInstance: articleBindings.pageInstance,
|
|
753
|
+
pemMetadata: "article-autosave",
|
|
754
|
+
track: articleBindings.track,
|
|
755
|
+
response: "status",
|
|
756
|
+
});
|
|
757
|
+
exactKeys(result, ["contentType", "status"], "LinkedIn Article cover browser request");
|
|
758
|
+
if (result.status !== 200) {
|
|
759
|
+
throw new Error(
|
|
760
|
+
`LinkedIn Article cover browser request returned an unreviewed response (${articleReadbackResponseCategory(result.status, result.contentType, null)})`,
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
},
|
|
764
|
+
updateContentV2: async (draftId, document, imageAssetUrns) => {
|
|
765
|
+
if (
|
|
766
|
+
activeEditor?.kind !== "edit"
|
|
767
|
+
|| activeEditor.draftId !== draftId
|
|
768
|
+
|| articleBindings === null
|
|
769
|
+
) throw new Error("LinkedIn Article content update omitted its exact editor binding");
|
|
770
|
+
const result = await run({
|
|
771
|
+
method: "POST",
|
|
772
|
+
path: exactRequestPath(linkedInArticleDraftEntityUrl(draftId)),
|
|
773
|
+
referrer: articleEditUrl(draftId),
|
|
774
|
+
body: canonicalJson(buildLinkedInArticleContentPatchV2(document, imageAssetUrns)),
|
|
775
|
+
pageInstance: articleBindings.pageInstance,
|
|
776
|
+
pemMetadata: "article-autosave",
|
|
777
|
+
track: articleBindings.track,
|
|
778
|
+
response: "status",
|
|
779
|
+
});
|
|
780
|
+
exactKeys(result, ["contentType", "status"], "LinkedIn Article content browser request");
|
|
781
|
+
if (result.status !== 200) {
|
|
782
|
+
throw new Error(
|
|
783
|
+
`LinkedIn Article content browser request returned an unreviewed response (${articleReadbackResponseCategory(result.status, result.contentType, null)})`,
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
},
|
|
787
|
+
close: async () => {
|
|
788
|
+
if (closed) return;
|
|
789
|
+
closed = true;
|
|
790
|
+
await finalizeBrowserSession(session);
|
|
791
|
+
},
|
|
792
|
+
};
|
|
793
|
+
return Object.freeze(transport);
|
|
794
|
+
}
|