@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,913 @@
|
|
|
1
|
+
import { Blob } from "node:buffer";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { constants } from "node:fs";
|
|
4
|
+
import { open } from "node:fs/promises";
|
|
5
|
+
import { types as nodeTypes } from "node:util";
|
|
6
|
+
|
|
7
|
+
import type { BrowserFileResolver } from "../browser";
|
|
8
|
+
import { canonicalJson } from "../canonical-json";
|
|
9
|
+
import type { FileInputValue, OperationInput } from "../model";
|
|
10
|
+
import type { WebSessionOperationDeadline } from "../web-session-execution";
|
|
11
|
+
import {
|
|
12
|
+
isoBmffMp4VideoMetadata,
|
|
13
|
+
isoBmffVideoDimensions,
|
|
14
|
+
} from "./iso-bmff";
|
|
15
|
+
|
|
16
|
+
const MAX_INSTAGRAM_VIDEO_BYTES = 128 * 1024 * 1024;
|
|
17
|
+
const MAX_INSTAGRAM_THUMBNAIL_BYTES = 8 * 1024 * 1024;
|
|
18
|
+
const INSTAGRAM_MEDIA_ID_PATTERN = /^[1-9][0-9]{0,31}(?:_[1-9][0-9]{0,31})?$/u;
|
|
19
|
+
const INSTAGRAM_SHORTCODE_PATTERN = /^[A-Za-z0-9_-]{1,64}$/u;
|
|
20
|
+
const INSTAGRAM_VIEWER_ID_PATTERN = /^[1-9][0-9]{0,31}$/u;
|
|
21
|
+
const INSTAGRAM_ORIGIN = "https://www.instagram.com";
|
|
22
|
+
const INSTAGRAM_VIDEO_PUBLISH_BINDING_KEYS = Object.freeze([
|
|
23
|
+
"audience",
|
|
24
|
+
"byteLength",
|
|
25
|
+
"bytes",
|
|
26
|
+
"caption",
|
|
27
|
+
"durationMilliseconds",
|
|
28
|
+
"height",
|
|
29
|
+
"mediaSha256",
|
|
30
|
+
"mediaType",
|
|
31
|
+
"thumbnailByteLength",
|
|
32
|
+
"thumbnailBytes",
|
|
33
|
+
"thumbnailHeight",
|
|
34
|
+
"thumbnailMediaType",
|
|
35
|
+
"thumbnailSha256",
|
|
36
|
+
"thumbnailWidth",
|
|
37
|
+
"width",
|
|
38
|
+
] as const);
|
|
39
|
+
const TYPED_ARRAY_PROTOTYPE = Object.getPrototypeOf(
|
|
40
|
+
Uint8Array.prototype,
|
|
41
|
+
) as object;
|
|
42
|
+
const TYPED_ARRAY_BUFFER_GETTER = Object.getOwnPropertyDescriptor(
|
|
43
|
+
TYPED_ARRAY_PROTOTYPE,
|
|
44
|
+
"buffer",
|
|
45
|
+
)?.get;
|
|
46
|
+
const TYPED_ARRAY_BYTE_LENGTH_GETTER = Object.getOwnPropertyDescriptor(
|
|
47
|
+
TYPED_ARRAY_PROTOTYPE,
|
|
48
|
+
"byteLength",
|
|
49
|
+
)?.get;
|
|
50
|
+
const MP4_COMPATIBLE_BRANDS = Object.freeze(new Set([
|
|
51
|
+
"M4V ",
|
|
52
|
+
"MSNV",
|
|
53
|
+
"avc1",
|
|
54
|
+
"iso2",
|
|
55
|
+
"isom",
|
|
56
|
+
"mp41",
|
|
57
|
+
"mp42",
|
|
58
|
+
]));
|
|
59
|
+
|
|
60
|
+
export type InstagramVideoPublishPlan = Readonly<{
|
|
61
|
+
audience: "default";
|
|
62
|
+
caption: string;
|
|
63
|
+
media: FileInputValue;
|
|
64
|
+
thumbnail: FileInputValue;
|
|
65
|
+
}>;
|
|
66
|
+
|
|
67
|
+
export type InstagramBoundVideoPublish = Readonly<{
|
|
68
|
+
audience: "default";
|
|
69
|
+
bytes: Uint8Array<ArrayBuffer>;
|
|
70
|
+
byteLength: number;
|
|
71
|
+
caption: string;
|
|
72
|
+
durationMilliseconds: number;
|
|
73
|
+
height: number;
|
|
74
|
+
mediaType: "video/mp4";
|
|
75
|
+
mediaSha256: string;
|
|
76
|
+
thumbnailByteLength: number;
|
|
77
|
+
thumbnailBytes: Uint8Array<ArrayBuffer>;
|
|
78
|
+
thumbnailHeight: number;
|
|
79
|
+
thumbnailMediaType: "image/jpeg";
|
|
80
|
+
thumbnailSha256: string;
|
|
81
|
+
thumbnailWidth: number;
|
|
82
|
+
width: number;
|
|
83
|
+
}>;
|
|
84
|
+
|
|
85
|
+
export type InstagramVideoPublishDispatchSnapshot = Readonly<{
|
|
86
|
+
audience: "default";
|
|
87
|
+
body: Blob;
|
|
88
|
+
byteLength: number;
|
|
89
|
+
caption: string;
|
|
90
|
+
durationMilliseconds: number;
|
|
91
|
+
height: number;
|
|
92
|
+
mediaType: "video/mp4";
|
|
93
|
+
mediaSha256: string;
|
|
94
|
+
thumbnailBody: Blob;
|
|
95
|
+
thumbnailByteLength: number;
|
|
96
|
+
thumbnailHeight: number;
|
|
97
|
+
thumbnailMediaType: "image/jpeg";
|
|
98
|
+
thumbnailSha256: string;
|
|
99
|
+
thumbnailWidth: number;
|
|
100
|
+
width: number;
|
|
101
|
+
}>;
|
|
102
|
+
|
|
103
|
+
export type InstagramAuthoredPostDeleteInput = Readonly<{
|
|
104
|
+
expectedCaption: string;
|
|
105
|
+
expectedMediaKind: "video";
|
|
106
|
+
mediaId: string;
|
|
107
|
+
}>;
|
|
108
|
+
|
|
109
|
+
export type InstagramVideoAcceptedTarget = Readonly<{
|
|
110
|
+
code: string;
|
|
111
|
+
mediaId: string;
|
|
112
|
+
url: string;
|
|
113
|
+
}>;
|
|
114
|
+
|
|
115
|
+
export type InstagramVideoReadbackExpectation = Readonly<{
|
|
116
|
+
expectedCaption: string;
|
|
117
|
+
expectedCode?: string;
|
|
118
|
+
mediaId: string;
|
|
119
|
+
viewerId: string;
|
|
120
|
+
}>;
|
|
121
|
+
|
|
122
|
+
export type InstagramVideoConfigureExpectation = Readonly<{
|
|
123
|
+
caption: string;
|
|
124
|
+
uploadId: string;
|
|
125
|
+
viewerId: string;
|
|
126
|
+
}>;
|
|
127
|
+
|
|
128
|
+
/** Exact remaining boundary after the authorized disposable lifecycle. */
|
|
129
|
+
export const INSTAGRAM_VIDEO_CAPTURE_BLOCKERS = Object.freeze({
|
|
130
|
+
"media.publish":
|
|
131
|
+
"the observed first configure response was 202 without an accepted target; neither a safe repeated configure POST nor an independent upload-ID reconciliation read is proven",
|
|
132
|
+
} as const);
|
|
133
|
+
|
|
134
|
+
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
135
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function exactInputKeys(
|
|
139
|
+
input: OperationInput,
|
|
140
|
+
required: readonly string[],
|
|
141
|
+
label: string,
|
|
142
|
+
): void {
|
|
143
|
+
const permitted = new Set(required);
|
|
144
|
+
if (Object.keys(input).some((key) => !permitted.has(key))) {
|
|
145
|
+
throw new Error(`${label} contained an unsupported input field`);
|
|
146
|
+
}
|
|
147
|
+
if (required.some((key) => !Object.hasOwn(input, key))) {
|
|
148
|
+
throw new Error(`${label} omitted a required input field`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function boundedCaption(value: unknown, label: string): string {
|
|
153
|
+
if (
|
|
154
|
+
typeof value !== "string"
|
|
155
|
+
|| value.length < 1
|
|
156
|
+
|| value.length > 1_000
|
|
157
|
+
|| /[\0\r]/u.test(value)
|
|
158
|
+
) throw new Error(`${label} must be 1 to 1000 bounded UTF-16 code units`);
|
|
159
|
+
return value;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function instagramMediaId(value: unknown, label: string): string {
|
|
163
|
+
if (typeof value !== "string" || !INSTAGRAM_MEDIA_ID_PATTERN.test(value)) {
|
|
164
|
+
throw new Error(`${label} must be one exact canonical Instagram media ID`);
|
|
165
|
+
}
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function instagramShortcode(value: unknown, label: string): string {
|
|
170
|
+
if (typeof value !== "string" || !INSTAGRAM_SHORTCODE_PATTERN.test(value)) {
|
|
171
|
+
throw new Error(`${label} must be one exact Instagram shortcode`);
|
|
172
|
+
}
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function instagramVideoPermalink(code: string): string {
|
|
177
|
+
return `${INSTAGRAM_ORIGIN}/p/${code}/`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function exactNormalizedKeys(
|
|
181
|
+
value: Readonly<Record<string, unknown>>,
|
|
182
|
+
expected: readonly string[],
|
|
183
|
+
label: string,
|
|
184
|
+
): void {
|
|
185
|
+
if (Object.keys(value).sort().join(",") !== [...expected].sort().join(",")) {
|
|
186
|
+
throw new Error(`${label} changed its bounded normalized shape`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function boundedForeignRecord(
|
|
191
|
+
value: unknown,
|
|
192
|
+
label: string,
|
|
193
|
+
maximumKeys: number,
|
|
194
|
+
): Readonly<Record<string, unknown>> {
|
|
195
|
+
if (!isRecord(value) || Object.keys(value).length > maximumKeys) {
|
|
196
|
+
throw new Error(`${label} must be one bounded object`);
|
|
197
|
+
}
|
|
198
|
+
return value;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function exactInstagramVideoPublishBinding(
|
|
202
|
+
value: unknown,
|
|
203
|
+
): Readonly<Record<string, unknown>> {
|
|
204
|
+
if (
|
|
205
|
+
typeof value !== "object"
|
|
206
|
+
|| value === null
|
|
207
|
+
) throw new Error("Instagram video binding must be one exact object");
|
|
208
|
+
if (nodeTypes.isProxy(value)) {
|
|
209
|
+
throw new Error("Instagram video binding must not be a proxy");
|
|
210
|
+
}
|
|
211
|
+
if (Array.isArray(value)) {
|
|
212
|
+
throw new Error("Instagram video binding must be one exact object");
|
|
213
|
+
}
|
|
214
|
+
const prototype: unknown = Object.getPrototypeOf(value);
|
|
215
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
216
|
+
throw new Error("Instagram video binding must use a plain prototype");
|
|
217
|
+
}
|
|
218
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
219
|
+
const ownKeys = Reflect.ownKeys(descriptors);
|
|
220
|
+
if (
|
|
221
|
+
ownKeys.length !== INSTAGRAM_VIDEO_PUBLISH_BINDING_KEYS.length
|
|
222
|
+
|| ownKeys.some((key) => typeof key !== "string")
|
|
223
|
+
|| (ownKeys as string[]).sort().join(",")
|
|
224
|
+
!== [...INSTAGRAM_VIDEO_PUBLISH_BINDING_KEYS].sort().join(",")
|
|
225
|
+
) throw new Error("Instagram video binding contained unsupported fields");
|
|
226
|
+
const snapshot = Object.create(null) as Record<string, unknown>;
|
|
227
|
+
for (const key of INSTAGRAM_VIDEO_PUBLISH_BINDING_KEYS) {
|
|
228
|
+
const descriptor = descriptors[key];
|
|
229
|
+
if (
|
|
230
|
+
descriptor === undefined
|
|
231
|
+
|| !descriptor.enumerable
|
|
232
|
+
|| !("value" in descriptor)
|
|
233
|
+
) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
"Instagram video binding must contain only enumerable data properties",
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
snapshot[key] = descriptor.value;
|
|
239
|
+
}
|
|
240
|
+
return Object.freeze(snapshot);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function snapshotInstagramBytes(
|
|
244
|
+
value: unknown,
|
|
245
|
+
maximumBytes: number,
|
|
246
|
+
label: "MP4" | "JPEG thumbnail",
|
|
247
|
+
): Uint8Array<ArrayBuffer> {
|
|
248
|
+
if (
|
|
249
|
+
typeof value !== "object"
|
|
250
|
+
|| value === null
|
|
251
|
+
|| nodeTypes.isProxy(value)
|
|
252
|
+
|| !(value instanceof Uint8Array)
|
|
253
|
+
|| Object.getPrototypeOf(value) !== Uint8Array.prototype
|
|
254
|
+
|| TYPED_ARRAY_BUFFER_GETTER === undefined
|
|
255
|
+
|| TYPED_ARRAY_BYTE_LENGTH_GETTER === undefined
|
|
256
|
+
) throw new Error(`Instagram video binding must contain one bounded ${label}`);
|
|
257
|
+
let buffer: unknown;
|
|
258
|
+
let byteLength: unknown;
|
|
259
|
+
try {
|
|
260
|
+
buffer = TYPED_ARRAY_BUFFER_GETTER.call(value) as unknown;
|
|
261
|
+
byteLength = TYPED_ARRAY_BYTE_LENGTH_GETTER.call(value) as unknown;
|
|
262
|
+
} catch {
|
|
263
|
+
throw new Error(`Instagram video binding must contain one bounded ${label}`);
|
|
264
|
+
}
|
|
265
|
+
if (
|
|
266
|
+
typeof byteLength !== "number"
|
|
267
|
+
|| !Number.isSafeInteger(byteLength)
|
|
268
|
+
|| byteLength < (label === "MP4" ? 24 : 16)
|
|
269
|
+
|| byteLength > maximumBytes
|
|
270
|
+
|| nodeTypes.isSharedArrayBuffer(buffer)
|
|
271
|
+
) throw new Error(`Instagram video binding must contain one bounded ${label}`);
|
|
272
|
+
const bytes = new Uint8Array(byteLength);
|
|
273
|
+
try {
|
|
274
|
+
Uint8Array.prototype.set.call(bytes, value);
|
|
275
|
+
} catch {
|
|
276
|
+
throw new Error(`Instagram video binding must contain one bounded ${label}`);
|
|
277
|
+
}
|
|
278
|
+
return bytes;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function instagramJpegDimensions(bytes: Uint8Array): Readonly<{
|
|
282
|
+
height: number;
|
|
283
|
+
width: number;
|
|
284
|
+
}> {
|
|
285
|
+
if (
|
|
286
|
+
bytes.byteLength < 16
|
|
287
|
+
|| bytes[0] !== 0xff
|
|
288
|
+
|| bytes[1] !== 0xd8
|
|
289
|
+
|| bytes.at(-2) !== 0xff
|
|
290
|
+
|| bytes.at(-1) !== 0xd9
|
|
291
|
+
) throw new Error("Instagram video thumbnail must be one complete JPEG");
|
|
292
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
293
|
+
const frameMarkers = new Set([
|
|
294
|
+
0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7,
|
|
295
|
+
0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf,
|
|
296
|
+
]);
|
|
297
|
+
let offset = 2;
|
|
298
|
+
let frame: Readonly<{ height: number; width: number }> | null = null;
|
|
299
|
+
while (offset < bytes.byteLength - 2) {
|
|
300
|
+
if (bytes[offset] !== 0xff) {
|
|
301
|
+
throw new Error("Instagram video thumbnail JPEG marker stream changed shape");
|
|
302
|
+
}
|
|
303
|
+
while (bytes[offset] === 0xff) offset += 1;
|
|
304
|
+
const marker = bytes[offset];
|
|
305
|
+
offset += 1;
|
|
306
|
+
if (marker === undefined || marker === 0x00) {
|
|
307
|
+
throw new Error("Instagram video thumbnail JPEG marker stream changed shape");
|
|
308
|
+
}
|
|
309
|
+
if (marker === 0xd9) break;
|
|
310
|
+
if (marker === 0xda) {
|
|
311
|
+
if (frame === null) {
|
|
312
|
+
throw new Error("Instagram video thumbnail JPEG omitted its frame dimensions");
|
|
313
|
+
}
|
|
314
|
+
return frame;
|
|
315
|
+
}
|
|
316
|
+
if (marker === 0xd8 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (offset + 2 > bytes.byteLength) {
|
|
320
|
+
throw new Error("Instagram video thumbnail JPEG segment exceeded its bytes");
|
|
321
|
+
}
|
|
322
|
+
const length = view.getUint16(offset);
|
|
323
|
+
if (length < 2 || offset + length > bytes.byteLength) {
|
|
324
|
+
throw new Error("Instagram video thumbnail JPEG segment exceeded its bytes");
|
|
325
|
+
}
|
|
326
|
+
if (frameMarkers.has(marker)) {
|
|
327
|
+
if (frame !== null || length < 8 || bytes[offset + 2] !== 8) {
|
|
328
|
+
throw new Error("Instagram video thumbnail JPEG frame changed shape");
|
|
329
|
+
}
|
|
330
|
+
const height = view.getUint16(offset + 3);
|
|
331
|
+
const width = view.getUint16(offset + 5);
|
|
332
|
+
if (height < 1 || height > 20_000 || width < 1 || width > 20_000) {
|
|
333
|
+
throw new Error("Instagram video thumbnail JPEG dimensions are outside the reviewed bound");
|
|
334
|
+
}
|
|
335
|
+
frame = Object.freeze({ height, width });
|
|
336
|
+
}
|
|
337
|
+
offset += length;
|
|
338
|
+
}
|
|
339
|
+
if (frame === null) {
|
|
340
|
+
throw new Error("Instagram video thumbnail JPEG omitted its frame dimensions");
|
|
341
|
+
}
|
|
342
|
+
return frame;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function planBoundFile(value: unknown): FileInputValue {
|
|
346
|
+
if (
|
|
347
|
+
!isRecord(value)
|
|
348
|
+
|| Object.keys(value).sort().join(",") !== "kind,reference"
|
|
349
|
+
|| value.kind !== "file"
|
|
350
|
+
|| typeof value.reference !== "string"
|
|
351
|
+
|| value.reference.length < 1
|
|
352
|
+
|| value.reference.length > 4_096
|
|
353
|
+
|| /[\0\r\n]/u.test(value.reference)
|
|
354
|
+
) throw new Error("input.media must be one exact plan-bound file");
|
|
355
|
+
return Object.freeze({ kind: "file", reference: value.reference });
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function requireMp4FileType(bytes: Uint8Array): void {
|
|
359
|
+
if (bytes.byteLength < 24) {
|
|
360
|
+
throw new Error("Instagram video must be one complete MP4 file");
|
|
361
|
+
}
|
|
362
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
363
|
+
const fileTypeBytes = view.getUint32(0);
|
|
364
|
+
if (
|
|
365
|
+
fileTypeBytes < 16
|
|
366
|
+
|| fileTypeBytes > bytes.byteLength
|
|
367
|
+
|| String.fromCharCode(...bytes.subarray(4, 8)) !== "ftyp"
|
|
368
|
+
|| (fileTypeBytes - 16) % 4 !== 0
|
|
369
|
+
) throw new Error("Instagram video must begin with one bounded MP4 file-type box");
|
|
370
|
+
const majorBrand = String.fromCharCode(...bytes.subarray(8, 12));
|
|
371
|
+
if (majorBrand === "qt ") {
|
|
372
|
+
throw new Error("Instagram video file-type box is not MP4-compatible");
|
|
373
|
+
}
|
|
374
|
+
const brands: string[] = [];
|
|
375
|
+
for (let offset = 8; offset < fileTypeBytes; offset += offset === 8 ? 8 : 4) {
|
|
376
|
+
brands.push(String.fromCharCode(...bytes.subarray(offset, offset + 4)));
|
|
377
|
+
}
|
|
378
|
+
if (!brands.some((brand) => MP4_COMPATIBLE_BRANDS.has(brand))) {
|
|
379
|
+
throw new Error("Instagram video file-type box is not MP4-compatible");
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function instagramVideoSha256(bytes: Uint8Array): string {
|
|
384
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function instagramVideoMetadata(bytes: Uint8Array): Readonly<{
|
|
388
|
+
durationMilliseconds: number;
|
|
389
|
+
height: number;
|
|
390
|
+
width: number;
|
|
391
|
+
}> {
|
|
392
|
+
requireMp4FileType(bytes);
|
|
393
|
+
const dimensions = isoBmffVideoDimensions(bytes, "Instagram video");
|
|
394
|
+
const metadata = isoBmffMp4VideoMetadata(
|
|
395
|
+
bytes,
|
|
396
|
+
"Instagram video",
|
|
397
|
+
Object.freeze({
|
|
398
|
+
compatibleBrands: Object.freeze([...MP4_COMPATIBLE_BRANDS]),
|
|
399
|
+
rejectedMajorBrands: Object.freeze(["qt "]),
|
|
400
|
+
}),
|
|
401
|
+
);
|
|
402
|
+
const durationMilliseconds = Math.floor(metadata.durationSeconds * 1_000);
|
|
403
|
+
if (
|
|
404
|
+
metadata.durationSeconds < 0.001
|
|
405
|
+
|| metadata.durationSeconds > 86_400
|
|
406
|
+
|| !Number.isSafeInteger(durationMilliseconds)
|
|
407
|
+
|| durationMilliseconds < 1
|
|
408
|
+
|| durationMilliseconds > 86_400_000
|
|
409
|
+
|| metadata.height !== dimensions.height
|
|
410
|
+
|| metadata.width !== dimensions.width
|
|
411
|
+
) throw new Error("Instagram video duration is outside the reviewed bound");
|
|
412
|
+
return Object.freeze({ durationMilliseconds, ...dimensions });
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function stableInstagramPublishFile(
|
|
416
|
+
path: string,
|
|
417
|
+
maximumBytes: number,
|
|
418
|
+
label: "video" | "video thumbnail",
|
|
419
|
+
operationDeadline?: WebSessionOperationDeadline,
|
|
420
|
+
): Promise<Uint8Array<ArrayBuffer>> {
|
|
421
|
+
const noFollow = "O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0;
|
|
422
|
+
const handle = operationDeadline === undefined
|
|
423
|
+
? await open(path, constants.O_RDONLY | noFollow)
|
|
424
|
+
: await operationDeadline.run(
|
|
425
|
+
() => open(path, constants.O_RDONLY | noFollow),
|
|
426
|
+
"authenticated web operation deadline",
|
|
427
|
+
);
|
|
428
|
+
try {
|
|
429
|
+
const before = operationDeadline === undefined
|
|
430
|
+
? await handle.stat()
|
|
431
|
+
: await operationDeadline.run(
|
|
432
|
+
() => handle.stat(),
|
|
433
|
+
"authenticated web operation deadline",
|
|
434
|
+
);
|
|
435
|
+
if (!before.isFile() || before.size < 16 || before.size > maximumBytes) {
|
|
436
|
+
throw new Error(
|
|
437
|
+
`Instagram ${label} must be a regular file within its reviewed in-memory bound`,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
const fileBytes = operationDeadline === undefined
|
|
441
|
+
? await handle.readFile()
|
|
442
|
+
: await operationDeadline.run(
|
|
443
|
+
() => handle.readFile(),
|
|
444
|
+
"authenticated web operation deadline",
|
|
445
|
+
);
|
|
446
|
+
const after = operationDeadline === undefined
|
|
447
|
+
? await handle.stat()
|
|
448
|
+
: await operationDeadline.run(
|
|
449
|
+
() => handle.stat(),
|
|
450
|
+
"authenticated web operation deadline",
|
|
451
|
+
);
|
|
452
|
+
if (
|
|
453
|
+
before.dev !== after.dev
|
|
454
|
+
|| before.ino !== after.ino
|
|
455
|
+
|| before.size !== after.size
|
|
456
|
+
|| before.mtimeMs !== after.mtimeMs
|
|
457
|
+
|| before.ctimeMs !== after.ctimeMs
|
|
458
|
+
|| fileBytes.byteLength !== before.size
|
|
459
|
+
) throw new Error(`Instagram ${label} changed while it was materialized`);
|
|
460
|
+
return new Uint8Array(fileBytes);
|
|
461
|
+
} finally {
|
|
462
|
+
await handle.close();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Validate the complete capture-neutral publication plan before any attachment
|
|
468
|
+
* path is resolved. Consumer-web request fields remain intentionally absent.
|
|
469
|
+
*/
|
|
470
|
+
export function prepareInstagramVideoPublishInput(
|
|
471
|
+
input: OperationInput,
|
|
472
|
+
): InstagramVideoPublishPlan {
|
|
473
|
+
exactInputKeys(
|
|
474
|
+
input,
|
|
475
|
+
["audience", "caption", "media", "thumbnail"],
|
|
476
|
+
"Instagram video publishing",
|
|
477
|
+
);
|
|
478
|
+
if (input.audience !== "default") {
|
|
479
|
+
throw new Error("input.audience must be the exact default Instagram audience");
|
|
480
|
+
}
|
|
481
|
+
return Object.freeze({
|
|
482
|
+
audience: "default" as const,
|
|
483
|
+
caption: boundedCaption(input.caption, "input.caption"),
|
|
484
|
+
media: planBoundFile(input.media),
|
|
485
|
+
thumbnail: planBoundFile(input.thumbnail),
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Resolve and bind one exact stable MP4. This local preflight performs no
|
|
491
|
+
* authenticated request and supplies no consumer-web upload implementation.
|
|
492
|
+
*/
|
|
493
|
+
export async function materializeInstagramVideoPublishInput(
|
|
494
|
+
input: OperationInput,
|
|
495
|
+
fileResolver: BrowserFileResolver | undefined,
|
|
496
|
+
operationDeadline?: WebSessionOperationDeadline,
|
|
497
|
+
): Promise<InstagramBoundVideoPublish> {
|
|
498
|
+
const plan = prepareInstagramVideoPublishInput(input);
|
|
499
|
+
if (fileResolver === undefined) {
|
|
500
|
+
throw new Error("Instagram video upload requires the plan-bound file resolver");
|
|
501
|
+
}
|
|
502
|
+
const resolve = () => fileResolver([plan.media, plan.thumbnail]);
|
|
503
|
+
const paths = operationDeadline === undefined
|
|
504
|
+
? await resolve()
|
|
505
|
+
: await operationDeadline.run(resolve, "authenticated web operation deadline");
|
|
506
|
+
operationDeadline?.throwIfUnavailable("authenticated web operation deadline");
|
|
507
|
+
if (
|
|
508
|
+
paths.length !== 2
|
|
509
|
+
|| typeof paths[0] !== "string"
|
|
510
|
+
|| typeof paths[1] !== "string"
|
|
511
|
+
) {
|
|
512
|
+
throw new Error(
|
|
513
|
+
"Instagram file resolver did not return the exact video and JPEG thumbnail paths",
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
const bytes = await stableInstagramPublishFile(
|
|
517
|
+
paths[0],
|
|
518
|
+
MAX_INSTAGRAM_VIDEO_BYTES,
|
|
519
|
+
"video",
|
|
520
|
+
operationDeadline,
|
|
521
|
+
);
|
|
522
|
+
const dimensions = instagramVideoMetadata(bytes);
|
|
523
|
+
const thumbnailBytes = await stableInstagramPublishFile(
|
|
524
|
+
paths[1],
|
|
525
|
+
MAX_INSTAGRAM_THUMBNAIL_BYTES,
|
|
526
|
+
"video thumbnail",
|
|
527
|
+
operationDeadline,
|
|
528
|
+
);
|
|
529
|
+
const thumbnailDimensions = instagramJpegDimensions(thumbnailBytes);
|
|
530
|
+
return Object.freeze({
|
|
531
|
+
audience: plan.audience,
|
|
532
|
+
bytes,
|
|
533
|
+
byteLength: bytes.byteLength,
|
|
534
|
+
caption: plan.caption,
|
|
535
|
+
durationMilliseconds: dimensions.durationMilliseconds,
|
|
536
|
+
height: dimensions.height,
|
|
537
|
+
mediaType: "video/mp4" as const,
|
|
538
|
+
mediaSha256: instagramVideoSha256(bytes),
|
|
539
|
+
thumbnailByteLength: thumbnailBytes.byteLength,
|
|
540
|
+
thumbnailBytes,
|
|
541
|
+
thumbnailHeight: thumbnailDimensions.height,
|
|
542
|
+
thumbnailMediaType: "image/jpeg" as const,
|
|
543
|
+
thumbnailSha256: instagramVideoSha256(thumbnailBytes),
|
|
544
|
+
thumbnailWidth: thumbnailDimensions.width,
|
|
545
|
+
width: dimensions.width,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Revalidate and snapshot the exact materialized MP4 immediately before a
|
|
551
|
+
* future dispatch. The capture-required operation does not call this helper.
|
|
552
|
+
*/
|
|
553
|
+
export function revalidateInstagramVideoPublishBindingForDispatch(
|
|
554
|
+
value: unknown,
|
|
555
|
+
): InstagramVideoPublishDispatchSnapshot {
|
|
556
|
+
const binding = exactInstagramVideoPublishBinding(value);
|
|
557
|
+
const bytes = snapshotInstagramBytes(
|
|
558
|
+
binding.bytes,
|
|
559
|
+
MAX_INSTAGRAM_VIDEO_BYTES,
|
|
560
|
+
"MP4",
|
|
561
|
+
);
|
|
562
|
+
const thumbnailBytes = snapshotInstagramBytes(
|
|
563
|
+
binding.thumbnailBytes,
|
|
564
|
+
MAX_INSTAGRAM_THUMBNAIL_BYTES,
|
|
565
|
+
"JPEG thumbnail",
|
|
566
|
+
);
|
|
567
|
+
const dimensions = instagramVideoMetadata(bytes);
|
|
568
|
+
const thumbnailDimensions = instagramJpegDimensions(thumbnailBytes);
|
|
569
|
+
const mediaSha256 = instagramVideoSha256(bytes);
|
|
570
|
+
const thumbnailSha256 = instagramVideoSha256(thumbnailBytes);
|
|
571
|
+
if (
|
|
572
|
+
!Number.isSafeInteger(binding.byteLength)
|
|
573
|
+
|| binding.byteLength !== bytes.byteLength
|
|
574
|
+
|| typeof binding.mediaSha256 !== "string"
|
|
575
|
+
|| !/^[a-f0-9]{64}$/u.test(binding.mediaSha256)
|
|
576
|
+
|| binding.mediaSha256 !== mediaSha256
|
|
577
|
+
|| binding.durationMilliseconds !== dimensions.durationMilliseconds
|
|
578
|
+
|| binding.height !== dimensions.height
|
|
579
|
+
|| binding.width !== dimensions.width
|
|
580
|
+
|| !Number.isSafeInteger(binding.thumbnailByteLength)
|
|
581
|
+
|| binding.thumbnailByteLength !== thumbnailBytes.byteLength
|
|
582
|
+
|| typeof binding.thumbnailSha256 !== "string"
|
|
583
|
+
|| !/^[a-f0-9]{64}$/u.test(binding.thumbnailSha256)
|
|
584
|
+
|| binding.thumbnailSha256 !== thumbnailSha256
|
|
585
|
+
|| binding.thumbnailHeight !== thumbnailDimensions.height
|
|
586
|
+
|| binding.thumbnailWidth !== thumbnailDimensions.width
|
|
587
|
+
) throw new Error("Instagram video binding changed from its exact bytes");
|
|
588
|
+
if (
|
|
589
|
+
binding.audience !== "default"
|
|
590
|
+
|| binding.mediaType !== "video/mp4"
|
|
591
|
+
|| binding.thumbnailMediaType !== "image/jpeg"
|
|
592
|
+
) {
|
|
593
|
+
throw new Error("Instagram video binding declarations are invalid");
|
|
594
|
+
}
|
|
595
|
+
const body = new Blob([bytes], { type: "video/mp4" });
|
|
596
|
+
const thumbnailBody = new Blob([thumbnailBytes], { type: "image/jpeg" });
|
|
597
|
+
if (
|
|
598
|
+
body.size !== bytes.byteLength
|
|
599
|
+
|| body.type !== "video/mp4"
|
|
600
|
+
|| thumbnailBody.size !== thumbnailBytes.byteLength
|
|
601
|
+
|| thumbnailBody.type !== "image/jpeg"
|
|
602
|
+
) {
|
|
603
|
+
throw new Error("Instagram video dispatch snapshot changed shape");
|
|
604
|
+
}
|
|
605
|
+
return Object.freeze({
|
|
606
|
+
audience: "default" as const,
|
|
607
|
+
body,
|
|
608
|
+
byteLength: bytes.byteLength,
|
|
609
|
+
caption: boundedCaption(binding.caption, "Instagram video binding caption"),
|
|
610
|
+
durationMilliseconds: dimensions.durationMilliseconds,
|
|
611
|
+
height: dimensions.height,
|
|
612
|
+
mediaSha256,
|
|
613
|
+
mediaType: "video/mp4" as const,
|
|
614
|
+
thumbnailBody,
|
|
615
|
+
thumbnailByteLength: thumbnailBytes.byteLength,
|
|
616
|
+
thumbnailHeight: thumbnailDimensions.height,
|
|
617
|
+
thumbnailMediaType: "image/jpeg" as const,
|
|
618
|
+
thumbnailSha256,
|
|
619
|
+
thumbnailWidth: thumbnailDimensions.width,
|
|
620
|
+
width: dimensions.width,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/** Validate the exact authored-video confirmation required before deletion. */
|
|
625
|
+
export function prepareInstagramAuthoredPostDeleteInput(
|
|
626
|
+
input: OperationInput,
|
|
627
|
+
): InstagramAuthoredPostDeleteInput {
|
|
628
|
+
exactInputKeys(
|
|
629
|
+
input,
|
|
630
|
+
["expected_caption", "expected_media_kind", "media_id"],
|
|
631
|
+
"Instagram authored-post deletion",
|
|
632
|
+
);
|
|
633
|
+
if (
|
|
634
|
+
typeof input.media_id !== "string"
|
|
635
|
+
|| !/^[1-9][0-9]{0,31}_[1-9][0-9]{0,31}$/u.test(input.media_id)
|
|
636
|
+
) throw new Error("input.media_id must be one exact full Instagram media ID");
|
|
637
|
+
if (input.expected_media_kind !== "video") {
|
|
638
|
+
throw new Error("input.expected_media_kind must be video");
|
|
639
|
+
}
|
|
640
|
+
return Object.freeze({
|
|
641
|
+
expectedCaption: boundedCaption(input.expected_caption, "input.expected_caption"),
|
|
642
|
+
expectedMediaKind: "video" as const,
|
|
643
|
+
mediaId: input.media_id,
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Bind the already-bounded `posts.read` projection to one exact authored video.
|
|
649
|
+
* The shortcode is provider-derived here and is never accepted as delete input.
|
|
650
|
+
*/
|
|
651
|
+
export function bindInstagramVideoMediaReadback(
|
|
652
|
+
value: unknown,
|
|
653
|
+
expectation: InstagramVideoReadbackExpectation,
|
|
654
|
+
): InstagramVideoAcceptedTarget {
|
|
655
|
+
const expectedMediaId = instagramMediaId(
|
|
656
|
+
expectation.mediaId,
|
|
657
|
+
"Instagram video readback expected media ID",
|
|
658
|
+
);
|
|
659
|
+
const expectedCaption = boundedCaption(
|
|
660
|
+
expectation.expectedCaption,
|
|
661
|
+
"Instagram video readback expected caption",
|
|
662
|
+
);
|
|
663
|
+
if (
|
|
664
|
+
typeof expectation.viewerId !== "string"
|
|
665
|
+
|| !INSTAGRAM_VIEWER_ID_PATTERN.test(expectation.viewerId)
|
|
666
|
+
) throw new Error("Instagram video readback expected viewer ID is invalid");
|
|
667
|
+
const expectedCode = expectation.expectedCode === undefined
|
|
668
|
+
? undefined
|
|
669
|
+
: instagramShortcode(
|
|
670
|
+
expectation.expectedCode,
|
|
671
|
+
"Instagram video readback expected shortcode",
|
|
672
|
+
);
|
|
673
|
+
if (!isRecord(value)) {
|
|
674
|
+
throw new Error("Instagram video readback must be one bounded media projection");
|
|
675
|
+
}
|
|
676
|
+
exactNormalizedKeys(value, [
|
|
677
|
+
"caption",
|
|
678
|
+
"code",
|
|
679
|
+
"comment_count",
|
|
680
|
+
"has_liked",
|
|
681
|
+
"has_viewer_saved",
|
|
682
|
+
"id",
|
|
683
|
+
"like_count",
|
|
684
|
+
"media_type",
|
|
685
|
+
"pk",
|
|
686
|
+
"taken_at",
|
|
687
|
+
"user",
|
|
688
|
+
], "Instagram video readback");
|
|
689
|
+
const mediaId = instagramMediaId(value.id, "Instagram video readback media ID");
|
|
690
|
+
const code = instagramShortcode(value.code, "Instagram video readback shortcode");
|
|
691
|
+
if (
|
|
692
|
+
mediaId !== expectedMediaId
|
|
693
|
+
|| value.caption !== expectedCaption
|
|
694
|
+
|| value.media_type !== 2
|
|
695
|
+
|| (expectedCode !== undefined && code !== expectedCode)
|
|
696
|
+
) throw new Error("Instagram video readback did not bind the confirmed video");
|
|
697
|
+
if (!isRecord(value.user)) {
|
|
698
|
+
throw new Error("Instagram video readback did not bind the confirmed actor");
|
|
699
|
+
}
|
|
700
|
+
exactNormalizedKeys(
|
|
701
|
+
value.user,
|
|
702
|
+
["full_name", "id", "username"],
|
|
703
|
+
"Instagram video readback actor",
|
|
704
|
+
);
|
|
705
|
+
if (value.user.id !== expectation.viewerId) {
|
|
706
|
+
throw new Error("Instagram video readback did not bind the confirmed actor");
|
|
707
|
+
}
|
|
708
|
+
return Object.freeze({
|
|
709
|
+
code,
|
|
710
|
+
mediaId,
|
|
711
|
+
url: instagramVideoPermalink(code),
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function parseInstagramVideoPermalink(value: unknown, code: string): string {
|
|
716
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 2_048) {
|
|
717
|
+
throw new Error("Instagram provider-accepted video target returned an invalid permalink");
|
|
718
|
+
}
|
|
719
|
+
let url: URL;
|
|
720
|
+
try {
|
|
721
|
+
url = new URL(value);
|
|
722
|
+
} catch {
|
|
723
|
+
throw new Error("Instagram provider-accepted video target returned an invalid permalink");
|
|
724
|
+
}
|
|
725
|
+
if (
|
|
726
|
+
url.origin !== INSTAGRAM_ORIGIN
|
|
727
|
+
|| url.username !== ""
|
|
728
|
+
|| url.password !== ""
|
|
729
|
+
|| url.pathname !== `/p/${code}/`
|
|
730
|
+
|| url.search !== ""
|
|
731
|
+
|| url.hash !== ""
|
|
732
|
+
) throw new Error("Instagram provider-accepted video target returned an invalid permalink");
|
|
733
|
+
return url.href;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/** Serialize only provider-derived, secret-free target identity. */
|
|
737
|
+
export function instagramVideoAcceptedTargetIdentifier(
|
|
738
|
+
target: InstagramVideoAcceptedTarget,
|
|
739
|
+
): string {
|
|
740
|
+
const mediaId = instagramMediaId(
|
|
741
|
+
target.mediaId,
|
|
742
|
+
"Instagram provider-accepted video target media ID",
|
|
743
|
+
);
|
|
744
|
+
const code = instagramShortcode(
|
|
745
|
+
target.code,
|
|
746
|
+
"Instagram provider-accepted video target shortcode",
|
|
747
|
+
);
|
|
748
|
+
const url = parseInstagramVideoPermalink(target.url, code);
|
|
749
|
+
return canonicalJson({ code, mediaId, url });
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/** Parse the exact encrypted target later supplied to read-only reconciliation. */
|
|
753
|
+
export function parseInstagramVideoAcceptedTargetIdentifier(
|
|
754
|
+
identifier: string,
|
|
755
|
+
): InstagramVideoAcceptedTarget {
|
|
756
|
+
if (typeof identifier !== "string" || identifier.length < 1 || identifier.length > 4_096) {
|
|
757
|
+
throw new Error("Instagram provider-accepted video target is not canonical JSON");
|
|
758
|
+
}
|
|
759
|
+
let value: unknown;
|
|
760
|
+
try {
|
|
761
|
+
value = JSON.parse(identifier);
|
|
762
|
+
} catch {
|
|
763
|
+
throw new Error("Instagram provider-accepted video target is not canonical JSON");
|
|
764
|
+
}
|
|
765
|
+
if (!isRecord(value)) {
|
|
766
|
+
throw new Error("Instagram provider-accepted video target contained unsupported fields");
|
|
767
|
+
}
|
|
768
|
+
exactNormalizedKeys(
|
|
769
|
+
value,
|
|
770
|
+
["code", "mediaId", "url"],
|
|
771
|
+
"Instagram provider-accepted video target",
|
|
772
|
+
);
|
|
773
|
+
const target = Object.freeze({
|
|
774
|
+
code: instagramShortcode(
|
|
775
|
+
value.code,
|
|
776
|
+
"Instagram provider-accepted video target shortcode",
|
|
777
|
+
),
|
|
778
|
+
mediaId: instagramMediaId(
|
|
779
|
+
value.mediaId,
|
|
780
|
+
"Instagram provider-accepted video target media ID",
|
|
781
|
+
),
|
|
782
|
+
url: "",
|
|
783
|
+
});
|
|
784
|
+
const parsed = Object.freeze({
|
|
785
|
+
code: target.code,
|
|
786
|
+
mediaId: target.mediaId,
|
|
787
|
+
url: parseInstagramVideoPermalink(value.url, target.code),
|
|
788
|
+
});
|
|
789
|
+
if (canonicalJson(parsed) !== identifier) {
|
|
790
|
+
throw new Error("Instagram provider-accepted video target is not canonical");
|
|
791
|
+
}
|
|
792
|
+
return parsed;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
/** Decode one provider-returned shortcode into its exact decimal media PK. */
|
|
796
|
+
export function instagramShortcodeMediaPk(value: unknown): string {
|
|
797
|
+
const code = instagramShortcode(value, "Instagram video shortcode");
|
|
798
|
+
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
799
|
+
let decoded = 0n;
|
|
800
|
+
for (const character of code) {
|
|
801
|
+
const digit = alphabet.indexOf(character);
|
|
802
|
+
if (digit < 0) throw new Error("Instagram video shortcode is invalid");
|
|
803
|
+
decoded = (decoded * 64n) + BigInt(digit);
|
|
804
|
+
}
|
|
805
|
+
const pk = decoded.toString(10);
|
|
806
|
+
if (decoded < 1n || !/^[1-9][0-9]{0,31}$/u.test(pk)) {
|
|
807
|
+
throw new Error("Instagram video shortcode decoded outside the media-PK bound");
|
|
808
|
+
}
|
|
809
|
+
return pk;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/** Strict one-field acknowledgement observed for both Instagram ruploads. */
|
|
813
|
+
export function assertInstagramVideoUploadAcknowledgement(value: unknown): void {
|
|
814
|
+
if (
|
|
815
|
+
!isRecord(value)
|
|
816
|
+
|| Object.keys(value).sort().join(",") !== "status"
|
|
817
|
+
|| value.status !== "ok"
|
|
818
|
+
) throw new Error("Instagram video upload acknowledgement changed shape");
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Validate the observed asynchronous configure response without authorizing a
|
|
823
|
+
* second configure POST. The captured 202 body was exactly status+message.
|
|
824
|
+
*/
|
|
825
|
+
export function assertInstagramVideoConfigureIndeterminate(value: unknown): void {
|
|
826
|
+
if (
|
|
827
|
+
!isRecord(value)
|
|
828
|
+
|| Object.keys(value).sort().join(",") !== "message,status"
|
|
829
|
+
|| value.status !== "fail"
|
|
830
|
+
|| typeof value.message !== "string"
|
|
831
|
+
|| value.message.length < 1
|
|
832
|
+
|| value.message.length > 2_048
|
|
833
|
+
|| /[\0\r]/u.test(value.message)
|
|
834
|
+
) throw new Error("Instagram video configure 202 envelope changed shape");
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Project the fixed public-bundle configure contract and bind every identifier
|
|
839
|
+
* established by the live accepted response. Unknown raw media metadata stays
|
|
840
|
+
* private and is not projected; the independent media-info GET remains the
|
|
841
|
+
* authoritative actor/type/caption readback.
|
|
842
|
+
*/
|
|
843
|
+
export function parseInstagramVideoConfigureAccepted(
|
|
844
|
+
value: unknown,
|
|
845
|
+
expectation: InstagramVideoConfigureExpectation,
|
|
846
|
+
): InstagramVideoAcceptedTarget {
|
|
847
|
+
const root = boundedForeignRecord(
|
|
848
|
+
value,
|
|
849
|
+
"Instagram video configure response",
|
|
850
|
+
32,
|
|
851
|
+
);
|
|
852
|
+
const uploadId = instagramMediaId(
|
|
853
|
+
expectation.uploadId,
|
|
854
|
+
"Instagram video configure expected upload ID",
|
|
855
|
+
);
|
|
856
|
+
const viewerId = instagramMediaId(
|
|
857
|
+
expectation.viewerId,
|
|
858
|
+
"Instagram video configure expected viewer ID",
|
|
859
|
+
);
|
|
860
|
+
const caption = boundedCaption(
|
|
861
|
+
expectation.caption,
|
|
862
|
+
"Instagram video configure expected caption",
|
|
863
|
+
);
|
|
864
|
+
if (root.status !== "ok" || root.upload_id !== uploadId) {
|
|
865
|
+
throw new Error("Instagram video configure response did not accept the exact upload");
|
|
866
|
+
}
|
|
867
|
+
const media = boundedForeignRecord(
|
|
868
|
+
root.media,
|
|
869
|
+
"Instagram video configure response.media",
|
|
870
|
+
256,
|
|
871
|
+
);
|
|
872
|
+
const mediaId = instagramMediaId(
|
|
873
|
+
media.id,
|
|
874
|
+
"Instagram video configure response media ID",
|
|
875
|
+
);
|
|
876
|
+
const pk = instagramMediaId(
|
|
877
|
+
media.pk,
|
|
878
|
+
"Instagram video configure response media PK",
|
|
879
|
+
);
|
|
880
|
+
if (pk.includes("_") || mediaId !== `${pk}_${viewerId}`) {
|
|
881
|
+
throw new Error("Instagram video configure response did not bind the current actor");
|
|
882
|
+
}
|
|
883
|
+
const code = instagramShortcode(
|
|
884
|
+
media.code,
|
|
885
|
+
"Instagram video configure response shortcode",
|
|
886
|
+
);
|
|
887
|
+
if (instagramShortcodeMediaPk(code) !== pk) {
|
|
888
|
+
throw new Error("Instagram video configure response shortcode changed the media PK");
|
|
889
|
+
}
|
|
890
|
+
const captionContainer = boundedForeignRecord(
|
|
891
|
+
media.caption,
|
|
892
|
+
"Instagram video configure response.media.caption",
|
|
893
|
+
64,
|
|
894
|
+
);
|
|
895
|
+
if (captionContainer.text !== caption || media.media_type !== 2) {
|
|
896
|
+
throw new Error("Instagram video configure response did not bind the exact video");
|
|
897
|
+
}
|
|
898
|
+
return Object.freeze({
|
|
899
|
+
code,
|
|
900
|
+
mediaId,
|
|
901
|
+
url: instagramVideoPermalink(code),
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/** Fixed public-bundle projection for the observed deletion response. */
|
|
906
|
+
export function assertInstagramVideoDeleteAcknowledgement(value: unknown): void {
|
|
907
|
+
if (
|
|
908
|
+
!isRecord(value)
|
|
909
|
+
|| Object.keys(value).sort().join(",") !== "did_delete,status"
|
|
910
|
+
|| value.status !== "ok"
|
|
911
|
+
|| value.did_delete !== true
|
|
912
|
+
) throw new Error("Instagram video deletion acknowledgement changed shape");
|
|
913
|
+
}
|