@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,906 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isLiteralLanguageTag, normalizeAuthContextName } from "./args";
|
|
3
|
+
import { compareUtf8 } from "./utf8-order";
|
|
4
|
+
|
|
5
|
+
export interface CaptionSelection {
|
|
6
|
+
readonly source: "manual" | "automatic";
|
|
7
|
+
readonly language: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface AcquisitionIdentity {
|
|
11
|
+
/** Exact provider identity used only for probe/capture comparison and hashing. */
|
|
12
|
+
readonly extractor: string;
|
|
13
|
+
readonly id: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const YT_DLP_OPAQUE_IDENTITY_PROFILE = "yt-dlp-opaque-url-v1" as const;
|
|
17
|
+
export const YT_DLP_AUTH_IDENTITY_PROFILE = "yt-dlp-auth-context-v1" as const;
|
|
18
|
+
export const AUTH_CONTEXT_IDENTITY_PROFILE = "wrench-media-auth-context-v1" as const;
|
|
19
|
+
|
|
20
|
+
/** Path-free inputs that let schema verification reproduce an opaque yt-dlp key. */
|
|
21
|
+
export interface OpaqueYtDlpIdentity {
|
|
22
|
+
readonly profile: typeof YT_DLP_OPAQUE_IDENTITY_PROFILE;
|
|
23
|
+
readonly providerIdentitySha256: string;
|
|
24
|
+
readonly requestedUrlSha256: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type YtDlpPrivateAccessMode = "browser" | "ambient_config";
|
|
28
|
+
|
|
29
|
+
/** Path-free inputs that isolate one explicit private-access realm. */
|
|
30
|
+
export interface AuthenticatedYtDlpIdentity {
|
|
31
|
+
readonly profile: typeof YT_DLP_AUTH_IDENTITY_PROFILE;
|
|
32
|
+
readonly providerIdentitySha256: string;
|
|
33
|
+
readonly requestedUrlSha256: string;
|
|
34
|
+
readonly accessMode: YtDlpPrivateAccessMode;
|
|
35
|
+
readonly authContext: {
|
|
36
|
+
readonly profile: typeof AUTH_CONTEXT_IDENTITY_PROFILE;
|
|
37
|
+
readonly sha256: string;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface YtDlpAuthorizationIdentityInput {
|
|
42
|
+
readonly mode: YtDlpPrivateAccessMode;
|
|
43
|
+
readonly contextSha256: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type SourceProjection = "youtube" | "opaque";
|
|
47
|
+
|
|
48
|
+
export interface ProbeMetadata {
|
|
49
|
+
readonly acquisitionIdentity: AcquisitionIdentity;
|
|
50
|
+
readonly projection: SourceProjection;
|
|
51
|
+
/** Exact unowned values that must be scrubbed from downstream diagnostics. */
|
|
52
|
+
readonly diagnosticRedactions: readonly string[];
|
|
53
|
+
readonly id: string;
|
|
54
|
+
readonly extractor: string;
|
|
55
|
+
readonly extractorDirectory: string;
|
|
56
|
+
readonly itemDirectory: string;
|
|
57
|
+
readonly assetKey: string;
|
|
58
|
+
/** Present only for unowned yt-dlp sources; never contains a raw URL or provider value. */
|
|
59
|
+
readonly opaqueYtDlpIdentity?: OpaqueYtDlpIdentity;
|
|
60
|
+
/** Present only for explicitly named private access; contains digests, never credentials. */
|
|
61
|
+
readonly authenticatedYtDlpIdentity?: AuthenticatedYtDlpIdentity;
|
|
62
|
+
readonly canonicalUrl: string;
|
|
63
|
+
readonly title?: string;
|
|
64
|
+
readonly uploader?: string;
|
|
65
|
+
readonly channel?: string;
|
|
66
|
+
readonly description?: string;
|
|
67
|
+
readonly license?: string;
|
|
68
|
+
readonly uploadDate?: string;
|
|
69
|
+
readonly timestamp?: number;
|
|
70
|
+
readonly durationSeconds?: number;
|
|
71
|
+
readonly originalLanguage?: string;
|
|
72
|
+
readonly manualCaptionLanguages: readonly string[];
|
|
73
|
+
readonly automaticCaptionLanguages: readonly string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const IDENTITY_KEY_VERSION = 1 as const;
|
|
77
|
+
const DIRECT_HTTP_IDENTITY_VERSION = 1 as const;
|
|
78
|
+
const OWNED_YOUTUBE_EXTRACTOR = "Youtube" as const;
|
|
79
|
+
const DIRECT_HTTP_EXTRACTOR = "DirectHttp" as const;
|
|
80
|
+
const OPAQUE_EXTRACTOR = "External" as const;
|
|
81
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
|
|
82
|
+
const YOUTUBE_VIDEO_ID_PATTERN = /^[A-Za-z0-9_-]{11}$/u;
|
|
83
|
+
const IDENTITY_SEGMENT_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
|
|
84
|
+
const EXTRACTOR_DIRECTORY_MAX_LENGTH = 80;
|
|
85
|
+
const WINDOWS_DEVICE_NAME_PATTERN = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/iu;
|
|
86
|
+
|
|
87
|
+
export const PROVIDER_METADATA_SCHEMA_VERSION = 1 as const;
|
|
88
|
+
|
|
89
|
+
export interface ProviderMetadataDocument {
|
|
90
|
+
readonly schemaVersion: typeof PROVIDER_METADATA_SCHEMA_VERSION;
|
|
91
|
+
readonly sourceAssetKey: string;
|
|
92
|
+
readonly source: {
|
|
93
|
+
readonly extractor: string;
|
|
94
|
+
readonly id: string;
|
|
95
|
+
readonly canonicalUrl: string;
|
|
96
|
+
readonly title?: string;
|
|
97
|
+
readonly uploader?: string;
|
|
98
|
+
readonly channel?: string;
|
|
99
|
+
readonly description?: string;
|
|
100
|
+
readonly license?: string;
|
|
101
|
+
readonly uploadDate?: string;
|
|
102
|
+
readonly timestamp?: number;
|
|
103
|
+
readonly durationSeconds?: number;
|
|
104
|
+
};
|
|
105
|
+
readonly captions: {
|
|
106
|
+
readonly originalLanguage?: string;
|
|
107
|
+
readonly manualLanguages: readonly string[];
|
|
108
|
+
readonly automaticLanguages: readonly string[];
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export type ParseProbeResult =
|
|
113
|
+
| { readonly ok: true; readonly metadata: ProbeMetadata }
|
|
114
|
+
| {
|
|
115
|
+
readonly ok: false;
|
|
116
|
+
readonly kind: "invalid";
|
|
117
|
+
readonly message: string;
|
|
118
|
+
}
|
|
119
|
+
| {
|
|
120
|
+
readonly ok: false;
|
|
121
|
+
readonly kind: "unsupported";
|
|
122
|
+
readonly reason: "playlist" | "multi-video" | "live" | "upcoming" | "post-live" | "drm";
|
|
123
|
+
readonly message: string;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
127
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function hasControlCharacter(value: string): boolean {
|
|
131
|
+
for (const character of value) {
|
|
132
|
+
const codePoint = character.codePointAt(0);
|
|
133
|
+
if (codePoint !== undefined && (codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f))) return true;
|
|
134
|
+
}
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function isWellFormedIdentity(value: string): boolean {
|
|
139
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
140
|
+
const codeUnit = value.charCodeAt(index);
|
|
141
|
+
if (codeUnit >= 0xd800 && codeUnit <= 0xdbff) {
|
|
142
|
+
const next = value.charCodeAt(index + 1);
|
|
143
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) return false;
|
|
144
|
+
index += 1;
|
|
145
|
+
} else if (codeUnit >= 0xdc00 && codeUnit <= 0xdfff) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function assertWellFormedIdentity(value: string): void {
|
|
153
|
+
if (!isWellFormedIdentity(value)) {
|
|
154
|
+
throw new TypeError("provider identity contains ill-formed UTF-16");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function boundedString(record: Readonly<Record<string, unknown>>, key: string, maximum = 4_096): string | undefined {
|
|
159
|
+
const value = record[key];
|
|
160
|
+
if (typeof value !== "string") return undefined;
|
|
161
|
+
const normalized = value.normalize("NFC").trim();
|
|
162
|
+
return normalized.length > 0 && normalized.length <= maximum ? normalized : undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function boundedIdentityString(
|
|
166
|
+
record: Readonly<Record<string, unknown>>,
|
|
167
|
+
key: string,
|
|
168
|
+
maximum: number,
|
|
169
|
+
): string | undefined {
|
|
170
|
+
const value = record[key];
|
|
171
|
+
return typeof value === "string"
|
|
172
|
+
&& value.length > 0
|
|
173
|
+
&& value.length <= maximum
|
|
174
|
+
&& !hasControlCharacter(value)
|
|
175
|
+
&& isWellFormedIdentity(value)
|
|
176
|
+
? value
|
|
177
|
+
: undefined;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function finiteNumber(record: Readonly<Record<string, unknown>>, key: string): number | undefined {
|
|
181
|
+
const value = record[key];
|
|
182
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function diagnosticRedactions(
|
|
186
|
+
record: Readonly<Record<string, unknown>>,
|
|
187
|
+
acquisitionIdentity: AcquisitionIdentity,
|
|
188
|
+
): readonly string[] {
|
|
189
|
+
const values: string[] = [acquisitionIdentity.extractor, acquisitionIdentity.id];
|
|
190
|
+
for (const key of [
|
|
191
|
+
"title",
|
|
192
|
+
"uploader",
|
|
193
|
+
"channel",
|
|
194
|
+
"description",
|
|
195
|
+
"license",
|
|
196
|
+
"upload_date",
|
|
197
|
+
"language",
|
|
198
|
+
]) {
|
|
199
|
+
const value = record[key];
|
|
200
|
+
if (
|
|
201
|
+
typeof value === "string"
|
|
202
|
+
&& value.length > 0
|
|
203
|
+
&& value.length <= 64 * 1_024
|
|
204
|
+
&& !hasControlCharacter(value)
|
|
205
|
+
&& isWellFormedIdentity(value)
|
|
206
|
+
) {
|
|
207
|
+
values.push(value);
|
|
208
|
+
const normalized = value.normalize("NFC").trim();
|
|
209
|
+
if (
|
|
210
|
+
normalized.length > 0
|
|
211
|
+
&& normalized.length <= 64 * 1_024
|
|
212
|
+
&& !hasControlCharacter(normalized)
|
|
213
|
+
&& isWellFormedIdentity(normalized)
|
|
214
|
+
) values.push(normalized);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Longest-first exact replacement is deterministic and prevents a shorter
|
|
218
|
+
// identity from destroying a match for a longer descriptive value.
|
|
219
|
+
return [...new Set(values)].toSorted((left, right) => right.length - left.length);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const youtubeHosts = new Set([
|
|
223
|
+
"youtube.com",
|
|
224
|
+
"www.youtube.com",
|
|
225
|
+
"m.youtube.com",
|
|
226
|
+
"music.youtube.com",
|
|
227
|
+
"youtu.be",
|
|
228
|
+
"www.youtu.be",
|
|
229
|
+
"youtube-nocookie.com",
|
|
230
|
+
"www.youtube-nocookie.com",
|
|
231
|
+
]);
|
|
232
|
+
|
|
233
|
+
function parsedSafeWebUrl(value: string | undefined): URL | undefined {
|
|
234
|
+
if (value === undefined || value.length > 8_192 || hasControlCharacter(value)) return undefined;
|
|
235
|
+
try {
|
|
236
|
+
const parsed = new URL(value);
|
|
237
|
+
if ((parsed.protocol !== "http:" && parsed.protocol !== "https:") || parsed.username !== "" || parsed.password !== "") {
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
return parsed;
|
|
241
|
+
} catch {
|
|
242
|
+
return undefined;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function isOwnedYouTubeSource(
|
|
247
|
+
extractor: string,
|
|
248
|
+
providerId: string,
|
|
249
|
+
candidateUrls: readonly (string | undefined)[],
|
|
250
|
+
): boolean {
|
|
251
|
+
// The exact extractor key is part of the ownership allowlist. Prefix or
|
|
252
|
+
// case-insensitive matching would let a plugin-defined extractor opt itself
|
|
253
|
+
// into persistence of raw identity and descriptive metadata.
|
|
254
|
+
return extractor === OWNED_YOUTUBE_EXTRACTOR
|
|
255
|
+
&& YOUTUBE_VIDEO_ID_PATTERN.test(providerId)
|
|
256
|
+
&& candidateUrls.some((value) => {
|
|
257
|
+
const parsed = parsedSafeWebUrl(value);
|
|
258
|
+
return parsed !== undefined && youtubeHosts.has(parsed.hostname.toLowerCase());
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function canonicalSourceUrl(
|
|
263
|
+
candidateUrls: readonly (string | undefined)[],
|
|
264
|
+
projection: SourceProjection,
|
|
265
|
+
providerId: string,
|
|
266
|
+
): string | undefined {
|
|
267
|
+
if (projection === "youtube") {
|
|
268
|
+
// YouTube's public video identity belongs in `v`; every other query
|
|
269
|
+
// parameter is navigation state, tracking data, or potentially secret.
|
|
270
|
+
const canonical = new URL("https://www.youtube.com/watch");
|
|
271
|
+
canonical.searchParams.set("v", providerId);
|
|
272
|
+
return canonical.href;
|
|
273
|
+
}
|
|
274
|
+
for (const value of candidateUrls) {
|
|
275
|
+
const parsed = parsedSafeWebUrl(value);
|
|
276
|
+
if (parsed === undefined) continue;
|
|
277
|
+
// Ghostget media does not own the semantics or privacy of arbitrary URL paths.
|
|
278
|
+
// Persist only the public origin until a provider-specific canonicalizer
|
|
279
|
+
// can explicitly allowlist more.
|
|
280
|
+
parsed.hash = "";
|
|
281
|
+
parsed.search = "";
|
|
282
|
+
parsed.pathname = "/";
|
|
283
|
+
return parsed.href;
|
|
284
|
+
}
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function youtubeCanonicalUrl(providerId: string): string {
|
|
289
|
+
const canonical = new URL("https://www.youtube.com/watch");
|
|
290
|
+
canonical.searchParams.set("v", providerId);
|
|
291
|
+
return canonical.href;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function languageKeys(value: unknown): readonly string[] {
|
|
295
|
+
if (!isRecord(value)) return [];
|
|
296
|
+
return Object.keys(value)
|
|
297
|
+
.filter((key) => {
|
|
298
|
+
const tracks = value[key];
|
|
299
|
+
return Array.isArray(tracks)
|
|
300
|
+
&& tracks.some(isRecord)
|
|
301
|
+
&& isLiteralLanguageTag(key)
|
|
302
|
+
&& !hasControlCharacter(key)
|
|
303
|
+
&& isWellFormedIdentity(key)
|
|
304
|
+
&& !key.includes("/")
|
|
305
|
+
&& !key.includes("\\");
|
|
306
|
+
})
|
|
307
|
+
.toSorted(compareUtf8);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function isPortableIdentityDirectorySegment(value: string): boolean {
|
|
311
|
+
return IDENTITY_SEGMENT_PATTERN.test(value)
|
|
312
|
+
&& value !== "."
|
|
313
|
+
&& value !== ".."
|
|
314
|
+
&& !value.endsWith(".")
|
|
315
|
+
&& !WINDOWS_DEVICE_NAME_PATTERN.test(value);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function identityDirectorySegment(value: string, fallbackPrefix: string): string {
|
|
319
|
+
const normalized = value.normalize("NFKC");
|
|
320
|
+
// NFKC is useful for detecting compatibility characters, but accepting the
|
|
321
|
+
// normalized spelling would collapse distinct provider identities onto the
|
|
322
|
+
// same physical directory. Only an already-canonical raw value is reused.
|
|
323
|
+
if (
|
|
324
|
+
normalized === value
|
|
325
|
+
&& isPortableIdentityDirectorySegment(value)
|
|
326
|
+
) {
|
|
327
|
+
return normalized;
|
|
328
|
+
}
|
|
329
|
+
const digest = createHash("sha256").update(value, "utf8").digest("hex").slice(0, 24);
|
|
330
|
+
return `${fallbackPrefix}-${digest}`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* A readable but filesystem-portable provider item leaf. The digest is always
|
|
335
|
+
* present because provider IDs are case-sensitive while common macOS and
|
|
336
|
+
* Windows volumes are not; it also prevents Unicode normalization aliases.
|
|
337
|
+
*/
|
|
338
|
+
export function sourceItemDirectory(value: string): string {
|
|
339
|
+
assertWellFormedIdentity(value);
|
|
340
|
+
const digest = createHash("sha256").update(value, "utf8").digest("hex").slice(0, 24);
|
|
341
|
+
const readable = value.normalize("NFKC").toLowerCase()
|
|
342
|
+
.replace(/[^a-z0-9._-]+/gu, "-")
|
|
343
|
+
.replace(/^[._-]+|[.-]+$/gu, "");
|
|
344
|
+
const maximumPrefixLength = 128 - digest.length - 1;
|
|
345
|
+
const prefix = readable
|
|
346
|
+
.slice(0, maximumPrefixLength)
|
|
347
|
+
.replace(/[.-]+$/gu, "") || "item";
|
|
348
|
+
return `${prefix}-${digest}`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function sourceExtractorDirectory(value: string): string {
|
|
352
|
+
assertWellFormedIdentity(value);
|
|
353
|
+
const compatibilityNormalized = value.normalize("NFKC");
|
|
354
|
+
const lower = compatibilityNormalized.toLowerCase();
|
|
355
|
+
const slug = lower
|
|
356
|
+
.replace(/[^a-z0-9._-]+/gu, "-")
|
|
357
|
+
.replace(/^[._-]+|[.-]+$/gu, "");
|
|
358
|
+
const digest = createHash("sha256").update(value, "utf8").digest("hex").slice(0, 24);
|
|
359
|
+
const readablePrefix = slug.length === 0 || slug === "." || slug === ".." ? "source" : slug;
|
|
360
|
+
const maximumPrefixLength = EXTRACTOR_DIRECTORY_MAX_LENGTH - digest.length - 1;
|
|
361
|
+
return `${readablePrefix.slice(0, maximumPrefixLength).replace(/[-.]+$/gu, "") || "source"}-${digest}`;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function tupleDigest(domain: string, components: readonly string[]): string {
|
|
365
|
+
const hash = createHash("sha256");
|
|
366
|
+
hash.update("wrench-media-identity-key\0", "utf8");
|
|
367
|
+
for (const component of [domain, ...components]) {
|
|
368
|
+
const bytes = new TextEncoder().encode(component);
|
|
369
|
+
const length = new Uint8Array(8);
|
|
370
|
+
new DataView(length.buffer).setBigUint64(0, BigInt(bytes.byteLength), false);
|
|
371
|
+
hash.update(length);
|
|
372
|
+
hash.update(bytes);
|
|
373
|
+
}
|
|
374
|
+
return hash.digest("hex");
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function invalidProbe(message: string): Extract<ParseProbeResult, { readonly kind: "invalid" }> {
|
|
378
|
+
return { ok: false, kind: "invalid", message };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function unsupportedProbe(
|
|
382
|
+
reason: Extract<ParseProbeResult, { readonly kind: "unsupported" }>["reason"],
|
|
383
|
+
): Extract<ParseProbeResult, { readonly kind: "unsupported" }> {
|
|
384
|
+
const labels = {
|
|
385
|
+
playlist: "playlist",
|
|
386
|
+
"multi-video": "multi-video source",
|
|
387
|
+
live: "live source",
|
|
388
|
+
upcoming: "upcoming live source",
|
|
389
|
+
"post-live": "post-live source",
|
|
390
|
+
drm: "DRM-protected source",
|
|
391
|
+
} as const;
|
|
392
|
+
return {
|
|
393
|
+
ok: false,
|
|
394
|
+
kind: "unsupported",
|
|
395
|
+
reason,
|
|
396
|
+
message: `yt-dlp probe returned an unsupported ${labels[reason]}`,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function unsupportedProbeEnvelope(
|
|
401
|
+
value: Readonly<Record<string, unknown>>,
|
|
402
|
+
): Extract<ParseProbeResult, { readonly kind: "unsupported" }> | null {
|
|
403
|
+
if (value["_type"] === "playlist") return unsupportedProbe("playlist");
|
|
404
|
+
if (value["_type"] === "multi_video") return unsupportedProbe("multi-video");
|
|
405
|
+
|
|
406
|
+
const liveStatus = value["live_status"];
|
|
407
|
+
if (value["is_live"] === true || liveStatus === "is_live") return unsupportedProbe("live");
|
|
408
|
+
if (value["is_upcoming"] === true || liveStatus === "is_upcoming") {
|
|
409
|
+
return unsupportedProbe("upcoming");
|
|
410
|
+
}
|
|
411
|
+
if (liveStatus === "post_live") return unsupportedProbe("post-live");
|
|
412
|
+
|
|
413
|
+
if (value["has_drm"] === true || value["_has_drm"] === true) {
|
|
414
|
+
return unsupportedProbe("drm");
|
|
415
|
+
}
|
|
416
|
+
const requestedFormats = value["requested_formats"];
|
|
417
|
+
if (
|
|
418
|
+
Array.isArray(requestedFormats)
|
|
419
|
+
&& requestedFormats.some((format) => isRecord(format) && format["has_drm"] === true)
|
|
420
|
+
) {
|
|
421
|
+
return unsupportedProbe("drm");
|
|
422
|
+
}
|
|
423
|
+
const formats = value["formats"];
|
|
424
|
+
const formatRecords = Array.isArray(formats) ? formats.filter(isRecord) : [];
|
|
425
|
+
if (
|
|
426
|
+
Array.isArray(formats)
|
|
427
|
+
&& formatRecords.length > 0
|
|
428
|
+
&& formatRecords.length === formats.length
|
|
429
|
+
&& formatRecords.every((format) => format["has_drm"] === true)
|
|
430
|
+
) {
|
|
431
|
+
return unsupportedProbe("drm");
|
|
432
|
+
}
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Domain-separated digest of the exact, case-sensitive provider tuple. */
|
|
437
|
+
export function providerIdentitySha256(extractor: string, providerId: string): string {
|
|
438
|
+
assertWellFormedIdentity(extractor);
|
|
439
|
+
assertWellFormedIdentity(providerId);
|
|
440
|
+
return tupleDigest("source", [extractor, providerId]);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** SHA-256 of the normalized HTTP(S) request URL after removing only its fragment. */
|
|
444
|
+
export function requestedUrlSha256(value: string): string {
|
|
445
|
+
const parsed = parsedSafeWebUrl(value);
|
|
446
|
+
if (parsed === undefined) throw new TypeError("yt-dlp requested URL is malformed");
|
|
447
|
+
parsed.hash = "";
|
|
448
|
+
return createHash("sha256").update(parsed.href, "utf8").digest("hex");
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Stable digest of a canonical user-declared authorization realm name. */
|
|
452
|
+
export function authContextSha256(value: string): string {
|
|
453
|
+
const normalized = normalizeAuthContextName(value);
|
|
454
|
+
if (normalized === null) throw new TypeError("authorization context is malformed");
|
|
455
|
+
return tupleDigest("auth-context", [normalized]);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** A collision-resistant, domain-separated identity for the raw provider tuple. */
|
|
459
|
+
export function sourceAssetKey(extractor: string, providerId: string): string {
|
|
460
|
+
return `source-v${String(IDENTITY_KEY_VERSION)}-${providerIdentitySha256(extractor, providerId)}`;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** Builds the URL-qualified identity used by every unowned yt-dlp source. */
|
|
464
|
+
export function opaqueYtDlpSourceAssetKey(identity: OpaqueYtDlpIdentity): string {
|
|
465
|
+
if (
|
|
466
|
+
identity.profile !== YT_DLP_OPAQUE_IDENTITY_PROFILE
|
|
467
|
+
|| !SHA256_PATTERN.test(identity.providerIdentitySha256)
|
|
468
|
+
|| !SHA256_PATTERN.test(identity.requestedUrlSha256)
|
|
469
|
+
) {
|
|
470
|
+
throw new TypeError("opaque yt-dlp identity is malformed");
|
|
471
|
+
}
|
|
472
|
+
return `source-v2-${tupleDigest("yt-dlp-opaque-url", [
|
|
473
|
+
identity.providerIdentitySha256,
|
|
474
|
+
identity.requestedUrlSha256,
|
|
475
|
+
])}`;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Stable public projection of a URL-qualified opaque yt-dlp source key. */
|
|
479
|
+
export function opaqueYtDlpSourceId(assetKey: string): string {
|
|
480
|
+
const match = /^source-v2-([0-9a-f]{64})$/u.exec(assetKey);
|
|
481
|
+
if (match?.[1] === undefined) throw new TypeError("opaque yt-dlp source asset key is malformed");
|
|
482
|
+
return `opaque-v2-${match[1]}`;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/** Builds the disjoint opaque identity used by browser and ambient-config access. */
|
|
486
|
+
export function authenticatedYtDlpSourceAssetKey(
|
|
487
|
+
identity: AuthenticatedYtDlpIdentity,
|
|
488
|
+
): string {
|
|
489
|
+
if (
|
|
490
|
+
identity.profile !== YT_DLP_AUTH_IDENTITY_PROFILE
|
|
491
|
+
|| !SHA256_PATTERN.test(identity.providerIdentitySha256)
|
|
492
|
+
|| !SHA256_PATTERN.test(identity.requestedUrlSha256)
|
|
493
|
+
|| (identity.accessMode !== "browser" && identity.accessMode !== "ambient_config")
|
|
494
|
+
|| identity.authContext.profile !== AUTH_CONTEXT_IDENTITY_PROFILE
|
|
495
|
+
|| !SHA256_PATTERN.test(identity.authContext.sha256)
|
|
496
|
+
) {
|
|
497
|
+
throw new TypeError("authenticated yt-dlp identity is malformed");
|
|
498
|
+
}
|
|
499
|
+
return `source-v3-${tupleDigest("yt-dlp-auth-context", [
|
|
500
|
+
identity.providerIdentitySha256,
|
|
501
|
+
identity.requestedUrlSha256,
|
|
502
|
+
identity.accessMode,
|
|
503
|
+
identity.authContext.sha256,
|
|
504
|
+
])}`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/** Stable public projection of an authenticated opaque yt-dlp source key. */
|
|
508
|
+
export function authenticatedYtDlpSourceId(assetKey: string): string {
|
|
509
|
+
const match = /^source-v3-([0-9a-f]{64})$/u.exec(assetKey);
|
|
510
|
+
if (match?.[1] === undefined) {
|
|
511
|
+
throw new TypeError("authenticated yt-dlp source asset key is malformed");
|
|
512
|
+
}
|
|
513
|
+
return `opaque-v3-${match[1]}`;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export interface DirectHttpMetadataInput {
|
|
517
|
+
/** Origin-only public projection of the requested URL. */
|
|
518
|
+
readonly requestedOrigin: string;
|
|
519
|
+
/** SHA-256 of the exact fragment-stripped requested URL. */
|
|
520
|
+
readonly requestedUrlSha256: string;
|
|
521
|
+
/** SHA-256 of the complete response body. */
|
|
522
|
+
readonly bodySha256: string;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Builds a final direct-file identity only after the complete body is known.
|
|
527
|
+
* URL secrets never cross this boundary: callers provide an origin and hashes.
|
|
528
|
+
*/
|
|
529
|
+
export function createDirectHttpMetadata(input: DirectHttpMetadataInput): ProbeMetadata {
|
|
530
|
+
if (
|
|
531
|
+
!isOriginOnlyCanonicalUrl(input.requestedOrigin)
|
|
532
|
+
|| !SHA256_PATTERN.test(input.requestedUrlSha256)
|
|
533
|
+
|| !SHA256_PATTERN.test(input.bodySha256)
|
|
534
|
+
) {
|
|
535
|
+
throw new TypeError("direct HTTP identity is malformed");
|
|
536
|
+
}
|
|
537
|
+
const acquisitionId = `direct-http-v${String(DIRECT_HTTP_IDENTITY_VERSION)}-${tupleDigest(
|
|
538
|
+
"direct-http",
|
|
539
|
+
[input.requestedUrlSha256, input.bodySha256],
|
|
540
|
+
)}`;
|
|
541
|
+
const assetKey = sourceAssetKey(DIRECT_HTTP_EXTRACTOR, acquisitionId);
|
|
542
|
+
const projectedId = opaqueSourceId(assetKey);
|
|
543
|
+
return {
|
|
544
|
+
acquisitionIdentity: { extractor: DIRECT_HTTP_EXTRACTOR, id: acquisitionId },
|
|
545
|
+
projection: "opaque",
|
|
546
|
+
diagnosticRedactions: [acquisitionId, DIRECT_HTTP_EXTRACTOR],
|
|
547
|
+
id: projectedId,
|
|
548
|
+
extractor: OPAQUE_EXTRACTOR,
|
|
549
|
+
extractorDirectory: sourceExtractorDirectory(OPAQUE_EXTRACTOR),
|
|
550
|
+
itemDirectory: sourceItemDirectory(projectedId),
|
|
551
|
+
assetKey,
|
|
552
|
+
canonicalUrl: input.requestedOrigin,
|
|
553
|
+
manualCaptionLanguages: [],
|
|
554
|
+
automaticCaptionLanguages: [],
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** Stable opaque identity projected from a raw provider tuple digest. */
|
|
559
|
+
export function opaqueSourceId(assetKey: string): string {
|
|
560
|
+
const prefix = `source-v${String(IDENTITY_KEY_VERSION)}-`;
|
|
561
|
+
if (!assetKey.startsWith(prefix) || !/^[0-9a-f]{64}$/u.test(assetKey.slice(prefix.length))) {
|
|
562
|
+
throw new TypeError("source asset key is malformed");
|
|
563
|
+
}
|
|
564
|
+
return `opaque-v${String(IDENTITY_KEY_VERSION)}-${assetKey.slice(prefix.length)}`;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function isOriginOnlyCanonicalUrl(value: string): boolean {
|
|
568
|
+
try {
|
|
569
|
+
const url = new URL(value);
|
|
570
|
+
return (url.protocol === "http:" || url.protocol === "https:")
|
|
571
|
+
&& url.username === ""
|
|
572
|
+
&& url.password === ""
|
|
573
|
+
&& url.pathname === "/"
|
|
574
|
+
&& url.search === ""
|
|
575
|
+
&& url.hash === ""
|
|
576
|
+
&& url.href === `${url.origin}/`;
|
|
577
|
+
} catch {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** Validates the complete in-memory projection before any path or artifact write. */
|
|
583
|
+
export function isNormalizedProbeMetadata(metadata: ProbeMetadata): boolean {
|
|
584
|
+
try {
|
|
585
|
+
if (metadata.projection !== "youtube" && metadata.projection !== "opaque") return false;
|
|
586
|
+
const raw = metadata.acquisitionIdentity;
|
|
587
|
+
if (
|
|
588
|
+
raw.extractor.length === 0
|
|
589
|
+
|| raw.extractor.length > 512
|
|
590
|
+
|| raw.id.length === 0
|
|
591
|
+
|| raw.id.length > 512
|
|
592
|
+
|| hasControlCharacter(raw.extractor)
|
|
593
|
+
|| hasControlCharacter(raw.id)
|
|
594
|
+
|| !isWellFormedIdentity(raw.extractor)
|
|
595
|
+
|| !isWellFormedIdentity(raw.id)
|
|
596
|
+
) return false;
|
|
597
|
+
const v1AssetKey = sourceAssetKey(raw.extractor, raw.id);
|
|
598
|
+
const opaqueIdentity = metadata.opaqueYtDlpIdentity;
|
|
599
|
+
const authenticatedIdentity = metadata.authenticatedYtDlpIdentity;
|
|
600
|
+
if (opaqueIdentity !== undefined && authenticatedIdentity !== undefined) return false;
|
|
601
|
+
const assetKey = authenticatedIdentity !== undefined
|
|
602
|
+
? authenticatedYtDlpSourceAssetKey(authenticatedIdentity)
|
|
603
|
+
: opaqueIdentity === undefined
|
|
604
|
+
? v1AssetKey
|
|
605
|
+
: opaqueYtDlpSourceAssetKey(opaqueIdentity);
|
|
606
|
+
const expectedExtractor = metadata.projection === "youtube"
|
|
607
|
+
? OWNED_YOUTUBE_EXTRACTOR
|
|
608
|
+
: OPAQUE_EXTRACTOR;
|
|
609
|
+
const expectedId = metadata.projection === "youtube"
|
|
610
|
+
? raw.id
|
|
611
|
+
: authenticatedIdentity !== undefined
|
|
612
|
+
? authenticatedYtDlpSourceId(assetKey)
|
|
613
|
+
: opaqueIdentity === undefined
|
|
614
|
+
? opaqueSourceId(assetKey)
|
|
615
|
+
: opaqueYtDlpSourceId(assetKey);
|
|
616
|
+
if (
|
|
617
|
+
metadata.assetKey !== assetKey
|
|
618
|
+
|| metadata.extractor !== expectedExtractor
|
|
619
|
+
|| metadata.id !== expectedId
|
|
620
|
+
|| metadata.extractorDirectory !== sourceExtractorDirectory(expectedExtractor)
|
|
621
|
+
|| metadata.itemDirectory !== sourceItemDirectory(expectedId)
|
|
622
|
+
|| !isPortableIdentityDirectorySegment(metadata.extractorDirectory)
|
|
623
|
+
|| !isPortableIdentityDirectorySegment(metadata.itemDirectory)
|
|
624
|
+
) return false;
|
|
625
|
+
|
|
626
|
+
if (metadata.projection === "youtube") {
|
|
627
|
+
return raw.extractor === OWNED_YOUTUBE_EXTRACTOR
|
|
628
|
+
&& YOUTUBE_VIDEO_ID_PATTERN.test(raw.id)
|
|
629
|
+
&& metadata.canonicalUrl === youtubeCanonicalUrl(raw.id)
|
|
630
|
+
&& opaqueIdentity === undefined
|
|
631
|
+
&& authenticatedIdentity === undefined
|
|
632
|
+
&& metadata.diagnosticRedactions.length === 0;
|
|
633
|
+
}
|
|
634
|
+
const directHttpIdentity = raw.extractor === DIRECT_HTTP_EXTRACTOR
|
|
635
|
+
&& /^direct-http-v1-[0-9a-f]{64}$/u.test(raw.id);
|
|
636
|
+
if (directHttpIdentity) {
|
|
637
|
+
if (opaqueIdentity !== undefined || authenticatedIdentity !== undefined) return false;
|
|
638
|
+
} else if (authenticatedIdentity !== undefined) {
|
|
639
|
+
if (
|
|
640
|
+
authenticatedIdentity.profile !== YT_DLP_AUTH_IDENTITY_PROFILE
|
|
641
|
+
|| authenticatedIdentity.providerIdentitySha256 !== providerIdentitySha256(raw.extractor, raw.id)
|
|
642
|
+
|| !SHA256_PATTERN.test(authenticatedIdentity.requestedUrlSha256)
|
|
643
|
+
|| (authenticatedIdentity.accessMode !== "browser" && authenticatedIdentity.accessMode !== "ambient_config")
|
|
644
|
+
|| authenticatedIdentity.authContext.profile !== AUTH_CONTEXT_IDENTITY_PROFILE
|
|
645
|
+
|| !SHA256_PATTERN.test(authenticatedIdentity.authContext.sha256)
|
|
646
|
+
) return false;
|
|
647
|
+
} else {
|
|
648
|
+
if (
|
|
649
|
+
opaqueIdentity === undefined
|
|
650
|
+
|| opaqueIdentity.profile !== YT_DLP_OPAQUE_IDENTITY_PROFILE
|
|
651
|
+
|| opaqueIdentity.providerIdentitySha256 !== providerIdentitySha256(raw.extractor, raw.id)
|
|
652
|
+
|| !SHA256_PATTERN.test(opaqueIdentity.requestedUrlSha256)
|
|
653
|
+
) return false;
|
|
654
|
+
}
|
|
655
|
+
if (!isOriginOnlyCanonicalUrl(metadata.canonicalUrl)) return false;
|
|
656
|
+
if (
|
|
657
|
+
metadata.title !== undefined
|
|
658
|
+
|| metadata.uploader !== undefined
|
|
659
|
+
|| metadata.channel !== undefined
|
|
660
|
+
|| metadata.description !== undefined
|
|
661
|
+
|| metadata.license !== undefined
|
|
662
|
+
|| metadata.uploadDate !== undefined
|
|
663
|
+
|| metadata.timestamp !== undefined
|
|
664
|
+
|| metadata.durationSeconds !== undefined
|
|
665
|
+
|| metadata.originalLanguage !== undefined
|
|
666
|
+
) return false;
|
|
667
|
+
if (
|
|
668
|
+
!metadata.diagnosticRedactions.includes(raw.extractor)
|
|
669
|
+
|| !metadata.diagnosticRedactions.includes(raw.id)
|
|
670
|
+
|| metadata.diagnosticRedactions.length > 16
|
|
671
|
+
|| metadata.diagnosticRedactions.some((value) =>
|
|
672
|
+
value.length === 0
|
|
673
|
+
|| value.length > 64 * 1_024
|
|
674
|
+
|| hasControlCharacter(value)
|
|
675
|
+
|| !isWellFormedIdentity(value))
|
|
676
|
+
) return false;
|
|
677
|
+
return true;
|
|
678
|
+
} catch {
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
/** A disjoint identity for one focused view of a source archive. */
|
|
684
|
+
export function variantAssetKey(
|
|
685
|
+
sourceKey: string,
|
|
686
|
+
variantSegments: readonly string[],
|
|
687
|
+
): string {
|
|
688
|
+
return `variant-v${String(IDENTITY_KEY_VERSION)}-${tupleDigest("variant", [sourceKey, ...variantSegments])}`;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
export function parseProbeMetadata(
|
|
692
|
+
value: unknown,
|
|
693
|
+
requestedUrl: string,
|
|
694
|
+
authorization?: YtDlpAuthorizationIdentityInput,
|
|
695
|
+
): ParseProbeResult {
|
|
696
|
+
if (!isRecord(value)) return invalidProbe("yt-dlp probe did not return an object");
|
|
697
|
+
const unsupported = unsupportedProbeEnvelope(value);
|
|
698
|
+
if (unsupported !== null) return unsupported;
|
|
699
|
+
const id = boundedIdentityString(value, "id", 512);
|
|
700
|
+
const extractor = boundedIdentityString(value, "extractor_key", 512)
|
|
701
|
+
?? boundedIdentityString(value, "extractor", 512);
|
|
702
|
+
if (id === undefined || extractor === undefined) {
|
|
703
|
+
return invalidProbe("yt-dlp probe is missing a bounded extractor identity");
|
|
704
|
+
}
|
|
705
|
+
const candidateUrls = [
|
|
706
|
+
boundedString(value, "webpage_url", 8_192),
|
|
707
|
+
boundedString(value, "original_url", 8_192),
|
|
708
|
+
requestedUrl,
|
|
709
|
+
] as const;
|
|
710
|
+
if (
|
|
711
|
+
authorization !== undefined
|
|
712
|
+
&& (
|
|
713
|
+
(authorization.mode !== "browser" && authorization.mode !== "ambient_config")
|
|
714
|
+
|| !SHA256_PATTERN.test(authorization.contextSha256)
|
|
715
|
+
)
|
|
716
|
+
) return invalidProbe("yt-dlp authorization identity is malformed");
|
|
717
|
+
const projection: SourceProjection = authorization === undefined
|
|
718
|
+
&& isOwnedYouTubeSource(extractor, id, candidateUrls)
|
|
719
|
+
? "youtube"
|
|
720
|
+
: "opaque";
|
|
721
|
+
const canonicalUrl = canonicalSourceUrl(candidateUrls, projection, id);
|
|
722
|
+
if (canonicalUrl === undefined) return invalidProbe("yt-dlp probe is missing a safe canonical URL");
|
|
723
|
+
let opaqueIdentity: OpaqueYtDlpIdentity | undefined;
|
|
724
|
+
let authenticatedIdentity: AuthenticatedYtDlpIdentity | undefined;
|
|
725
|
+
if (projection === "opaque") {
|
|
726
|
+
let requestDigest: string;
|
|
727
|
+
try {
|
|
728
|
+
requestDigest = requestedUrlSha256(requestedUrl);
|
|
729
|
+
} catch {
|
|
730
|
+
return invalidProbe("yt-dlp probe is missing a safe requested URL");
|
|
731
|
+
}
|
|
732
|
+
const providerDigest = providerIdentitySha256(extractor, id);
|
|
733
|
+
if (authorization === undefined) {
|
|
734
|
+
opaqueIdentity = {
|
|
735
|
+
profile: YT_DLP_OPAQUE_IDENTITY_PROFILE,
|
|
736
|
+
providerIdentitySha256: providerDigest,
|
|
737
|
+
requestedUrlSha256: requestDigest,
|
|
738
|
+
};
|
|
739
|
+
} else {
|
|
740
|
+
authenticatedIdentity = {
|
|
741
|
+
profile: YT_DLP_AUTH_IDENTITY_PROFILE,
|
|
742
|
+
providerIdentitySha256: providerDigest,
|
|
743
|
+
requestedUrlSha256: requestDigest,
|
|
744
|
+
accessMode: authorization.mode,
|
|
745
|
+
authContext: {
|
|
746
|
+
profile: AUTH_CONTEXT_IDENTITY_PROFILE,
|
|
747
|
+
sha256: authorization.contextSha256,
|
|
748
|
+
},
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
const assetKey = authenticatedIdentity !== undefined
|
|
753
|
+
? authenticatedYtDlpSourceAssetKey(authenticatedIdentity)
|
|
754
|
+
: opaqueIdentity === undefined
|
|
755
|
+
? sourceAssetKey(extractor, id)
|
|
756
|
+
: opaqueYtDlpSourceAssetKey(opaqueIdentity);
|
|
757
|
+
const projectedExtractor = projection === "youtube" ? OWNED_YOUTUBE_EXTRACTOR : OPAQUE_EXTRACTOR;
|
|
758
|
+
const projectedId = projection === "youtube"
|
|
759
|
+
? id
|
|
760
|
+
: authenticatedIdentity === undefined
|
|
761
|
+
? opaqueYtDlpSourceId(assetKey)
|
|
762
|
+
: authenticatedYtDlpSourceId(assetKey);
|
|
763
|
+
const extractorPath = sourceExtractorDirectory(projectedExtractor);
|
|
764
|
+
const itemPath = sourceItemDirectory(projectedId);
|
|
765
|
+
// Descriptive fields are provider-owned public metadata only when the source
|
|
766
|
+
// is on Ghostget media's narrow ownership allowlist. Generic extractors frequently
|
|
767
|
+
// derive these strings from signed URL basenames.
|
|
768
|
+
const title = projection === "youtube" ? boundedString(value, "title", 2_048) : undefined;
|
|
769
|
+
const uploader = projection === "youtube" ? boundedString(value, "uploader", 1_024) : undefined;
|
|
770
|
+
const channel = projection === "youtube" ? boundedString(value, "channel", 1_024) : undefined;
|
|
771
|
+
const description = projection === "youtube" ? boundedString(value, "description", 64 * 1_024) : undefined;
|
|
772
|
+
const license = projection === "youtube" ? boundedString(value, "license", 1_024) : undefined;
|
|
773
|
+
const uploadDate = projection === "youtube" ? boundedString(value, "upload_date", 32) : undefined;
|
|
774
|
+
const timestamp = projection === "youtube" ? finiteNumber(value, "timestamp") : undefined;
|
|
775
|
+
const durationSeconds = projection === "youtube" ? finiteNumber(value, "duration") : undefined;
|
|
776
|
+
const originalLanguage = projection === "youtube" ? boundedString(value, "language", 128) : undefined;
|
|
777
|
+
return {
|
|
778
|
+
ok: true,
|
|
779
|
+
metadata: {
|
|
780
|
+
acquisitionIdentity: { extractor, id },
|
|
781
|
+
projection,
|
|
782
|
+
diagnosticRedactions: projection === "opaque"
|
|
783
|
+
? diagnosticRedactions(value, { extractor, id })
|
|
784
|
+
: [],
|
|
785
|
+
id: projectedId,
|
|
786
|
+
extractor: projectedExtractor,
|
|
787
|
+
extractorDirectory: extractorPath,
|
|
788
|
+
itemDirectory: itemPath,
|
|
789
|
+
assetKey,
|
|
790
|
+
...(opaqueIdentity === undefined ? {} : { opaqueYtDlpIdentity: opaqueIdentity }),
|
|
791
|
+
...(authenticatedIdentity === undefined
|
|
792
|
+
? {}
|
|
793
|
+
: { authenticatedYtDlpIdentity: authenticatedIdentity }),
|
|
794
|
+
canonicalUrl,
|
|
795
|
+
...(title === undefined ? {} : { title }),
|
|
796
|
+
...(uploader === undefined ? {} : { uploader }),
|
|
797
|
+
...(channel === undefined ? {} : { channel }),
|
|
798
|
+
...(description === undefined ? {} : { description }),
|
|
799
|
+
...(license === undefined ? {} : { license }),
|
|
800
|
+
...(uploadDate === undefined ? {} : { uploadDate }),
|
|
801
|
+
...(timestamp === undefined ? {} : { timestamp }),
|
|
802
|
+
...(durationSeconds === undefined ? {} : { durationSeconds }),
|
|
803
|
+
...(originalLanguage === undefined ? {} : { originalLanguage }),
|
|
804
|
+
manualCaptionLanguages: languageKeys(value["subtitles"]),
|
|
805
|
+
automaticCaptionLanguages: languageKeys(value["automatic_captions"]),
|
|
806
|
+
},
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Builds the complete provider metadata artifact from Ghostget media's owned probe model.
|
|
812
|
+
* Unknown yt-dlp fields never cross this boundary, including formats, fragment
|
|
813
|
+
* URLs, request headers, cookies, and extractor-private state.
|
|
814
|
+
*/
|
|
815
|
+
export function createProviderMetadataDocument(
|
|
816
|
+
metadata: ProbeMetadata,
|
|
817
|
+
): ProviderMetadataDocument {
|
|
818
|
+
if (!isNormalizedProbeMetadata(metadata)) {
|
|
819
|
+
throw new TypeError("provider metadata projection is inconsistent");
|
|
820
|
+
}
|
|
821
|
+
return {
|
|
822
|
+
schemaVersion: PROVIDER_METADATA_SCHEMA_VERSION,
|
|
823
|
+
sourceAssetKey: metadata.assetKey,
|
|
824
|
+
source: {
|
|
825
|
+
extractor: metadata.extractor,
|
|
826
|
+
id: metadata.id,
|
|
827
|
+
canonicalUrl: metadata.canonicalUrl,
|
|
828
|
+
...(metadata.title === undefined ? {} : { title: metadata.title }),
|
|
829
|
+
...(metadata.uploader === undefined ? {} : { uploader: metadata.uploader }),
|
|
830
|
+
...(metadata.channel === undefined ? {} : { channel: metadata.channel }),
|
|
831
|
+
...(metadata.description === undefined ? {} : { description: metadata.description }),
|
|
832
|
+
...(metadata.license === undefined ? {} : { license: metadata.license }),
|
|
833
|
+
...(metadata.uploadDate === undefined ? {} : { uploadDate: metadata.uploadDate }),
|
|
834
|
+
...(metadata.timestamp === undefined ? {} : { timestamp: metadata.timestamp }),
|
|
835
|
+
...(metadata.durationSeconds === undefined
|
|
836
|
+
? {}
|
|
837
|
+
: { durationSeconds: metadata.durationSeconds }),
|
|
838
|
+
},
|
|
839
|
+
captions: {
|
|
840
|
+
...(metadata.originalLanguage === undefined
|
|
841
|
+
? {}
|
|
842
|
+
: { originalLanguage: metadata.originalLanguage }),
|
|
843
|
+
manualLanguages: [...metadata.manualCaptionLanguages],
|
|
844
|
+
automaticLanguages: [...metadata.automaticCaptionLanguages],
|
|
845
|
+
},
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
export function renderProviderMetadataJson(metadata: ProbeMetadata): string {
|
|
850
|
+
return `${JSON.stringify(createProviderMetadataDocument(metadata), null, 2)}\n`;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
function languageFamily(value: string): string {
|
|
854
|
+
return value.toLowerCase().split("-")[0] ?? "";
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
export function selectCaption(metadata: ProbeMetadata, requestedLanguage: string): CaptionSelection | null {
|
|
858
|
+
const requested = requestedLanguage.toLowerCase();
|
|
859
|
+
const requestedFamily = languageFamily(requested);
|
|
860
|
+
const original = metadata.originalLanguage?.toLowerCase();
|
|
861
|
+
const originalFamily = original === undefined ? undefined : languageFamily(original);
|
|
862
|
+
const relevance = (language: string): number => {
|
|
863
|
+
const normalized = language.toLowerCase();
|
|
864
|
+
if (normalized === requested) return 0;
|
|
865
|
+
if (languageFamily(normalized) === requestedFamily) return 1;
|
|
866
|
+
if (original !== undefined && normalized === original) return 2;
|
|
867
|
+
if (originalFamily !== undefined && languageFamily(normalized) === originalFamily) return 3;
|
|
868
|
+
return 4;
|
|
869
|
+
};
|
|
870
|
+
|
|
871
|
+
let best: Readonly<{
|
|
872
|
+
selection: CaptionSelection;
|
|
873
|
+
relevance: number;
|
|
874
|
+
quality: number;
|
|
875
|
+
order: number;
|
|
876
|
+
}> | null = null;
|
|
877
|
+
let order = 0;
|
|
878
|
+
for (const [source, languages] of [
|
|
879
|
+
["manual", metadata.manualCaptionLanguages],
|
|
880
|
+
["automatic", metadata.automaticCaptionLanguages],
|
|
881
|
+
] as const) {
|
|
882
|
+
const quality = source === "manual" ? 0 : 1;
|
|
883
|
+
for (const language of languages) {
|
|
884
|
+
const candidate = {
|
|
885
|
+
selection: { source, language },
|
|
886
|
+
relevance: relevance(language),
|
|
887
|
+
quality,
|
|
888
|
+
order,
|
|
889
|
+
} as const;
|
|
890
|
+
order += 1;
|
|
891
|
+
if (
|
|
892
|
+
best === null
|
|
893
|
+
|| candidate.relevance < best.relevance
|
|
894
|
+
|| (candidate.relevance === best.relevance && candidate.quality < best.quality)
|
|
895
|
+
|| (
|
|
896
|
+
candidate.relevance === best.relevance
|
|
897
|
+
&& candidate.quality === best.quality
|
|
898
|
+
&& candidate.order < best.order
|
|
899
|
+
)
|
|
900
|
+
) {
|
|
901
|
+
best = candidate;
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
return best?.selection ?? null;
|
|
906
|
+
}
|