@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,830 @@
|
|
|
1
|
+
import {
|
|
2
|
+
encodeRestliV2Value,
|
|
3
|
+
LINKEDIN_GRAPHQL_PATH,
|
|
4
|
+
linkedInPersonalProfilePublicIdentifier,
|
|
5
|
+
linkedInPersonalProfileTarget,
|
|
6
|
+
type LinkedInProfileTarget,
|
|
7
|
+
} from "./linkedin-web";
|
|
8
|
+
|
|
9
|
+
export const LINKEDIN_PROFILE_CONTACT_INFO_QUERY_NAME =
|
|
10
|
+
"voyagerIdentityDashProfileContactInfo";
|
|
11
|
+
export const LINKEDIN_CONTACT_DETAILS_OVERLAY_SCREEN_ID =
|
|
12
|
+
"com.linkedin.sdui.flagshipnav.profile.ProfileContactDetailsOverlay";
|
|
13
|
+
const CONTACT_QUERY_ID =
|
|
14
|
+
/^voyagerIdentityDashProfileContactInfo\.[0-9a-f]{32}$/u;
|
|
15
|
+
|
|
16
|
+
const LINKEDIN_ORIGIN = "https://www.linkedin.com";
|
|
17
|
+
const PROFILE_URN = /^urn:li:fsd_profile:[A-Za-z0-9_-]{1,256}$/u;
|
|
18
|
+
const VIEWER_SUBJECT = /^urn:li:fsd_profile:[0-9]{1,32}$/u;
|
|
19
|
+
const EMAIL = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,24}$/u;
|
|
20
|
+
const PHONE = /^\+?[0-9][0-9 .\-()]{6,30}[0-9]$/u;
|
|
21
|
+
const BIRTHDAY = /^(?:[0-9]{4}-)?(?:0[1-9]|1[0-2])-(?:0[1-9]|[12][0-9]|3[01])$/u;
|
|
22
|
+
const CONNECTED_DISPLAY =
|
|
23
|
+
/^(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ([1-9]|[12][0-9]|3[01]), ([0-9]{4})$/u;
|
|
24
|
+
const MONTHS = Object.freeze({
|
|
25
|
+
Jan: "01",
|
|
26
|
+
Feb: "02",
|
|
27
|
+
Mar: "03",
|
|
28
|
+
Apr: "04",
|
|
29
|
+
May: "05",
|
|
30
|
+
Jun: "06",
|
|
31
|
+
Jul: "07",
|
|
32
|
+
Aug: "08",
|
|
33
|
+
Sep: "09",
|
|
34
|
+
Oct: "10",
|
|
35
|
+
Nov: "11",
|
|
36
|
+
Dec: "12",
|
|
37
|
+
});
|
|
38
|
+
const HTML_ENTITY =
|
|
39
|
+
/&(?:nbsp|quot|amp|lt|gt|apos|#(?:[xX][0-9A-Fa-f]{1,6}|[0-9]{1,7}));/gu;
|
|
40
|
+
const MAX_HTML_BYTES = 8 * 1024 * 1024;
|
|
41
|
+
const MAX_CODE_TAGS = 256;
|
|
42
|
+
const MAX_COMO_ROOTS = 8;
|
|
43
|
+
const MAX_WALK_NODES = 500_000;
|
|
44
|
+
const MAX_PHONES = 8;
|
|
45
|
+
const MAX_WEBSITES = 8;
|
|
46
|
+
const CONTACT_TYPE_SUFFIX = "ProfileContactInfo";
|
|
47
|
+
|
|
48
|
+
export type LinkedInContactInfoTarget = LinkedInProfileTarget;
|
|
49
|
+
|
|
50
|
+
export type LinkedInContactRelationship = "first-degree";
|
|
51
|
+
|
|
52
|
+
export type LinkedInProfileContactBinding = {
|
|
53
|
+
readonly vanity: string;
|
|
54
|
+
readonly profileUrn: string;
|
|
55
|
+
readonly url: string;
|
|
56
|
+
readonly relationship: LinkedInContactRelationship;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type LinkedInContactFields = {
|
|
60
|
+
readonly email: string | null;
|
|
61
|
+
readonly profileUrl: string | null;
|
|
62
|
+
readonly connectedSince: string | null;
|
|
63
|
+
readonly phones: readonly string[];
|
|
64
|
+
readonly websites: readonly string[];
|
|
65
|
+
readonly birthday: string | null;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type LinkedInContactInfo = {
|
|
69
|
+
readonly schemaVersion: 1;
|
|
70
|
+
readonly provider: "linkedin";
|
|
71
|
+
readonly profile: LinkedInProfileContactBinding;
|
|
72
|
+
readonly viewer: { readonly subject: string };
|
|
73
|
+
readonly observedAt: string;
|
|
74
|
+
readonly completeness: "complete" | "partial";
|
|
75
|
+
readonly contact: LinkedInContactFields;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type LinkedInContactInfoJsonInput = {
|
|
79
|
+
readonly profileUrl: string;
|
|
80
|
+
readonly profileUrn: string;
|
|
81
|
+
readonly queryId?: string;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
type JsonRecord = Readonly<Record<string, unknown>>;
|
|
85
|
+
|
|
86
|
+
function isRecord(value: unknown): value is JsonRecord {
|
|
87
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function boundedText(value: unknown, label: string, maximum: number): string {
|
|
91
|
+
if (
|
|
92
|
+
typeof value !== "string"
|
|
93
|
+
|| value.length < 1
|
|
94
|
+
|| value.length > maximum
|
|
95
|
+
|| /[\0\r]/u.test(value)
|
|
96
|
+
) throw new Error(`${label} must be a bounded string`);
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function observationTime(value: string): string {
|
|
101
|
+
if (
|
|
102
|
+
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(value)
|
|
103
|
+
|| !Number.isFinite(Date.parse(value))
|
|
104
|
+
) throw new Error("LinkedIn contact-info observedAt must be an exact UTC timestamp");
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function viewerSubject(value: unknown): string {
|
|
109
|
+
const subject = boundedText(value, "LinkedIn contact-info viewer subject", 512);
|
|
110
|
+
if (!VIEWER_SUBJECT.test(subject)) {
|
|
111
|
+
throw new Error("LinkedIn contact-info viewer subject has an unsupported format");
|
|
112
|
+
}
|
|
113
|
+
return subject;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function profileUrn(value: unknown): string {
|
|
117
|
+
const urn = boundedText(value, "LinkedIn contact-info profile URN", 512);
|
|
118
|
+
if (!PROFILE_URN.test(urn)) {
|
|
119
|
+
throw new Error("LinkedIn contact-info profile URN changed format");
|
|
120
|
+
}
|
|
121
|
+
return urn;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function linkedInContactInfoTarget(value: unknown): LinkedInContactInfoTarget {
|
|
125
|
+
return linkedInPersonalProfileTarget(value);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function contactQueryId(value: unknown): string {
|
|
129
|
+
const queryId = boundedText(value, "LinkedIn contact-info GraphQL queryId", 128);
|
|
130
|
+
if (!CONTACT_QUERY_ID.test(queryId)) {
|
|
131
|
+
throw new Error("LinkedIn contact-info GraphQL queryId changed format");
|
|
132
|
+
}
|
|
133
|
+
return queryId;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function buildLinkedInProfileContactInfoGraphqlPath(input: {
|
|
137
|
+
readonly profileUrn: unknown;
|
|
138
|
+
readonly queryId?: unknown;
|
|
139
|
+
}): string {
|
|
140
|
+
const urn = profileUrn(input.profileUrn);
|
|
141
|
+
const selector = input.queryId === undefined
|
|
142
|
+
? `queryName=${encodeURIComponent(LINKEDIN_PROFILE_CONTACT_INFO_QUERY_NAME)}`
|
|
143
|
+
: `queryId=${encodeURIComponent(contactQueryId(input.queryId))}`;
|
|
144
|
+
return `${LINKEDIN_GRAPHQL_PATH}?includeWebMetadata=true&${selector}&variables=(profileUrn:${encodeRestliV2Value(urn)})`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function linkedInProfileContactInfoGraphqlUrl(input: {
|
|
148
|
+
readonly profileUrn: unknown;
|
|
149
|
+
readonly queryId?: unknown;
|
|
150
|
+
}): URL {
|
|
151
|
+
return new URL(buildLinkedInProfileContactInfoGraphqlPath(input), LINKEDIN_ORIGIN);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function resolveLinkedInProfileContactInfoQueryId(html: unknown): string | undefined {
|
|
155
|
+
if (typeof html !== "string" || html.length < 1 || html.length > MAX_HTML_BYTES) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
const unique = new Set<string>();
|
|
159
|
+
for (const match of html.matchAll(/voyagerIdentityDashProfileContactInfo\.([0-9a-f]{32})/giu)) {
|
|
160
|
+
const decoration = match[1];
|
|
161
|
+
if (decoration === undefined) continue;
|
|
162
|
+
unique.add(`voyagerIdentityDashProfileContactInfo.${decoration.toLowerCase()}`);
|
|
163
|
+
}
|
|
164
|
+
if (unique.size !== 1) return undefined;
|
|
165
|
+
return unique.values().next().value;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function assertLinkedInContactInfoGraphqlUrl(url: URL): void {
|
|
169
|
+
const queryNames = [...url.searchParams.keys()];
|
|
170
|
+
const queryKey = queryNames[1];
|
|
171
|
+
if (
|
|
172
|
+
queryNames.length !== 3
|
|
173
|
+
|| queryNames[0] !== "includeWebMetadata"
|
|
174
|
+
|| queryNames[2] !== "variables"
|
|
175
|
+
|| (queryKey !== "queryId" && queryKey !== "queryName")
|
|
176
|
+
|| url.searchParams.get("includeWebMetadata") !== "true"
|
|
177
|
+
|| url.searchParams.getAll("includeWebMetadata").length !== 1
|
|
178
|
+
|| url.searchParams.getAll("variables").length !== 1
|
|
179
|
+
|| (queryKey !== undefined && url.searchParams.getAll(queryKey).length !== 1)
|
|
180
|
+
) throw new Error("LinkedIn contact-info request escaped its exact reviewed route");
|
|
181
|
+
if (queryKey === "queryId") {
|
|
182
|
+
contactQueryId(url.searchParams.get("queryId"));
|
|
183
|
+
} else if (url.searchParams.get("queryName") !== LINKEDIN_PROFILE_CONTACT_INFO_QUERY_NAME) {
|
|
184
|
+
throw new Error("LinkedIn contact-info request escaped its exact reviewed route");
|
|
185
|
+
}
|
|
186
|
+
const variables = url.searchParams.get("variables");
|
|
187
|
+
if (
|
|
188
|
+
typeof variables !== "string"
|
|
189
|
+
|| !/^\(profileUrn:urn:li:fsd_profile:[A-Za-z0-9_-]{1,256}\)$/u.test(variables)
|
|
190
|
+
) throw new Error("LinkedIn contact-info request escaped its exact reviewed route");
|
|
191
|
+
profileUrn(/^\(profileUrn:(urn:li:fsd_profile:[A-Za-z0-9_-]{1,256})\)$/u.exec(variables)?.[1]);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function assertLinkedInContactInfoRequest(requestValue: unknown): void {
|
|
195
|
+
if (!isRecord(requestValue)) {
|
|
196
|
+
throw new Error("LinkedIn contact-info request must be an object");
|
|
197
|
+
}
|
|
198
|
+
const method = boundedText(requestValue.method, "LinkedIn contact-info request method", 16)
|
|
199
|
+
.toUpperCase();
|
|
200
|
+
if (method !== "GET") throw new Error("LinkedIn contact-info reads require GET");
|
|
201
|
+
const rawUrl = requestValue.url;
|
|
202
|
+
if (!(rawUrl instanceof URL) && typeof rawUrl !== "string") {
|
|
203
|
+
throw new Error("LinkedIn contact-info request URL is invalid");
|
|
204
|
+
}
|
|
205
|
+
const url = rawUrl instanceof URL ? new URL(rawUrl.href) : new URL(rawUrl);
|
|
206
|
+
if (
|
|
207
|
+
url.origin === LINKEDIN_ORIGIN
|
|
208
|
+
&& url.username === ""
|
|
209
|
+
&& url.password === ""
|
|
210
|
+
&& url.hash === ""
|
|
211
|
+
) {
|
|
212
|
+
if (url.search === "" && url.pathname === "/voyager/api/me") return;
|
|
213
|
+
if (url.search === "" && /^\/in\/[A-Za-z0-9][A-Za-z0-9_-]{1,99}\/$/u.test(url.pathname)) {
|
|
214
|
+
linkedInPersonalProfileTarget(url.href);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (url.pathname === LINKEDIN_GRAPHQL_PATH) {
|
|
218
|
+
assertLinkedInContactInfoGraphqlUrl(url);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
throw new Error("LinkedIn contact-info request escaped its exact reviewed route");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function decodeHtmlEntity(entity: string): string {
|
|
226
|
+
if (entity === " ") return " ";
|
|
227
|
+
if (entity === """) return '"';
|
|
228
|
+
if (entity === "&") return "&";
|
|
229
|
+
if (entity === "<") return "<";
|
|
230
|
+
if (entity === ">") return ">";
|
|
231
|
+
if (entity === "'") return "'";
|
|
232
|
+
const numeric = /^&#(?:[xX]([0-9A-Fa-f]{1,6})|([0-9]{1,7}));$/u.exec(entity);
|
|
233
|
+
if (numeric === null) {
|
|
234
|
+
throw new Error("LinkedIn contact-info page used an unsupported HTML entity");
|
|
235
|
+
}
|
|
236
|
+
const codePoint = Number.parseInt(
|
|
237
|
+
numeric[1] ?? numeric[2] ?? "",
|
|
238
|
+
numeric[1] === undefined ? 10 : 16,
|
|
239
|
+
);
|
|
240
|
+
if (
|
|
241
|
+
!Number.isSafeInteger(codePoint)
|
|
242
|
+
|| codePoint < 0
|
|
243
|
+
|| codePoint > 0x10_FFFF
|
|
244
|
+
|| (codePoint >= 0xD800 && codePoint <= 0xDFFF)
|
|
245
|
+
) throw new Error("LinkedIn contact-info page used an invalid numeric HTML entity");
|
|
246
|
+
return String.fromCodePoint(codePoint);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function htmlAttribute(value: string, name: string): string | null {
|
|
250
|
+
const matches = [...value.matchAll(
|
|
251
|
+
new RegExp(`(?:^|\\s)${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, "giu"),
|
|
252
|
+
)];
|
|
253
|
+
if (matches.length === 0) return null;
|
|
254
|
+
if (matches.length !== 1) throw new Error(`LinkedIn contact-info HTML repeated ${name}`);
|
|
255
|
+
const raw = matches[0]?.[1] ?? matches[0]?.[2];
|
|
256
|
+
if (raw === undefined || raw.length > 4_096) {
|
|
257
|
+
throw new Error(`LinkedIn contact-info HTML ${name} exceeded its reviewed bound`);
|
|
258
|
+
}
|
|
259
|
+
return raw.replace(HTML_ENTITY, (entity) => decodeHtmlEntity(entity));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function extractBalancedJsonObject(source: string, start: number): string | undefined {
|
|
263
|
+
if (source[start] !== "{") return undefined;
|
|
264
|
+
let depth = 0;
|
|
265
|
+
let inString = false;
|
|
266
|
+
let escaped = false;
|
|
267
|
+
const limit = Math.min(source.length, start + MAX_HTML_BYTES);
|
|
268
|
+
for (let index = start; index < limit; index += 1) {
|
|
269
|
+
const character = source[index];
|
|
270
|
+
if (character === undefined) break;
|
|
271
|
+
if (inString) {
|
|
272
|
+
if (escaped) {
|
|
273
|
+
escaped = false;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (character === "\\") {
|
|
277
|
+
escaped = true;
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (character === "\"") inString = false;
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (character === "\"") {
|
|
284
|
+
inString = true;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (character === "{") depth += 1;
|
|
288
|
+
else if (character === "}") {
|
|
289
|
+
depth -= 1;
|
|
290
|
+
if (depth === 0) return source.slice(start, index + 1);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function extractComoRehydrationRoots(html: string): unknown[] {
|
|
297
|
+
const roots: unknown[] = [];
|
|
298
|
+
const marker = "__como_rehydration__";
|
|
299
|
+
let searchFrom = 0;
|
|
300
|
+
while (searchFrom < html.length && roots.length < MAX_COMO_ROOTS) {
|
|
301
|
+
const markerIndex = html.indexOf(marker, searchFrom);
|
|
302
|
+
if (markerIndex === -1) break;
|
|
303
|
+
let cursor = markerIndex + marker.length;
|
|
304
|
+
while (cursor < html.length && /\s/u.test(html[cursor] ?? "")) cursor += 1;
|
|
305
|
+
if (html[cursor] !== "=") {
|
|
306
|
+
searchFrom = markerIndex + marker.length;
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
cursor += 1;
|
|
310
|
+
while (cursor < html.length && /\s/u.test(html[cursor] ?? "")) cursor += 1;
|
|
311
|
+
if (html[cursor] !== "{") {
|
|
312
|
+
searchFrom = cursor;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const json = extractBalancedJsonObject(html, cursor);
|
|
316
|
+
if (json === undefined) {
|
|
317
|
+
searchFrom = cursor + 1;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
try {
|
|
321
|
+
roots.push(JSON.parse(json) as unknown);
|
|
322
|
+
} catch {
|
|
323
|
+
throw new Error("LinkedIn contact-info bootstrap payload contained malformed JSON");
|
|
324
|
+
}
|
|
325
|
+
searchFrom = cursor + json.length;
|
|
326
|
+
}
|
|
327
|
+
return roots;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function embeddedRecords(html: unknown): readonly JsonRecord[] {
|
|
331
|
+
if (typeof html !== "string" || html.length < 1 || html.length > MAX_HTML_BYTES) {
|
|
332
|
+
throw new Error("LinkedIn contact-info profile page exceeded its reviewed HTML bound");
|
|
333
|
+
}
|
|
334
|
+
const roots: unknown[] = [];
|
|
335
|
+
let codeTags = 0;
|
|
336
|
+
for (const match of html.matchAll(/<code\b([^>]{0,4096})>([\s\S]*?)<\/code>/giu)) {
|
|
337
|
+
codeTags += 1;
|
|
338
|
+
if (codeTags > MAX_CODE_TAGS) {
|
|
339
|
+
throw new Error("LinkedIn contact-info profile page returned too many code payloads");
|
|
340
|
+
}
|
|
341
|
+
const attributes = match[1];
|
|
342
|
+
const body = match[2];
|
|
343
|
+
if (attributes === undefined || body === undefined) continue;
|
|
344
|
+
const id = htmlAttribute(attributes, "id");
|
|
345
|
+
if (id === null || !/^bpr-guid-[0-9]{1,12}$/u.test(id)) continue;
|
|
346
|
+
if (body.length < 1 || body.length > 1024 * 1024) {
|
|
347
|
+
throw new Error("LinkedIn contact-info bootstrap payload exceeded its reviewed bound");
|
|
348
|
+
}
|
|
349
|
+
const json = body.replace(HTML_ENTITY, (entity) => decodeHtmlEntity(entity)).trim();
|
|
350
|
+
try {
|
|
351
|
+
roots.push(JSON.parse(json) as unknown);
|
|
352
|
+
} catch {
|
|
353
|
+
throw new Error("LinkedIn contact-info bootstrap payload contained malformed JSON");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
roots.push(...extractComoRehydrationRoots(html));
|
|
357
|
+
if (roots.length < 1) {
|
|
358
|
+
throw new Error("LinkedIn contact-info profile page omitted its bootstrap payloads");
|
|
359
|
+
}
|
|
360
|
+
const records: JsonRecord[] = [];
|
|
361
|
+
const stack = roots.map((value) => ({ value, depth: 0 }));
|
|
362
|
+
let nodes = 0;
|
|
363
|
+
while (stack.length > 0) {
|
|
364
|
+
const next = stack.pop()!;
|
|
365
|
+
nodes += 1;
|
|
366
|
+
if (nodes > MAX_WALK_NODES || next.depth > 32) {
|
|
367
|
+
throw new Error("LinkedIn contact-info bootstrap exceeded its traversal bound");
|
|
368
|
+
}
|
|
369
|
+
if (Array.isArray(next.value)) {
|
|
370
|
+
if (next.value.length > 20_000) {
|
|
371
|
+
throw new Error("LinkedIn contact-info bootstrap array exceeded its reviewed bound");
|
|
372
|
+
}
|
|
373
|
+
for (const value of next.value) stack.push({ value, depth: next.depth + 1 });
|
|
374
|
+
continue;
|
|
375
|
+
}
|
|
376
|
+
if (!isRecord(next.value)) continue;
|
|
377
|
+
records.push(next.value);
|
|
378
|
+
for (const value of Object.values(next.value)) {
|
|
379
|
+
stack.push({ value, depth: next.depth + 1 });
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return Object.freeze(records);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function typeName(value: unknown): string {
|
|
386
|
+
return typeof value === "string" ? value : "";
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function distanceValue(record: JsonRecord): string | number | null {
|
|
390
|
+
const raw = record.memberDistance ?? record.networkDistance ?? record.distance;
|
|
391
|
+
if (typeof raw === "string" || typeof raw === "number") return raw;
|
|
392
|
+
if (isRecord(raw) && (typeof raw.value === "string" || typeof raw.value === "number")) {
|
|
393
|
+
return raw.value;
|
|
394
|
+
}
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function isFirstDegree(value: string | number): boolean {
|
|
399
|
+
return value === "DISTANCE_1" || value === 1 || value === "1";
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function isSelfDistance(value: string | number): boolean {
|
|
403
|
+
return value === "DISTANCE_SELF" || value === "SELF" || value === 0 || value === "0";
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function optionalPublicIdentifier(value: unknown): string | null {
|
|
407
|
+
if (typeof value !== "string" || value.length < 1) return null;
|
|
408
|
+
try {
|
|
409
|
+
return linkedInPersonalProfilePublicIdentifier(value);
|
|
410
|
+
} catch {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function vanityFromHref(value: unknown): string | null {
|
|
416
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 2_048) return null;
|
|
417
|
+
let url: URL;
|
|
418
|
+
try {
|
|
419
|
+
url = new URL(value.startsWith("http") ? value : `https://www.linkedin.com${value.startsWith("/") ? value : `/${value}`}`);
|
|
420
|
+
} catch {
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
if (url.hostname !== "www.linkedin.com" && url.hostname !== "linkedin.com") return null;
|
|
424
|
+
const match = /^\/in\/([A-Za-z0-9][A-Za-z0-9_-]{1,99})\/?$/u.exec(url.pathname);
|
|
425
|
+
if (match?.[1] === undefined) return null;
|
|
426
|
+
return optionalPublicIdentifier(match[1]);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function vanityFromRecord(record: JsonRecord): string | null {
|
|
430
|
+
return optionalPublicIdentifier(record.publicIdentifier)
|
|
431
|
+
?? optionalPublicIdentifier(record.vanityName)
|
|
432
|
+
?? vanityFromHref(record.profileUrl)
|
|
433
|
+
?? vanityFromHref(record.url)
|
|
434
|
+
?? vanityFromHref(record.canonicalUrl)
|
|
435
|
+
?? vanityFromHref(record.navigationUrl);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function profileUrnFromRecord(record: JsonRecord): string | null {
|
|
439
|
+
for (const key of ["entityUrn", "objectUrn", "profileUrn"] as const) {
|
|
440
|
+
const value = record[key];
|
|
441
|
+
if (typeof value !== "string") continue;
|
|
442
|
+
try {
|
|
443
|
+
return profileUrn(value);
|
|
444
|
+
} catch {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function selfProfileError(): Error {
|
|
452
|
+
return new Error(
|
|
453
|
+
"LinkedIn contacts.read reads one 1st-degree connection; use profiles.read for the signed-in self profile",
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function projectLinkedInProfileContactBinding(input: {
|
|
458
|
+
readonly profileHtml: unknown;
|
|
459
|
+
readonly profileUrl: unknown;
|
|
460
|
+
readonly expectedViewerSubject: unknown;
|
|
461
|
+
}): LinkedInProfileContactBinding {
|
|
462
|
+
const target = linkedInContactInfoTarget(input.profileUrl);
|
|
463
|
+
const viewer = viewerSubject(input.expectedViewerSubject);
|
|
464
|
+
const records = embeddedRecords(input.profileHtml);
|
|
465
|
+
const vanityRecords = records.filter((record) => vanityFromRecord(record) === target.slug);
|
|
466
|
+
if (vanityRecords.length < 1) {
|
|
467
|
+
throw new Error("LinkedIn contact-info profile page did not bind the requested vanity");
|
|
468
|
+
}
|
|
469
|
+
const urns = new Set<string>();
|
|
470
|
+
for (const record of vanityRecords) {
|
|
471
|
+
const urn = profileUrnFromRecord(record);
|
|
472
|
+
if (urn !== null) urns.add(urn);
|
|
473
|
+
}
|
|
474
|
+
if (urns.size < 1) {
|
|
475
|
+
throw new Error("LinkedIn contact-info profile page omitted its target identity");
|
|
476
|
+
}
|
|
477
|
+
if (urns.size !== 1) {
|
|
478
|
+
throw new Error("LinkedIn contact-info profile page exposed ambiguous target identities");
|
|
479
|
+
}
|
|
480
|
+
const urn = urns.values().next().value!;
|
|
481
|
+
if (urn === viewer) throw selfProfileError();
|
|
482
|
+
const related = records.filter((record) =>
|
|
483
|
+
vanityFromRecord(record) === target.slug || profileUrnFromRecord(record) === urn
|
|
484
|
+
);
|
|
485
|
+
const distances = related
|
|
486
|
+
.map(distanceValue)
|
|
487
|
+
.filter((value): value is string | number => value !== null);
|
|
488
|
+
const unique = [...new Set(distances.map((value) => String(value)))];
|
|
489
|
+
if (unique.length !== 1 || distances[0] === undefined) {
|
|
490
|
+
throw new Error("LinkedIn contact-info profile page omitted or contradicted its relationship distance");
|
|
491
|
+
}
|
|
492
|
+
const distance = distances[0];
|
|
493
|
+
if (isSelfDistance(distance)) throw selfProfileError();
|
|
494
|
+
if (!isFirstDegree(distance)) {
|
|
495
|
+
throw new Error(
|
|
496
|
+
"LinkedIn hid Contact info because the signed-in viewer is not a 1st-degree connection of this profile",
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
return Object.freeze({
|
|
500
|
+
vanity: target.slug,
|
|
501
|
+
profileUrn: urn,
|
|
502
|
+
url: target.url,
|
|
503
|
+
relationship: "first-degree",
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export function projectLinkedInEmbeddedContactFields(
|
|
508
|
+
html: unknown,
|
|
509
|
+
vanity: string,
|
|
510
|
+
): LinkedInContactFields | undefined {
|
|
511
|
+
const records = embeddedRecords(html);
|
|
512
|
+
const entities = collectRecords(records).filter(contactEntity);
|
|
513
|
+
if (entities.length < 1) return undefined;
|
|
514
|
+
return projectFields(records, vanity);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function oneUnique<T>(
|
|
518
|
+
values: readonly T[],
|
|
519
|
+
label: string,
|
|
520
|
+
): T | null {
|
|
521
|
+
const unique = [...new Set(values)];
|
|
522
|
+
if (unique.length === 0) return null;
|
|
523
|
+
if (unique.length !== 1) throw new Error(`${label} was ambiguous`);
|
|
524
|
+
return unique[0]!;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function emailAddress(value: unknown, label: string): string {
|
|
528
|
+
const email = boundedText(value, label, 254).toLowerCase();
|
|
529
|
+
if (!EMAIL.test(email)) throw new Error(`${label} is not a reviewed email`);
|
|
530
|
+
return email;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function optionalEmail(value: unknown, label: string): string | null {
|
|
534
|
+
if (value === undefined || value === null || value === "") return null;
|
|
535
|
+
return emailAddress(value, label);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function phoneNumber(value: unknown, label: string): string {
|
|
539
|
+
const phone = boundedText(value, label, 32);
|
|
540
|
+
if (!PHONE.test(phone)) throw new Error(`${label} is not a reviewed phone number`);
|
|
541
|
+
return phone;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function websiteUrl(value: unknown, label: string): string {
|
|
545
|
+
const raw = boundedText(value, label, 2_048);
|
|
546
|
+
let url: URL;
|
|
547
|
+
try {
|
|
548
|
+
url = new URL(raw);
|
|
549
|
+
} catch {
|
|
550
|
+
throw new Error(`${label} must be an absolute URL`);
|
|
551
|
+
}
|
|
552
|
+
if (
|
|
553
|
+
(url.protocol !== "https:" && url.protocol !== "http:")
|
|
554
|
+
|| url.username !== ""
|
|
555
|
+
|| url.password !== ""
|
|
556
|
+
) throw new Error(`${label} must be a safe public HTTP URL`);
|
|
557
|
+
return url.href;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function linkedInProfileHref(value: unknown, expectedVanity: string): string | null {
|
|
561
|
+
if (value === undefined || value === null || value === "") return null;
|
|
562
|
+
const raw = boundedText(value, "LinkedIn contact-info profile link", 2_048);
|
|
563
|
+
let url: URL;
|
|
564
|
+
try {
|
|
565
|
+
url = new URL(raw.startsWith("http") ? raw : `https://${raw}`);
|
|
566
|
+
} catch {
|
|
567
|
+
throw new Error("LinkedIn contact-info profile link must be an absolute URL");
|
|
568
|
+
}
|
|
569
|
+
if (
|
|
570
|
+
url.hostname !== "www.linkedin.com" && url.hostname !== "linkedin.com"
|
|
571
|
+
) throw new Error("LinkedIn contact-info profile link escaped LinkedIn");
|
|
572
|
+
const match = /^\/in\/([A-Za-z0-9][A-Za-z0-9_-]{1,99})\/?$/u.exec(url.pathname);
|
|
573
|
+
if (match?.[1] === undefined) {
|
|
574
|
+
throw new Error("LinkedIn contact-info profile link has an unsupported path");
|
|
575
|
+
}
|
|
576
|
+
const vanity = linkedInPersonalProfilePublicIdentifier(match[1]);
|
|
577
|
+
if (vanity !== expectedVanity) {
|
|
578
|
+
throw new Error("LinkedIn contact-info profile link does not match the requested vanity");
|
|
579
|
+
}
|
|
580
|
+
return `https://www.linkedin.com/in/${vanity}/`;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function padDay(value: number): string {
|
|
584
|
+
return String(value).padStart(2, "0");
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function birthdayValue(value: unknown): string | null {
|
|
588
|
+
if (value === undefined || value === null || value === "") return null;
|
|
589
|
+
if (typeof value === "string") {
|
|
590
|
+
const text = boundedText(value, "LinkedIn contact-info birthday", 16);
|
|
591
|
+
if (!BIRTHDAY.test(text)) throw new Error("LinkedIn contact-info birthday changed format");
|
|
592
|
+
return text;
|
|
593
|
+
}
|
|
594
|
+
if (!isRecord(value)) throw new Error("LinkedIn contact-info birthday changed shape");
|
|
595
|
+
const month = value.month;
|
|
596
|
+
const day = value.day;
|
|
597
|
+
if (!Number.isSafeInteger(month) || (month as number) < 1 || (month as number) > 12) {
|
|
598
|
+
throw new Error("LinkedIn contact-info birthday month is invalid");
|
|
599
|
+
}
|
|
600
|
+
if (!Number.isSafeInteger(day) || (day as number) < 1 || (day as number) > 31) {
|
|
601
|
+
throw new Error("LinkedIn contact-info birthday day is invalid");
|
|
602
|
+
}
|
|
603
|
+
if (value.year === undefined || value.year === null) {
|
|
604
|
+
return `${padDay(month as number)}-${padDay(day as number)}`;
|
|
605
|
+
}
|
|
606
|
+
if (
|
|
607
|
+
!Number.isSafeInteger(value.year)
|
|
608
|
+
|| (value.year as number) < 1900
|
|
609
|
+
|| (value.year as number) > 2100
|
|
610
|
+
) throw new Error("LinkedIn contact-info birthday year is invalid");
|
|
611
|
+
return `${value.year}-${padDay(month as number)}-${padDay(day as number)}`;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function connectedSinceValue(value: unknown): string | null {
|
|
615
|
+
if (value === undefined || value === null || value === "") return null;
|
|
616
|
+
if (typeof value === "number") {
|
|
617
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
618
|
+
throw new Error("LinkedIn contact-info connected-since timestamp is invalid");
|
|
619
|
+
}
|
|
620
|
+
const date = new Date(value);
|
|
621
|
+
if (!Number.isFinite(date.valueOf())) {
|
|
622
|
+
throw new Error("LinkedIn contact-info connected-since timestamp is invalid");
|
|
623
|
+
}
|
|
624
|
+
return date.toISOString().slice(0, 10);
|
|
625
|
+
}
|
|
626
|
+
const text = boundedText(value, "LinkedIn contact-info connected-since", 32);
|
|
627
|
+
if (/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/u.test(text)) return text;
|
|
628
|
+
const match = CONNECTED_DISPLAY.exec(text);
|
|
629
|
+
if (match === null) {
|
|
630
|
+
throw new Error("LinkedIn contact-info connected-since changed format");
|
|
631
|
+
}
|
|
632
|
+
const month = MONTHS[match[1] as keyof typeof MONTHS];
|
|
633
|
+
const day = padDay(Number(match[2]));
|
|
634
|
+
return `${match[3]}-${month}-${day}`;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function phonesFromUnknown(value: unknown): readonly string[] {
|
|
638
|
+
if (value === undefined || value === null) return Object.freeze([]);
|
|
639
|
+
if (!Array.isArray(value) || value.length > MAX_PHONES) {
|
|
640
|
+
throw new Error("LinkedIn contact-info phone list exceeded its reviewed bound");
|
|
641
|
+
}
|
|
642
|
+
return Object.freeze(value.map((item, index) => {
|
|
643
|
+
if (typeof item === "string") return phoneNumber(item, `LinkedIn contact-info phone[${index}]`);
|
|
644
|
+
if (!isRecord(item)) throw new Error("LinkedIn contact-info phone changed shape");
|
|
645
|
+
return phoneNumber(
|
|
646
|
+
item.number ?? item.phoneNumber ?? item.value,
|
|
647
|
+
`LinkedIn contact-info phone[${index}]`,
|
|
648
|
+
);
|
|
649
|
+
}));
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function websitesFromUnknown(value: unknown): readonly string[] {
|
|
653
|
+
if (value === undefined || value === null) return Object.freeze([]);
|
|
654
|
+
if (!Array.isArray(value) || value.length > MAX_WEBSITES) {
|
|
655
|
+
throw new Error("LinkedIn contact-info website list exceeded its reviewed bound");
|
|
656
|
+
}
|
|
657
|
+
return Object.freeze(value.map((item, index) => {
|
|
658
|
+
if (typeof item === "string") return websiteUrl(item, `LinkedIn contact-info website[${index}]`);
|
|
659
|
+
if (!isRecord(item)) throw new Error("LinkedIn contact-info website changed shape");
|
|
660
|
+
return websiteUrl(item.url ?? item.value, `LinkedIn contact-info website[${index}]`);
|
|
661
|
+
}));
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function labelKey(value: unknown): string {
|
|
665
|
+
return boundedText(value, "LinkedIn contact-info field label", 64).trim().toLowerCase();
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function labeledFields(value: unknown): JsonRecord | null {
|
|
669
|
+
if (!Array.isArray(value) || value.length > 32) return null;
|
|
670
|
+
const collected: {
|
|
671
|
+
email?: string;
|
|
672
|
+
profileUrl?: string;
|
|
673
|
+
connectedSince?: string;
|
|
674
|
+
phones: string[];
|
|
675
|
+
websites: string[];
|
|
676
|
+
birthday?: string;
|
|
677
|
+
} = { phones: [], websites: [] };
|
|
678
|
+
for (const item of value) {
|
|
679
|
+
if (!isRecord(item)) continue;
|
|
680
|
+
const label = item.label ?? item.title ?? item.heading;
|
|
681
|
+
const raw = item.value ?? item.text ?? item.href;
|
|
682
|
+
if (typeof label !== "string") continue;
|
|
683
|
+
const key = labelKey(label);
|
|
684
|
+
if (key === "email") {
|
|
685
|
+
const email = optionalEmail(raw, "LinkedIn contact-info Email");
|
|
686
|
+
if (email !== null) collected.email = email;
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (key === "connected since") {
|
|
690
|
+
const connected = connectedSinceValue(raw);
|
|
691
|
+
if (connected !== null) collected.connectedSince = connected;
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
if (key === "phone" || key === "phone number") {
|
|
695
|
+
collected.phones.push(phoneNumber(raw, "LinkedIn contact-info Phone"));
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
if (key === "website" || key === "websites") {
|
|
699
|
+
collected.websites.push(websiteUrl(raw, "LinkedIn contact-info Website"));
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
if (key === "birthday") {
|
|
703
|
+
const birthday = birthdayValue(raw);
|
|
704
|
+
if (birthday !== null) collected.birthday = birthday;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (
|
|
708
|
+
collected.email === undefined
|
|
709
|
+
&& collected.profileUrl === undefined
|
|
710
|
+
&& collected.connectedSince === undefined
|
|
711
|
+
&& collected.phones.length === 0
|
|
712
|
+
&& collected.websites.length === 0
|
|
713
|
+
&& collected.birthday === undefined
|
|
714
|
+
) return null;
|
|
715
|
+
return Object.freeze({
|
|
716
|
+
emailAddress: collected.email ?? null,
|
|
717
|
+
connectedAt: collected.connectedSince ?? null,
|
|
718
|
+
phoneNumbers: collected.phones,
|
|
719
|
+
websites: collected.websites,
|
|
720
|
+
birthDateOn: collected.birthday ?? null,
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
function contactEntity(record: JsonRecord): boolean {
|
|
725
|
+
const type = typeName(record.$type);
|
|
726
|
+
return type.endsWith(CONTACT_TYPE_SUFFIX)
|
|
727
|
+
|| record.emailAddress !== undefined
|
|
728
|
+
|| record.connectedAt !== undefined
|
|
729
|
+
|| record.phoneNumbers !== undefined
|
|
730
|
+
|| record.websites !== undefined
|
|
731
|
+
|| record.birthDateOn !== undefined;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function collectRecords(value: unknown): readonly JsonRecord[] {
|
|
735
|
+
const records: JsonRecord[] = [];
|
|
736
|
+
const stack = [{ value, depth: 0 }];
|
|
737
|
+
let nodes = 0;
|
|
738
|
+
while (stack.length > 0) {
|
|
739
|
+
const next = stack.pop()!;
|
|
740
|
+
nodes += 1;
|
|
741
|
+
if (nodes > MAX_WALK_NODES || next.depth > 32) {
|
|
742
|
+
throw new Error("LinkedIn contact-info payload exceeded its traversal bound");
|
|
743
|
+
}
|
|
744
|
+
if (Array.isArray(next.value)) {
|
|
745
|
+
if (next.value.length > 20_000) {
|
|
746
|
+
throw new Error("LinkedIn contact-info payload array exceeded its reviewed bound");
|
|
747
|
+
}
|
|
748
|
+
const labeled = labeledFields(next.value);
|
|
749
|
+
if (labeled !== null) records.push(labeled);
|
|
750
|
+
for (const item of next.value) stack.push({ value: item, depth: next.depth + 1 });
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
if (!isRecord(next.value)) continue;
|
|
754
|
+
records.push(next.value);
|
|
755
|
+
const nestedLabels = labeledFields(next.value.fields ?? next.value.items ?? next.value.rows);
|
|
756
|
+
if (nestedLabels !== null) records.push(nestedLabels);
|
|
757
|
+
for (const item of Object.values(next.value)) {
|
|
758
|
+
stack.push({ value: item, depth: next.depth + 1 });
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
return Object.freeze(records);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function projectFields(
|
|
765
|
+
payload: unknown,
|
|
766
|
+
vanity: string,
|
|
767
|
+
): LinkedInContactFields {
|
|
768
|
+
if (!isRecord(payload) && !Array.isArray(payload)) {
|
|
769
|
+
throw new Error("LinkedIn contact-info payload must be a JSON object or array");
|
|
770
|
+
}
|
|
771
|
+
const records = collectRecords(payload);
|
|
772
|
+
const entities = records.filter(contactEntity);
|
|
773
|
+
if (entities.length < 1) {
|
|
774
|
+
throw new Error("LinkedIn contact-info payload omitted its contact fields");
|
|
775
|
+
}
|
|
776
|
+
const emails = entities
|
|
777
|
+
.map((record) => optionalEmail(record.emailAddress ?? record.email, "LinkedIn contact-info email"))
|
|
778
|
+
.filter((value): value is string => value !== null);
|
|
779
|
+
const profileUrls = entities
|
|
780
|
+
.map((record) => linkedInProfileHref(
|
|
781
|
+
record.profileUrl ?? record.vanityName ?? record.publicIdentifier,
|
|
782
|
+
vanity,
|
|
783
|
+
))
|
|
784
|
+
.filter((value): value is string => value !== null);
|
|
785
|
+
const connected = entities
|
|
786
|
+
.map((record) => connectedSinceValue(record.connectedAt ?? record.connectedSince))
|
|
787
|
+
.filter((value): value is string => value !== null);
|
|
788
|
+
const birthdays = entities
|
|
789
|
+
.map((record) => birthdayValue(record.birthDateOn ?? record.birthday))
|
|
790
|
+
.filter((value): value is string => value !== null);
|
|
791
|
+
const phones = entities.flatMap((record) => [...phonesFromUnknown(record.phoneNumbers ?? record.phones)]);
|
|
792
|
+
const websites = entities.flatMap((record) => [...websitesFromUnknown(record.websites)]);
|
|
793
|
+
return Object.freeze({
|
|
794
|
+
email: oneUnique(emails, "LinkedIn contact-info email"),
|
|
795
|
+
profileUrl: oneUnique(profileUrls, "LinkedIn contact-info profile link") ?? `https://www.linkedin.com/in/${vanity}/`,
|
|
796
|
+
connectedSince: oneUnique(connected, "LinkedIn contact-info connected-since"),
|
|
797
|
+
phones: Object.freeze([...new Set(phones)]),
|
|
798
|
+
websites: Object.freeze([...new Set(websites)]),
|
|
799
|
+
birthday: oneUnique(birthdays, "LinkedIn contact-info birthday"),
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
export function projectLinkedInContactInfo(input: {
|
|
804
|
+
readonly profileHtml: unknown;
|
|
805
|
+
readonly contactPayload: unknown;
|
|
806
|
+
readonly profileUrl: unknown;
|
|
807
|
+
readonly expectedViewerSubject: unknown;
|
|
808
|
+
readonly observedAt: string;
|
|
809
|
+
}): LinkedInContactInfo {
|
|
810
|
+
const binding = projectLinkedInProfileContactBinding({
|
|
811
|
+
profileHtml: input.profileHtml,
|
|
812
|
+
profileUrl: input.profileUrl,
|
|
813
|
+
expectedViewerSubject: input.expectedViewerSubject,
|
|
814
|
+
});
|
|
815
|
+
const contact = projectFields(input.contactPayload, binding.vanity);
|
|
816
|
+
const hasAny = contact.email !== null
|
|
817
|
+
|| contact.connectedSince !== null
|
|
818
|
+
|| contact.phones.length > 0
|
|
819
|
+
|| contact.websites.length > 0
|
|
820
|
+
|| contact.birthday !== null;
|
|
821
|
+
return Object.freeze({
|
|
822
|
+
schemaVersion: 1,
|
|
823
|
+
provider: "linkedin",
|
|
824
|
+
profile: binding,
|
|
825
|
+
viewer: Object.freeze({ subject: viewerSubject(input.expectedViewerSubject) }),
|
|
826
|
+
observedAt: observationTime(input.observedAt),
|
|
827
|
+
completeness: hasAny ? "complete" : "partial",
|
|
828
|
+
contact,
|
|
829
|
+
});
|
|
830
|
+
}
|