@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,948 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IdempotencyKind,
|
|
3
|
+
InputSchema,
|
|
4
|
+
OfficialProviderId,
|
|
5
|
+
OperationInput,
|
|
6
|
+
OperationRisk,
|
|
7
|
+
ProviderRecipe,
|
|
8
|
+
} from "./model";
|
|
9
|
+
import type { ProviderPluginOperationName } from "./provider-plugin-identifiers";
|
|
10
|
+
import type { ProviderPluginContractStateV1 } from "./provider-plugin";
|
|
11
|
+
|
|
12
|
+
/** Provider-owned bounded coverage label; the kernel does not close this set. */
|
|
13
|
+
export type ProviderCoverage = string;
|
|
14
|
+
|
|
15
|
+
export type ProviderContract = {
|
|
16
|
+
readonly provider: OfficialProviderId;
|
|
17
|
+
readonly operation: ProviderPluginOperationName;
|
|
18
|
+
readonly contractVersion: number;
|
|
19
|
+
readonly risk: OperationRisk;
|
|
20
|
+
readonly sideEffect: string;
|
|
21
|
+
readonly idempotency: IdempotencyKind;
|
|
22
|
+
readonly dedupeWindowMs: number;
|
|
23
|
+
readonly input: InputSchema;
|
|
24
|
+
readonly state: ProviderPluginContractStateV1;
|
|
25
|
+
/** Any one complete set authorizes the base operation. */
|
|
26
|
+
readonly requiredScopeSets: readonly (readonly string[])[];
|
|
27
|
+
readonly dispatch: "none" | "single" | "thread-items";
|
|
28
|
+
readonly coverage: readonly ProviderCoverage[];
|
|
29
|
+
/** Fixed endpoint families and semantic caveats bound into confirmation plans. */
|
|
30
|
+
readonly implementation: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
type ProviderContractDefinition = Omit<
|
|
34
|
+
ProviderContract,
|
|
35
|
+
"sideEffect" | "idempotency" | "dedupeWindowMs" | "state"
|
|
36
|
+
>;
|
|
37
|
+
|
|
38
|
+
const string = (
|
|
39
|
+
description: string,
|
|
40
|
+
options: {
|
|
41
|
+
readonly minLength?: number;
|
|
42
|
+
readonly maxLength?: number;
|
|
43
|
+
readonly enum?: readonly string[];
|
|
44
|
+
} = {},
|
|
45
|
+
) => ({ type: "string", description, ...options }) as const;
|
|
46
|
+
|
|
47
|
+
const number = (
|
|
48
|
+
description: string,
|
|
49
|
+
minimum: number,
|
|
50
|
+
maximum: number,
|
|
51
|
+
) => ({ type: "number", description, minimum, maximum }) as const;
|
|
52
|
+
|
|
53
|
+
const boolean = (description: string, allowed?: readonly boolean[]) => ({
|
|
54
|
+
type: "boolean",
|
|
55
|
+
description,
|
|
56
|
+
...(allowed === undefined ? {} : { enum: allowed }),
|
|
57
|
+
}) as const;
|
|
58
|
+
|
|
59
|
+
const file = (
|
|
60
|
+
description: string,
|
|
61
|
+
maxBytes: number,
|
|
62
|
+
mediaTypes: readonly string[],
|
|
63
|
+
) => ({ type: "file", description, maxBytes, mediaTypes }) as const;
|
|
64
|
+
|
|
65
|
+
const array = <T extends ReturnType<typeof string> | ReturnType<typeof number> | ReturnType<typeof file>>(
|
|
66
|
+
description: string,
|
|
67
|
+
items: T,
|
|
68
|
+
minItems: number,
|
|
69
|
+
maxItems: number,
|
|
70
|
+
) => ({ type: "array", description, items, minItems, maxItems }) as const;
|
|
71
|
+
|
|
72
|
+
function isFileInputValue(value: unknown): boolean {
|
|
73
|
+
return typeof value === "object"
|
|
74
|
+
&& value !== null
|
|
75
|
+
&& !Array.isArray(value)
|
|
76
|
+
&& (value as { readonly kind?: unknown }).kind === "file"
|
|
77
|
+
&& typeof (value as { readonly reference?: unknown }).reference === "string";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const linkedinDefinitions = {
|
|
81
|
+
"contacts.list": {
|
|
82
|
+
provider: "linkedin",
|
|
83
|
+
operation: "contacts.list",
|
|
84
|
+
contractVersion: 1,
|
|
85
|
+
risk: "R1",
|
|
86
|
+
input: {
|
|
87
|
+
properties: {
|
|
88
|
+
start: number("Zero-based first-degree connection offset; defaults to 0", 0, 100_000),
|
|
89
|
+
count: number("Bounded first-degree connection count; defaults to 10", 1, 50),
|
|
90
|
+
},
|
|
91
|
+
required: [],
|
|
92
|
+
},
|
|
93
|
+
requiredScopeSets: [["r_1st_connections", "r_liteprofile"]],
|
|
94
|
+
dispatch: "none",
|
|
95
|
+
coverage: [
|
|
96
|
+
"first-degree-connections",
|
|
97
|
+
"authenticated-viewer-binding",
|
|
98
|
+
"connection-localized-names",
|
|
99
|
+
"locale-selection-evidence",
|
|
100
|
+
"connection-set-total",
|
|
101
|
+
"offset-paging",
|
|
102
|
+
"unavailable-message-statistics",
|
|
103
|
+
],
|
|
104
|
+
implementation: "pre-network scope and person-URN validation, then subject-bound GET /v2/me?projection=(id) and an exact byte comparison between the authenticated person URN and OAuth locator before GET /v2/connections?q=viewer&projection=(elements(*(to~)),paging); exact offset-page, decorated person identity, locale-selection evidence, and total-count validation; restricted r_1st_connections and r_liteprofile approval required; no inbox or web fallback; message statistics are explicitly unavailable",
|
|
105
|
+
},
|
|
106
|
+
"posts.read": {
|
|
107
|
+
provider: "linkedin",
|
|
108
|
+
operation: "posts.read",
|
|
109
|
+
contractVersion: 1,
|
|
110
|
+
risk: "R1",
|
|
111
|
+
input: {
|
|
112
|
+
properties: {
|
|
113
|
+
mode: string("Read one post or list posts by one author", { enum: ["one", "author"] }),
|
|
114
|
+
post_urn: string("Exact LinkedIn share or UGC Post URN", { minLength: 1, maxLength: 500 }),
|
|
115
|
+
author: string("Exact person or organization author URN", { minLength: 1, maxLength: 500 }),
|
|
116
|
+
start: number("Zero-based author-list offset", 0, 100_000),
|
|
117
|
+
count: number("Bounded result count", 1, 100),
|
|
118
|
+
sort: string("Author-list ordering", { enum: ["LAST_MODIFIED", "CREATED"] }),
|
|
119
|
+
view: string("Reader-visible or author-visible representation", { enum: ["READER", "AUTHOR"] }),
|
|
120
|
+
},
|
|
121
|
+
required: ["mode"],
|
|
122
|
+
},
|
|
123
|
+
requiredScopeSets: [["r_member_social"], ["r_organization_social"]],
|
|
124
|
+
dispatch: "none",
|
|
125
|
+
coverage: ["post", "author-posts"],
|
|
126
|
+
implementation: "GET /rest/posts/{urn}; FINDER /rest/posts?q=author; LinkedIn-Version 202606; no home-feed reconstruction",
|
|
127
|
+
},
|
|
128
|
+
"posts.publish": {
|
|
129
|
+
provider: "linkedin",
|
|
130
|
+
operation: "posts.publish",
|
|
131
|
+
contractVersion: 1,
|
|
132
|
+
risk: "R3",
|
|
133
|
+
input: {
|
|
134
|
+
properties: {
|
|
135
|
+
author: string("Exact authenticated person or administered organization URN", { minLength: 1, maxLength: 500 }),
|
|
136
|
+
body: string("Exact post commentary", { minLength: 1, maxLength: 3_000 }),
|
|
137
|
+
visibility: string("Post visibility", { enum: ["PUBLIC", "CONNECTIONS"] }),
|
|
138
|
+
media: array(
|
|
139
|
+
"Images, or one video/document",
|
|
140
|
+
file("One reviewed post attachment; video must be MP4 and at least 75,000 bytes, with duration and codec validated by LinkedIn processing", 500 * 1024 * 1024, [
|
|
141
|
+
"image/jpeg",
|
|
142
|
+
"image/png",
|
|
143
|
+
"image/gif",
|
|
144
|
+
"video/mp4",
|
|
145
|
+
"application/pdf",
|
|
146
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
147
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
148
|
+
]),
|
|
149
|
+
1,
|
|
150
|
+
20,
|
|
151
|
+
),
|
|
152
|
+
media_title: string("Required title for a document; optional video title", { minLength: 1, maxLength: 200 }),
|
|
153
|
+
alt_text: string("Optional image alternative text", { minLength: 1, maxLength: 4_000 }),
|
|
154
|
+
article_url: string("Optional exact HTTPS source for an external article-card post", { minLength: 1, maxLength: 8_192 }),
|
|
155
|
+
article_title: string("Optional external article-card title", { minLength: 1, maxLength: 399 }),
|
|
156
|
+
article_description: string("Optional external article-card description", { minLength: 1, maxLength: 4_000 }),
|
|
157
|
+
},
|
|
158
|
+
required: ["author", "body"],
|
|
159
|
+
},
|
|
160
|
+
requiredScopeSets: [["w_member_social"], ["w_organization_social"]],
|
|
161
|
+
dispatch: "single",
|
|
162
|
+
coverage: ["write-result"],
|
|
163
|
+
implementation: "initialize fixed LinkedIn image/document/video upload; upload bytes without OAuth on signed video parts; poll assets to AVAILABLE with actor-specific read scope; finalize video; POST /rest/posts; external article cards require explicit source/title and omit thumbnail; no URL scraping",
|
|
164
|
+
},
|
|
165
|
+
"posts.repost": {
|
|
166
|
+
provider: "linkedin",
|
|
167
|
+
operation: "posts.repost",
|
|
168
|
+
contractVersion: 1,
|
|
169
|
+
risk: "R3",
|
|
170
|
+
input: {
|
|
171
|
+
properties: {
|
|
172
|
+
author: string("Exact authenticated person or administered organization URN", { minLength: 1, maxLength: 500 }),
|
|
173
|
+
target_urn: string("Exact LinkedIn share or UGC Post URN to reshare", { minLength: 1, maxLength: 500 }),
|
|
174
|
+
body: string("Exact reshare commentary", { minLength: 1, maxLength: 3_000 }),
|
|
175
|
+
visibility: string("Reshare visibility", { enum: ["PUBLIC", "CONNECTIONS"] }),
|
|
176
|
+
},
|
|
177
|
+
required: ["author", "target_urn", "body"],
|
|
178
|
+
},
|
|
179
|
+
requiredScopeSets: [["w_member_social"], ["w_organization_social"]],
|
|
180
|
+
dispatch: "single",
|
|
181
|
+
coverage: ["write-result"],
|
|
182
|
+
implementation: "POST /rest/posts with exact reshareContext.parent; main-feed distribution; actor- and subject-bound",
|
|
183
|
+
},
|
|
184
|
+
"comments.read": {
|
|
185
|
+
provider: "linkedin",
|
|
186
|
+
operation: "comments.read",
|
|
187
|
+
contractVersion: 1,
|
|
188
|
+
risk: "R1",
|
|
189
|
+
input: {
|
|
190
|
+
properties: {
|
|
191
|
+
target_urn: string("Exact post or composite parent-comment URN", { minLength: 1, maxLength: 1_000 }),
|
|
192
|
+
start: number("Zero-based comment offset", 0, 100_000),
|
|
193
|
+
count: number("Bounded comment count", 1, 100),
|
|
194
|
+
},
|
|
195
|
+
required: ["target_urn"],
|
|
196
|
+
},
|
|
197
|
+
requiredScopeSets: [["r_member_social_feed"], ["r_organization_social_feed"]],
|
|
198
|
+
dispatch: "none",
|
|
199
|
+
coverage: ["comments"],
|
|
200
|
+
implementation: "GET /rest/socialActions/{target}/comments; one level per request; explicit parent query for replies",
|
|
201
|
+
},
|
|
202
|
+
"comments.create": {
|
|
203
|
+
provider: "linkedin",
|
|
204
|
+
operation: "comments.create",
|
|
205
|
+
contractVersion: 1,
|
|
206
|
+
risk: "R3",
|
|
207
|
+
input: {
|
|
208
|
+
properties: {
|
|
209
|
+
actor: string("Exact authenticated person or administered organization URN", { minLength: 1, maxLength: 500 }),
|
|
210
|
+
target_urn: string("Exact post URN used in the endpoint path", { minLength: 1, maxLength: 1_000 }),
|
|
211
|
+
object_urn: string("Exact root activity/object URN required by LinkedIn", { minLength: 1, maxLength: 1_000 }),
|
|
212
|
+
body: string("Exact comment text", { minLength: 1, maxLength: 500 }),
|
|
213
|
+
},
|
|
214
|
+
required: ["actor", "target_urn", "object_urn", "body"],
|
|
215
|
+
},
|
|
216
|
+
requiredScopeSets: [["w_member_social_feed"], ["w_organization_social_feed"]],
|
|
217
|
+
dispatch: "single",
|
|
218
|
+
coverage: ["write-result"],
|
|
219
|
+
implementation: "POST /rest/socialActions/{post}/comments with explicit actor and object URNs; text only",
|
|
220
|
+
},
|
|
221
|
+
"replies.create": {
|
|
222
|
+
provider: "linkedin",
|
|
223
|
+
operation: "replies.create",
|
|
224
|
+
contractVersion: 1,
|
|
225
|
+
risk: "R3",
|
|
226
|
+
input: {
|
|
227
|
+
properties: {
|
|
228
|
+
actor: string("Exact authenticated person or administered organization URN", { minLength: 1, maxLength: 500 }),
|
|
229
|
+
parent_comment_urn: string("Exact composite parent-comment URN", { minLength: 1, maxLength: 1_000 }),
|
|
230
|
+
object_urn: string("Exact root post/object URN", { minLength: 1, maxLength: 1_000 }),
|
|
231
|
+
body: string("Exact reply text", { minLength: 1, maxLength: 500 }),
|
|
232
|
+
},
|
|
233
|
+
required: ["actor", "parent_comment_urn", "object_urn", "body"],
|
|
234
|
+
},
|
|
235
|
+
requiredScopeSets: [["w_member_social_feed"], ["w_organization_social_feed"]],
|
|
236
|
+
dispatch: "single",
|
|
237
|
+
coverage: ["write-result"],
|
|
238
|
+
implementation: "POST /rest/socialActions/{parent-comment}/comments with explicit parentComment and object; text only",
|
|
239
|
+
},
|
|
240
|
+
"reactions.set": {
|
|
241
|
+
provider: "linkedin",
|
|
242
|
+
operation: "reactions.set",
|
|
243
|
+
contractVersion: 1,
|
|
244
|
+
risk: "R2",
|
|
245
|
+
input: {
|
|
246
|
+
properties: {
|
|
247
|
+
actor: string("Exact authenticated person or administered organization URN", { minLength: 1, maxLength: 500 }),
|
|
248
|
+
target_urn: string("Exact post or comment URN", { minLength: 1, maxLength: 1_000 }),
|
|
249
|
+
reaction: string("Required reaction type when enabled is true; omitted when false", { enum: ["LIKE", "PRAISE", "EMPATHY", "INTEREST", "APPRECIATION", "ENTERTAINMENT"] }),
|
|
250
|
+
enabled: boolean("Create the named reaction when true; clear any current actor reaction when false"),
|
|
251
|
+
},
|
|
252
|
+
required: ["actor", "target_urn", "enabled"],
|
|
253
|
+
},
|
|
254
|
+
requiredScopeSets: [["w_member_social_feed"], ["w_organization_social_feed"]],
|
|
255
|
+
dispatch: "single",
|
|
256
|
+
coverage: ["write-result"],
|
|
257
|
+
implementation: "POST /rest/reactions?actor=... for a named reaction; DELETE composite reaction key clears any current actor reaction",
|
|
258
|
+
},
|
|
259
|
+
} as const satisfies Readonly<Record<string, ProviderContractDefinition>>;
|
|
260
|
+
|
|
261
|
+
const xDefinitions = {
|
|
262
|
+
"feeds.read": {
|
|
263
|
+
provider: "x",
|
|
264
|
+
operation: "feeds.read",
|
|
265
|
+
contractVersion: 1,
|
|
266
|
+
risk: "R1",
|
|
267
|
+
input: {
|
|
268
|
+
properties: {
|
|
269
|
+
feed: string("Exact official timeline or discovery surface", { enum: ["home-reverse-chronological", "user", "mentions", "list", "recent-search", "bookmarks"] }),
|
|
270
|
+
user_id: string("Exact 1-19 digit X user ID for user or mentions feeds", { minLength: 1, maxLength: 19 }),
|
|
271
|
+
list_id: string("Exact 1-19 digit X List ID", { minLength: 1, maxLength: 19 }),
|
|
272
|
+
query: string("Recent-search query covering at most seven days", { minLength: 1, maxLength: 512 }),
|
|
273
|
+
cursor: string("Opaque provider pagination token", { minLength: 1, maxLength: 4_096 }),
|
|
274
|
+
limit: number("Bounded result count", 1, 100),
|
|
275
|
+
since_id: string("Return posts newer than this 1-19 digit post ID", { minLength: 1, maxLength: 19 }),
|
|
276
|
+
until_id: string("Return posts older than this 1-19 digit post ID", { minLength: 1, maxLength: 19 }),
|
|
277
|
+
start_time: string("Inclusive ISO-8601 start time", { minLength: 20, maxLength: 40 }),
|
|
278
|
+
end_time: string("Exclusive ISO-8601 end time", { minLength: 20, maxLength: 40 }),
|
|
279
|
+
exclude_replies: boolean("Exclude replies where the endpoint supports it"),
|
|
280
|
+
exclude_reposts: boolean("Exclude reposts where the endpoint supports it"),
|
|
281
|
+
sort: string("Recent-search ordering", { enum: ["recency", "relevancy"] }),
|
|
282
|
+
},
|
|
283
|
+
required: ["feed"],
|
|
284
|
+
},
|
|
285
|
+
requiredScopeSets: [["tweet.read", "users.read"]],
|
|
286
|
+
dispatch: "none",
|
|
287
|
+
coverage: ["home-reverse-chronological", "user-posts", "mentions", "list-posts", "recent-search-7-days", "bookmarks"],
|
|
288
|
+
implementation: "fixed X v2 reverse-chron/user/mentions/List/recent-search/bookmark endpoints with endpoint-specific no-skip page minima; never For You",
|
|
289
|
+
},
|
|
290
|
+
"posts.read": {
|
|
291
|
+
provider: "x",
|
|
292
|
+
operation: "posts.read",
|
|
293
|
+
contractVersion: 1,
|
|
294
|
+
risk: "R1",
|
|
295
|
+
input: {
|
|
296
|
+
properties: {
|
|
297
|
+
post_ids: array("Unique exact X post IDs", string("One exact 1-19 digit post ID", { minLength: 1, maxLength: 19 }), 1, 100),
|
|
298
|
+
},
|
|
299
|
+
required: ["post_ids"],
|
|
300
|
+
},
|
|
301
|
+
requiredScopeSets: [["tweet.read", "users.read"]],
|
|
302
|
+
dispatch: "none",
|
|
303
|
+
coverage: ["post"],
|
|
304
|
+
implementation: "GET /2/tweets by bounded IDs with fixed rich fields and expansions",
|
|
305
|
+
},
|
|
306
|
+
"comments.read": {
|
|
307
|
+
provider: "x",
|
|
308
|
+
operation: "comments.read",
|
|
309
|
+
contractVersion: 1,
|
|
310
|
+
risk: "R1",
|
|
311
|
+
input: {
|
|
312
|
+
properties: {
|
|
313
|
+
post_id: string("Exact 1-19 digit root X post ID", { minLength: 1, maxLength: 19 }),
|
|
314
|
+
window: string("Recent seven-day search or entitled full-archive search", { enum: ["recent-7-days", "full-archive"] }),
|
|
315
|
+
cursor: string("Opaque reply-search next token", { minLength: 1, maxLength: 4_096 }),
|
|
316
|
+
limit: number("Bounded reply count", 10, 100),
|
|
317
|
+
},
|
|
318
|
+
required: ["post_id"],
|
|
319
|
+
},
|
|
320
|
+
requiredScopeSets: [["tweet.read", "users.read"]],
|
|
321
|
+
dispatch: "none",
|
|
322
|
+
coverage: ["replies-recent-search-7-days", "replies-full-archive-search"],
|
|
323
|
+
implementation: "GET recent or entitled full-archive search query conversation_id; cursor-preserving and never labeled complete",
|
|
324
|
+
},
|
|
325
|
+
"messaging.list": {
|
|
326
|
+
provider: "x",
|
|
327
|
+
operation: "messaging.list",
|
|
328
|
+
contractVersion: 1,
|
|
329
|
+
risk: "R1",
|
|
330
|
+
input: {
|
|
331
|
+
properties: {
|
|
332
|
+
view: string("Legacy DM events, encrypted Chat conversations, or encrypted Chat events", { enum: ["all", "participant", "conversation", "chat-conversations", "chat-events"] }),
|
|
333
|
+
target_id: string("Exact participant, legacy DM conversation, or Chat conversation ID selected by view", { minLength: 1, maxLength: 39 }),
|
|
334
|
+
cursor: string("Opaque legacy DM or Chat pagination token", { minLength: 1, maxLength: 4_096 }),
|
|
335
|
+
limit: number("Bounded event or conversation count", 1, 100),
|
|
336
|
+
},
|
|
337
|
+
required: ["view"],
|
|
338
|
+
},
|
|
339
|
+
requiredScopeSets: [["dm.read", "tweet.read", "users.read"]],
|
|
340
|
+
dispatch: "none",
|
|
341
|
+
coverage: ["dm-events-30-days", "chat-conversations", "chat-encrypted-events"],
|
|
342
|
+
implementation: "subject-bound GET legacy DM events or encrypted Chat conversations/events; Chat exposes metadata, request presence, and ciphertext envelopes but not plaintext or inbox folders",
|
|
343
|
+
},
|
|
344
|
+
"messaging.read": {
|
|
345
|
+
provider: "x",
|
|
346
|
+
operation: "messaging.read",
|
|
347
|
+
contractVersion: 1,
|
|
348
|
+
risk: "R1",
|
|
349
|
+
input: {
|
|
350
|
+
properties: {
|
|
351
|
+
event_id: string("Exact 1-19 digit legacy DM event ID", { minLength: 1, maxLength: 19 }),
|
|
352
|
+
},
|
|
353
|
+
required: ["event_id"],
|
|
354
|
+
},
|
|
355
|
+
requiredScopeSets: [["dm.read", "tweet.read", "users.read"]],
|
|
356
|
+
dispatch: "none",
|
|
357
|
+
coverage: ["dm-event"],
|
|
358
|
+
implementation: "subject-bound GET /2/dm_events/{event}; exact response identity; legacy DM only, not encrypted Chat",
|
|
359
|
+
},
|
|
360
|
+
"messaging.send": {
|
|
361
|
+
provider: "x",
|
|
362
|
+
operation: "messaging.send",
|
|
363
|
+
contractVersion: 1,
|
|
364
|
+
risk: "R3",
|
|
365
|
+
input: {
|
|
366
|
+
properties: {
|
|
367
|
+
target_kind: string("Send to one participant or existing legacy DM conversation", { enum: ["participant", "conversation"] }),
|
|
368
|
+
target_id: string("Exact participant or legacy DM conversation ID", { minLength: 1, maxLength: 39 }),
|
|
369
|
+
body: string("Optional exact legacy DM text; text or media is required", { minLength: 1, maxLength: 1_000 }),
|
|
370
|
+
media: file("Optional single legacy DM image, GIF, or video", 512 * 1024 * 1024, ["image/jpeg", "image/png", "image/gif", "video/mp4"]),
|
|
371
|
+
media_alt_text: string("Optional accessibility text applied to reviewed JPEG/PNG DM media", { minLength: 1, maxLength: 1_000 }),
|
|
372
|
+
recipient_opted_in: boolean("Required acknowledgement that the recipient opted into automated contact", [true]),
|
|
373
|
+
},
|
|
374
|
+
required: ["target_kind", "target_id", "recipient_opted_in"],
|
|
375
|
+
},
|
|
376
|
+
requiredScopeSets: [["dm.write", "dm.read", "tweet.read", "users.read"]],
|
|
377
|
+
dispatch: "single",
|
|
378
|
+
coverage: ["write-result"],
|
|
379
|
+
implementation: "optional official media upload then POST legacy DM message; explicit opt-in; not encrypted Chat",
|
|
380
|
+
},
|
|
381
|
+
"posts.publish": {
|
|
382
|
+
provider: "x",
|
|
383
|
+
operation: "posts.publish",
|
|
384
|
+
contractVersion: 1,
|
|
385
|
+
risk: "R3",
|
|
386
|
+
input: {
|
|
387
|
+
properties: {
|
|
388
|
+
body: string("Optional exact post text; text, media, or a poll is required", { minLength: 1, maxLength: 280 }),
|
|
389
|
+
media: array("Up to four images, or one GIF/video", file("One reviewed X post attachment", 512 * 1024 * 1024, ["image/jpeg", "image/png", "image/gif", "video/mp4"]), 1, 4),
|
|
390
|
+
media_alt_texts: array("Accessibility text aligned one-to-one with JPEG/PNG media", string("One exact image description", { minLength: 1, maxLength: 1_000 }), 1, 4),
|
|
391
|
+
poll_options: array("Two to four poll choices", string("One poll choice", { minLength: 1, maxLength: 25 }), 2, 4),
|
|
392
|
+
poll_duration_minutes: number("Poll duration in minutes", 5, 10_080),
|
|
393
|
+
reply_settings: string("Who may reply; everyone means omit the provider field", { enum: ["everyone", "mentionedUsers", "following", "subscribers", "verified"] }),
|
|
394
|
+
community_id: string("Optional exact 1-19 digit Community ID", { minLength: 1, maxLength: 19 }),
|
|
395
|
+
made_with_ai: boolean("When true, label the attached media as AI-generated; true requires media"),
|
|
396
|
+
},
|
|
397
|
+
required: [],
|
|
398
|
+
},
|
|
399
|
+
requiredScopeSets: [["tweet.read", "tweet.write", "users.read"]],
|
|
400
|
+
dispatch: "single",
|
|
401
|
+
coverage: ["write-result"],
|
|
402
|
+
implementation: "optional official X v2 media upload then POST /2/tweets; media/poll union enforced",
|
|
403
|
+
},
|
|
404
|
+
"replies.create": {
|
|
405
|
+
provider: "x",
|
|
406
|
+
operation: "replies.create",
|
|
407
|
+
contractVersion: 1,
|
|
408
|
+
risk: "R3",
|
|
409
|
+
input: {
|
|
410
|
+
properties: {
|
|
411
|
+
target_post_id: string("Exact 1-19 digit X post ID being replied to", { minLength: 1, maxLength: 19 }),
|
|
412
|
+
body: string("Optional exact reply text; text or media is required", { minLength: 1, maxLength: 280 }),
|
|
413
|
+
media: array("Up to four images, or one GIF/video", file("One reviewed reply attachment", 512 * 1024 * 1024, ["image/jpeg", "image/png", "image/gif", "video/mp4"]), 1, 4),
|
|
414
|
+
media_alt_texts: array("Accessibility text aligned one-to-one with JPEG/PNG media", string("One exact image description", { minLength: 1, maxLength: 1_000 }), 1, 4),
|
|
415
|
+
recipient_opted_in: boolean("Required acknowledgement of recipient opt-in for an automated reply", [true]),
|
|
416
|
+
author_invited_reply: boolean("Required acknowledgement that the author mentioned or quoted the replying account", [true]),
|
|
417
|
+
},
|
|
418
|
+
required: ["target_post_id", "recipient_opted_in", "author_invited_reply"],
|
|
419
|
+
},
|
|
420
|
+
requiredScopeSets: [["tweet.read", "tweet.write", "users.read"]],
|
|
421
|
+
dispatch: "single",
|
|
422
|
+
coverage: ["write-result"],
|
|
423
|
+
implementation: "POST /2/tweets reply; self-serve author-invitation and automated-recipient-opt-in acknowledgements required",
|
|
424
|
+
},
|
|
425
|
+
"threads.publish": {
|
|
426
|
+
provider: "x",
|
|
427
|
+
operation: "threads.publish",
|
|
428
|
+
contractVersion: 1,
|
|
429
|
+
risk: "R3",
|
|
430
|
+
input: {
|
|
431
|
+
properties: {
|
|
432
|
+
items: array("Exact ordered thread segments", string("One exact thread segment", { minLength: 1, maxLength: 280 }), 1, 25),
|
|
433
|
+
media: array("Up to 25 reviewed thread attachments in dispatch order", file("One reviewed thread attachment", 512 * 1024 * 1024, ["image/jpeg", "image/png", "image/gif", "video/mp4"]), 1, 25),
|
|
434
|
+
media_item_indices: array("One-based thread item index aligned one-to-one with media", number("One-based thread item index", 1, 25), 1, 25),
|
|
435
|
+
media_alt_texts: array("Accessibility text aligned one-to-one with JPEG/PNG thread media", string("One exact image description", { minLength: 1, maxLength: 1_000 }), 1, 25),
|
|
436
|
+
},
|
|
437
|
+
required: ["items"],
|
|
438
|
+
},
|
|
439
|
+
requiredScopeSets: [["tweet.read", "tweet.write", "users.read"]],
|
|
440
|
+
dispatch: "thread-items",
|
|
441
|
+
coverage: ["write-result"],
|
|
442
|
+
implementation: "sequential POST /2/tweets; each continuation replies to immediately preceding returned ID; a normal partial return includes committed IDs for manual reconciliation and is never replayed automatically",
|
|
443
|
+
},
|
|
444
|
+
"posts.repost": {
|
|
445
|
+
provider: "x",
|
|
446
|
+
operation: "posts.repost",
|
|
447
|
+
contractVersion: 1,
|
|
448
|
+
risk: "R3",
|
|
449
|
+
input: {
|
|
450
|
+
properties: {
|
|
451
|
+
post_id: string("Exact 1-19 digit X post ID", { minLength: 1, maxLength: 19 }),
|
|
452
|
+
enabled: boolean("Repost when true; undo the authenticated account's repost when false"),
|
|
453
|
+
},
|
|
454
|
+
required: ["post_id", "enabled"],
|
|
455
|
+
},
|
|
456
|
+
requiredScopeSets: [["tweet.read", "tweet.write", "users.read"]],
|
|
457
|
+
dispatch: "single",
|
|
458
|
+
coverage: ["write-result"],
|
|
459
|
+
implementation: "POST or DELETE /2/users/{me}/retweets desired-state endpoint",
|
|
460
|
+
},
|
|
461
|
+
"content.save": {
|
|
462
|
+
provider: "x",
|
|
463
|
+
operation: "content.save",
|
|
464
|
+
contractVersion: 1,
|
|
465
|
+
risk: "R2",
|
|
466
|
+
input: {
|
|
467
|
+
properties: {
|
|
468
|
+
post_id: string("Exact 1-19 digit X post ID", { minLength: 1, maxLength: 19 }),
|
|
469
|
+
enabled: boolean("Bookmark when true; remove the bookmark when false"),
|
|
470
|
+
},
|
|
471
|
+
required: ["post_id", "enabled"],
|
|
472
|
+
},
|
|
473
|
+
requiredScopeSets: [["bookmark.write", "tweet.read", "users.read"]],
|
|
474
|
+
dispatch: "single",
|
|
475
|
+
coverage: ["write-result"],
|
|
476
|
+
implementation: "POST or DELETE /2/users/{me}/bookmarks desired-state endpoint",
|
|
477
|
+
},
|
|
478
|
+
"articles.publish": {
|
|
479
|
+
provider: "x",
|
|
480
|
+
operation: "articles.publish",
|
|
481
|
+
contractVersion: 1,
|
|
482
|
+
risk: "R3",
|
|
483
|
+
input: {
|
|
484
|
+
properties: {
|
|
485
|
+
title: string("Exact X Article title", { minLength: 1, maxLength: 100 }),
|
|
486
|
+
body: string("Plain-text Article body converted to deterministic DraftJS blocks", { minLength: 1, maxLength: 20_000 }),
|
|
487
|
+
cover: file("Optional Article cover image", 5 * 1024 * 1024, ["image/jpeg", "image/png"]),
|
|
488
|
+
cover_alt_text: string("Optional accessibility text applied to the reviewed Article cover", { minLength: 1, maxLength: 1_000 }),
|
|
489
|
+
},
|
|
490
|
+
required: ["title", "body"],
|
|
491
|
+
},
|
|
492
|
+
requiredScopeSets: [["tweet.read", "tweet.write", "users.read"]],
|
|
493
|
+
dispatch: "single",
|
|
494
|
+
coverage: ["write-result"],
|
|
495
|
+
implementation: "optional official image upload; POST /2/articles/draft; POST /2/articles/{id}/publish; account eligibility required",
|
|
496
|
+
},
|
|
497
|
+
} as const satisfies Readonly<Record<string, ProviderContractDefinition>>;
|
|
498
|
+
|
|
499
|
+
const gmailDefinitions = {
|
|
500
|
+
"contacts.list": {
|
|
501
|
+
provider: "gmail",
|
|
502
|
+
operation: "contacts.list",
|
|
503
|
+
contractVersion: 5,
|
|
504
|
+
risk: "R1",
|
|
505
|
+
input: {
|
|
506
|
+
properties: {
|
|
507
|
+
collection: string("Google contact projection; defaults to saved contacts", { enum: ["contacts", "other-contacts", "interactions"] }),
|
|
508
|
+
after: string("Optional inclusive whole-second UTC lower bound for incremental interactions", { minLength: 24, maxLength: 24 }),
|
|
509
|
+
before: string("Fixed whole-second UTC cutoff required only for the interactions projection", { minLength: 24, maxLength: 24 }),
|
|
510
|
+
cursor: string("Opaque Google People or Gmail API next-page token", { minLength: 1, maxLength: 4_096 }),
|
|
511
|
+
include_dates: boolean("Include selected saved-contact display, given, middle, family, prefix, and suffix name fields, birthdays, and contact events; defaults to false and is unavailable for Other contacts"),
|
|
512
|
+
include_stats: boolean("Compute bounded sent and received Gmail statistics; defaults to true"),
|
|
513
|
+
limit: number("Bounded page count; defaults to 20 for contacts and 100 for interactions", 1, 100),
|
|
514
|
+
stats_scan_limit: number("Maximum messages scanned per contact and direction when include_stats is true; defaults to 100; limit multiplied by this value cannot exceed 2000", 1, 2_000),
|
|
515
|
+
},
|
|
516
|
+
required: [],
|
|
517
|
+
},
|
|
518
|
+
requiredScopeSets: [
|
|
519
|
+
[
|
|
520
|
+
"https://www.googleapis.com/auth/contacts.readonly",
|
|
521
|
+
"https://www.googleapis.com/auth/contacts.other.readonly",
|
|
522
|
+
"https://www.googleapis.com/auth/gmail.readonly",
|
|
523
|
+
],
|
|
524
|
+
[
|
|
525
|
+
"https://www.googleapis.com/auth/contacts.readonly",
|
|
526
|
+
"https://www.googleapis.com/auth/contacts.other.readonly",
|
|
527
|
+
"https://www.googleapis.com/auth/gmail.modify",
|
|
528
|
+
],
|
|
529
|
+
[
|
|
530
|
+
"https://www.googleapis.com/auth/contacts.readonly",
|
|
531
|
+
"https://www.googleapis.com/auth/contacts.other.readonly",
|
|
532
|
+
"https://mail.google.com/",
|
|
533
|
+
],
|
|
534
|
+
[
|
|
535
|
+
"https://www.googleapis.com/auth/contacts",
|
|
536
|
+
"https://www.googleapis.com/auth/contacts.other.readonly",
|
|
537
|
+
"https://www.googleapis.com/auth/gmail.readonly",
|
|
538
|
+
],
|
|
539
|
+
[
|
|
540
|
+
"https://www.googleapis.com/auth/contacts",
|
|
541
|
+
"https://www.googleapis.com/auth/contacts.other.readonly",
|
|
542
|
+
"https://www.googleapis.com/auth/gmail.modify",
|
|
543
|
+
],
|
|
544
|
+
[
|
|
545
|
+
"https://www.googleapis.com/auth/contacts",
|
|
546
|
+
"https://www.googleapis.com/auth/contacts.other.readonly",
|
|
547
|
+
"https://mail.google.com/",
|
|
548
|
+
],
|
|
549
|
+
],
|
|
550
|
+
dispatch: "none",
|
|
551
|
+
coverage: [
|
|
552
|
+
"contacts",
|
|
553
|
+
"other-contacts",
|
|
554
|
+
"contact-metadata",
|
|
555
|
+
"contact-email-addresses",
|
|
556
|
+
"optional-saved-contact-name-components",
|
|
557
|
+
"optional-saved-contact-birthdays",
|
|
558
|
+
"optional-saved-contact-events",
|
|
559
|
+
"optional-sent-counts",
|
|
560
|
+
"optional-received-counts",
|
|
561
|
+
"optional-last-sent-at",
|
|
562
|
+
"optional-last-received-at",
|
|
563
|
+
"bounded-stat-completeness",
|
|
564
|
+
"messages-in-fixed-half-open-window",
|
|
565
|
+
"spam-and-trash-excluded",
|
|
566
|
+
"draft-and-chat-excluded",
|
|
567
|
+
"sent-and-received-message-counts",
|
|
568
|
+
"first-and-last-interaction-times",
|
|
569
|
+
"30-90-365-day-counts",
|
|
570
|
+
"per-direction-completeness",
|
|
571
|
+
"opaque-pagination-evidence",
|
|
572
|
+
"send-as-alias-exclusion",
|
|
573
|
+
],
|
|
574
|
+
implementation: "subject-bound People contacts and Gmail interactions; saved contacts optionally batchGet selected display, given, middle, family, prefix, and suffix fields, birthdays, and events; Other contacts stay limited; interactions use a fixed half-open epoch window, list send-as aliases on page one, exclude spam/trash/drafts/chats, read metadata only, derive direction from SENT, dedupe addresses per message, expose rolling counts and explicit lower bounds, emit opaque ID evidence, and never read bodies",
|
|
575
|
+
},
|
|
576
|
+
"messaging.list": {
|
|
577
|
+
provider: "gmail",
|
|
578
|
+
operation: "messaging.list",
|
|
579
|
+
contractVersion: 1,
|
|
580
|
+
risk: "R1",
|
|
581
|
+
input: {
|
|
582
|
+
properties: {
|
|
583
|
+
view: string("List the inbox or execute one explicit Gmail search", { enum: ["inbox", "search"] }),
|
|
584
|
+
query: string("Exact Gmail search expression; required only for search", { minLength: 1, maxLength: 512 }),
|
|
585
|
+
cursor: string("Opaque Gmail next-page token", { minLength: 1, maxLength: 4_096 }),
|
|
586
|
+
limit: number("Bounded thread count", 1, 100),
|
|
587
|
+
include_spam_trash: boolean("Include matching spam and trash threads for search"),
|
|
588
|
+
},
|
|
589
|
+
required: ["view"],
|
|
590
|
+
},
|
|
591
|
+
requiredScopeSets: [
|
|
592
|
+
["https://www.googleapis.com/auth/gmail.readonly"],
|
|
593
|
+
["https://www.googleapis.com/auth/gmail.modify"],
|
|
594
|
+
["https://mail.google.com/"],
|
|
595
|
+
],
|
|
596
|
+
dispatch: "none",
|
|
597
|
+
coverage: ["inbox-threads", "search-threads", "thread-metadata", "thread-urls", "replayable-read-input"],
|
|
598
|
+
implementation: "subject-bound GET Gmail /gmail/v1/users/me/threads with fixed INBOX selection or one bounded query, followed by bounded metadata reads that emit exact thread URLs and messaging.read input; no modify, acknowledgement, or read-receipt request",
|
|
599
|
+
},
|
|
600
|
+
"messaging.read": {
|
|
601
|
+
provider: "gmail",
|
|
602
|
+
operation: "messaging.read",
|
|
603
|
+
contractVersion: 1,
|
|
604
|
+
risk: "R1",
|
|
605
|
+
input: {
|
|
606
|
+
properties: {
|
|
607
|
+
thread_id: string("Exact Gmail thread ID", { minLength: 1, maxLength: 256 }),
|
|
608
|
+
},
|
|
609
|
+
required: ["thread_id"],
|
|
610
|
+
},
|
|
611
|
+
requiredScopeSets: [
|
|
612
|
+
["https://www.googleapis.com/auth/gmail.readonly"],
|
|
613
|
+
["https://www.googleapis.com/auth/gmail.modify"],
|
|
614
|
+
["https://mail.google.com/"],
|
|
615
|
+
],
|
|
616
|
+
dispatch: "none",
|
|
617
|
+
coverage: ["thread", "message-metadata", "render-safe-bodies", "attachment-metadata", "thread-url"],
|
|
618
|
+
implementation: "subject-bound GET Gmail /gmail/v1/users/me/threads/{thread} with exact response identity, bounded MIME projection, stable part identities, attachment metadata, and an exact Gmail thread URL; no modify, acknowledgement, or read-receipt request",
|
|
619
|
+
},
|
|
620
|
+
} as const satisfies Readonly<Record<string, ProviderContractDefinition>>;
|
|
621
|
+
|
|
622
|
+
const gmailContactsListV4Definition = {
|
|
623
|
+
provider: "gmail",
|
|
624
|
+
operation: "contacts.list",
|
|
625
|
+
contractVersion: 4,
|
|
626
|
+
risk: "R1",
|
|
627
|
+
input: {
|
|
628
|
+
properties: {
|
|
629
|
+
collection: string("Google contact projection; defaults to saved contacts", { enum: ["contacts", "other-contacts", "interactions"] }),
|
|
630
|
+
after: string("Optional inclusive whole-second UTC lower bound for incremental interactions", { minLength: 24, maxLength: 24 }),
|
|
631
|
+
before: string("Fixed whole-second UTC cutoff required only for the interactions projection", { minLength: 24, maxLength: 24 }),
|
|
632
|
+
cursor: string("Opaque Google People or Gmail API next-page token", { minLength: 1, maxLength: 4_096 }),
|
|
633
|
+
include_stats: boolean("Compute bounded sent and received Gmail statistics; defaults to true"),
|
|
634
|
+
limit: number("Bounded page count; defaults to 20 for contacts and 100 for interactions", 1, 100),
|
|
635
|
+
stats_scan_limit: number("Maximum messages scanned per contact and direction when include_stats is true; defaults to 100; limit multiplied by this value cannot exceed 2000", 1, 2_000),
|
|
636
|
+
},
|
|
637
|
+
required: [],
|
|
638
|
+
},
|
|
639
|
+
requiredScopeSets: [
|
|
640
|
+
[
|
|
641
|
+
"https://www.googleapis.com/auth/contacts.readonly",
|
|
642
|
+
"https://www.googleapis.com/auth/contacts.other.readonly",
|
|
643
|
+
"https://www.googleapis.com/auth/gmail.readonly",
|
|
644
|
+
],
|
|
645
|
+
[
|
|
646
|
+
"https://www.googleapis.com/auth/contacts.readonly",
|
|
647
|
+
"https://www.googleapis.com/auth/contacts.other.readonly",
|
|
648
|
+
"https://www.googleapis.com/auth/gmail.modify",
|
|
649
|
+
],
|
|
650
|
+
[
|
|
651
|
+
"https://www.googleapis.com/auth/contacts.readonly",
|
|
652
|
+
"https://www.googleapis.com/auth/contacts.other.readonly",
|
|
653
|
+
"https://mail.google.com/",
|
|
654
|
+
],
|
|
655
|
+
[
|
|
656
|
+
"https://www.googleapis.com/auth/contacts",
|
|
657
|
+
"https://www.googleapis.com/auth/contacts.other.readonly",
|
|
658
|
+
"https://www.googleapis.com/auth/gmail.readonly",
|
|
659
|
+
],
|
|
660
|
+
[
|
|
661
|
+
"https://www.googleapis.com/auth/contacts",
|
|
662
|
+
"https://www.googleapis.com/auth/contacts.other.readonly",
|
|
663
|
+
"https://www.googleapis.com/auth/gmail.modify",
|
|
664
|
+
],
|
|
665
|
+
[
|
|
666
|
+
"https://www.googleapis.com/auth/contacts",
|
|
667
|
+
"https://www.googleapis.com/auth/contacts.other.readonly",
|
|
668
|
+
"https://mail.google.com/",
|
|
669
|
+
],
|
|
670
|
+
],
|
|
671
|
+
dispatch: "none",
|
|
672
|
+
coverage: [
|
|
673
|
+
"contacts",
|
|
674
|
+
"other-contacts",
|
|
675
|
+
"contact-metadata",
|
|
676
|
+
"contact-email-addresses",
|
|
677
|
+
"optional-sent-counts",
|
|
678
|
+
"optional-received-counts",
|
|
679
|
+
"optional-last-sent-at",
|
|
680
|
+
"optional-last-received-at",
|
|
681
|
+
"bounded-stat-completeness",
|
|
682
|
+
"messages-in-fixed-half-open-window",
|
|
683
|
+
"spam-and-trash-excluded",
|
|
684
|
+
"draft-and-chat-excluded",
|
|
685
|
+
"sent-and-received-message-counts",
|
|
686
|
+
"first-and-last-interaction-times",
|
|
687
|
+
"30-90-365-day-counts",
|
|
688
|
+
"per-direction-completeness",
|
|
689
|
+
"opaque-pagination-evidence",
|
|
690
|
+
"send-as-alias-exclusion",
|
|
691
|
+
],
|
|
692
|
+
implementation: "subject-bound People contacts plus Gmail interactions; saved contacts expand by batchGet; Other contacts retain their limited projection; interactions use one fixed half-open epoch window, list send-as aliases on page one, disable spam/trash, read metadata only, skip drafts/chats and lower-bound overlap, derive direction from SENT, dedupe addresses per message, expose rolling counts and undated lower bounds, emit opaque ID evidence, and never read bodies",
|
|
693
|
+
} as const satisfies ProviderContractDefinition;
|
|
694
|
+
|
|
695
|
+
type BoundProviderContracts<
|
|
696
|
+
Definitions extends Readonly<Record<string, ProviderContractDefinition>>,
|
|
697
|
+
> = {
|
|
698
|
+
readonly [Operation in keyof Definitions]: Definitions[Operation] & Pick<
|
|
699
|
+
ProviderContract,
|
|
700
|
+
"sideEffect" | "idempotency" | "dedupeWindowMs" | "state"
|
|
701
|
+
>;
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
function bindProviderSemantics<
|
|
705
|
+
Definitions extends Readonly<Record<string, ProviderContractDefinition>>,
|
|
706
|
+
>(definitions: Definitions): BoundProviderContracts<Definitions> {
|
|
707
|
+
return Object.fromEntries(Object.entries(definitions).map(([operation, definition]) => {
|
|
708
|
+
const mutating = definition.risk === "R2" || definition.risk === "R3";
|
|
709
|
+
return [operation, Object.freeze({
|
|
710
|
+
...definition,
|
|
711
|
+
sideEffect: mutating
|
|
712
|
+
? `Mutates ${definition.provider} through the exact confirmed ${definition.operation} official API contract.`
|
|
713
|
+
: "none",
|
|
714
|
+
idempotency: mutating ? "local-at-most-once" : "none",
|
|
715
|
+
dedupeWindowMs: mutating ? 86_400_000 : 0,
|
|
716
|
+
state: "observed",
|
|
717
|
+
})];
|
|
718
|
+
})) as BoundProviderContracts<Definitions>;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const linkedin = bindProviderSemantics(linkedinDefinitions);
|
|
722
|
+
const x = bindProviderSemantics(xDefinitions);
|
|
723
|
+
const gmail = bindProviderSemantics(gmailDefinitions);
|
|
724
|
+
const gmailContactsListV4 = bindProviderSemantics({
|
|
725
|
+
"contacts.list": gmailContactsListV4Definition,
|
|
726
|
+
})["contacts.list"];
|
|
727
|
+
|
|
728
|
+
export const providerContractDefinitions = { gmail, linkedin, x } as const;
|
|
729
|
+
export const gmailProviderContractDefinitionsV4 = Object.freeze({
|
|
730
|
+
...gmail,
|
|
731
|
+
"contacts.list": gmailContactsListV4,
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
export { planProviderContractDispatches } from "./provider-contract-planning";
|
|
735
|
+
|
|
736
|
+
export function providerContractConditionalInputIssues(
|
|
737
|
+
recipe: Pick<ProviderRecipe, "provider" | "action">,
|
|
738
|
+
input: OperationInput,
|
|
739
|
+
): readonly string[] {
|
|
740
|
+
const issues: string[] = [];
|
|
741
|
+
const xId = (name: string, value: unknown): void => {
|
|
742
|
+
if (typeof value === "string" && !/^[0-9]{1,19}$/u.test(value)) {
|
|
743
|
+
issues.push(`input.${name} must be a 1-19 digit X object ID`);
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
const xConversationId = (name: string, value: unknown): void => {
|
|
747
|
+
if (typeof value === "string" && !/^(?:[0-9]{15,19}|[0-9]{1,19}-[0-9]{1,19})$/u.test(value)) {
|
|
748
|
+
issues.push(`input.${name} must be an exact legacy X DM conversation ID`);
|
|
749
|
+
}
|
|
750
|
+
};
|
|
751
|
+
if (recipe.provider === "linkedin" && recipe.action === "posts.read") {
|
|
752
|
+
if (input.mode === "one") {
|
|
753
|
+
if (typeof input.post_urn !== "string") issues.push("input.post_urn is required when mode is one");
|
|
754
|
+
if (input.author !== undefined) issues.push("input.author is not accepted when mode is one");
|
|
755
|
+
}
|
|
756
|
+
if (input.mode === "author") {
|
|
757
|
+
if (typeof input.author !== "string") issues.push("input.author is required when mode is author");
|
|
758
|
+
if (input.post_urn !== undefined) issues.push("input.post_urn is not accepted when mode is author");
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
if (recipe.provider === "linkedin" && recipe.action === "posts.publish") {
|
|
762
|
+
const media = input.media;
|
|
763
|
+
if (Array.isArray(media) && typeof input.article_url === "string") issues.push("input.media and input.article_url are mutually exclusive");
|
|
764
|
+
if (typeof input.article_url === "string") {
|
|
765
|
+
try {
|
|
766
|
+
const url = new URL(input.article_url);
|
|
767
|
+
if (url.protocol !== "https:" || url.username !== "" || url.password !== "") {
|
|
768
|
+
issues.push("input.article_url must be a credential-free HTTPS URL");
|
|
769
|
+
}
|
|
770
|
+
} catch {
|
|
771
|
+
issues.push("input.article_url must be a credential-free HTTPS URL");
|
|
772
|
+
}
|
|
773
|
+
if (typeof input.article_title !== "string") issues.push("input.article_title is required with input.article_url");
|
|
774
|
+
} else if (input.article_title !== undefined || input.article_description !== undefined) {
|
|
775
|
+
issues.push("input.article_title and input.article_description require input.article_url");
|
|
776
|
+
}
|
|
777
|
+
if (input.alt_text !== undefined && !Array.isArray(media)) issues.push("input.alt_text requires input.media");
|
|
778
|
+
if (input.media_title !== undefined && !Array.isArray(media)) issues.push("input.media_title requires input.media");
|
|
779
|
+
}
|
|
780
|
+
if (recipe.provider === "linkedin" && recipe.action === "reactions.set") {
|
|
781
|
+
if (input.enabled === true && typeof input.reaction !== "string") {
|
|
782
|
+
issues.push("input.reaction is required when input.enabled is true");
|
|
783
|
+
}
|
|
784
|
+
if (input.enabled === false && input.reaction !== undefined) {
|
|
785
|
+
issues.push("input.reaction is not accepted when input.enabled is false because the operation clears any current reaction");
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
if (recipe.provider === "x" && recipe.action === "feeds.read") {
|
|
789
|
+
const feedLabel = typeof input.feed === "string" ? input.feed : "unknown feed";
|
|
790
|
+
if ((input.feed === "user" || input.feed === "mentions") && typeof input.user_id !== "string") {
|
|
791
|
+
issues.push("input.user_id is required for user and mentions feeds");
|
|
792
|
+
}
|
|
793
|
+
if (input.feed === "list" && typeof input.list_id !== "string") issues.push("input.list_id is required for the list feed");
|
|
794
|
+
if (input.feed === "recent-search" && typeof input.query !== "string") issues.push("input.query is required for recent-search");
|
|
795
|
+
if (input.feed !== "user" && input.feed !== "mentions" && input.user_id !== undefined) {
|
|
796
|
+
issues.push("input.user_id is accepted only for user and mentions feeds");
|
|
797
|
+
}
|
|
798
|
+
if (input.feed !== "list" && input.list_id !== undefined) issues.push("input.list_id is accepted only for the list feed");
|
|
799
|
+
if (input.feed !== "recent-search" && input.query !== undefined) {
|
|
800
|
+
issues.push("input.query is accepted only for recent-search");
|
|
801
|
+
}
|
|
802
|
+
const supportsTimeAndIdBounds = input.feed === "home-reverse-chronological"
|
|
803
|
+
|| input.feed === "user"
|
|
804
|
+
|| input.feed === "mentions"
|
|
805
|
+
|| input.feed === "recent-search";
|
|
806
|
+
for (const name of ["since_id", "until_id", "start_time", "end_time"] as const) {
|
|
807
|
+
if (!supportsTimeAndIdBounds && input[name] !== undefined) {
|
|
808
|
+
issues.push(`input.${name} is not accepted for ${feedLabel}`);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
if (input.feed !== "home-reverse-chronological" && input.feed !== "user") {
|
|
812
|
+
if (input.exclude_replies !== undefined) issues.push(`input.exclude_replies is not accepted for ${feedLabel}`);
|
|
813
|
+
if (input.exclude_reposts !== undefined) issues.push(`input.exclude_reposts is not accepted for ${feedLabel}`);
|
|
814
|
+
}
|
|
815
|
+
if (input.feed !== "recent-search" && input.sort !== undefined) {
|
|
816
|
+
issues.push(`input.sort is accepted only for recent-search`);
|
|
817
|
+
}
|
|
818
|
+
xId("user_id", input.user_id);
|
|
819
|
+
xId("list_id", input.list_id);
|
|
820
|
+
xId("since_id", input.since_id);
|
|
821
|
+
xId("until_id", input.until_id);
|
|
822
|
+
if ((input.feed === "user" || input.feed === "mentions") && typeof input.limit === "number" && input.limit < 5) {
|
|
823
|
+
issues.push("input.limit must be at least 5 for user and mentions feeds");
|
|
824
|
+
}
|
|
825
|
+
if (input.feed === "recent-search" && typeof input.limit === "number" && input.limit < 10) {
|
|
826
|
+
issues.push("input.limit must be at least 10 for recent-search");
|
|
827
|
+
}
|
|
828
|
+
for (const name of ["start_time", "end_time"] as const) {
|
|
829
|
+
const value = input[name];
|
|
830
|
+
if (typeof value === "string" && (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?Z$/u.test(value) || !Number.isFinite(Date.parse(value)))) {
|
|
831
|
+
issues.push(`input.${name} must be a valid UTC RFC 3339 timestamp`);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (recipe.provider === "x" && recipe.action === "posts.read") {
|
|
836
|
+
if (Array.isArray(input.post_ids)) {
|
|
837
|
+
for (const [index, value] of input.post_ids.entries()) xId(`post_ids[${index}]`, value);
|
|
838
|
+
if (input.post_ids.every((value) => typeof value === "string") && new Set(input.post_ids).size !== input.post_ids.length) {
|
|
839
|
+
issues.push("input.post_ids must contain unique X post IDs");
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
if (recipe.provider === "x" && recipe.action === "comments.read") xId("post_id", input.post_id);
|
|
844
|
+
if (recipe.provider === "x" && recipe.action === "messaging.list") {
|
|
845
|
+
const needsTarget = input.view === "participant" || input.view === "conversation" || input.view === "chat-events";
|
|
846
|
+
if (needsTarget && typeof input.target_id !== "string") {
|
|
847
|
+
issues.push("input.target_id is required for participant, conversation, and chat-events views");
|
|
848
|
+
}
|
|
849
|
+
if ((input.view === "all" || input.view === "chat-conversations") && input.target_id !== undefined) {
|
|
850
|
+
issues.push(`input.target_id is not accepted when view is ${input.view}`);
|
|
851
|
+
}
|
|
852
|
+
if (input.view === "participant") xId("target_id", input.target_id);
|
|
853
|
+
if (input.view === "conversation") xConversationId("target_id", input.target_id);
|
|
854
|
+
if (input.view === "chat-events" && typeof input.target_id === "string"
|
|
855
|
+
&& !/^(?:[0-9]{1,19}|[0-9]{1,19}-[0-9]{1,19}|g[0-9]{1,19})$/u.test(input.target_id)) {
|
|
856
|
+
issues.push("input.target_id must be an exact X Chat recipient or conversation ID");
|
|
857
|
+
}
|
|
858
|
+
if (input.view === "chat-events" && typeof input.target_id === "string" && input.target_id.includes("-")) {
|
|
859
|
+
const [left, right] = input.target_id.split("-");
|
|
860
|
+
if (left === right) issues.push("input.target_id must identify two different X Chat participants");
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
if (recipe.provider === "x" && recipe.action === "messaging.read") xId("event_id", input.event_id);
|
|
864
|
+
if (recipe.provider === "x" && recipe.action === "messaging.send") {
|
|
865
|
+
if (input.target_kind === "participant") xId("target_id", input.target_id);
|
|
866
|
+
if (input.target_kind === "conversation") xConversationId("target_id", input.target_id);
|
|
867
|
+
if (input.target_kind === "conversation" && typeof input.target_id === "string" && input.target_id.includes("-")) {
|
|
868
|
+
const [left, right] = input.target_id.split("-");
|
|
869
|
+
if (left === right) issues.push("input.target_id must identify two different legacy X DM participants");
|
|
870
|
+
}
|
|
871
|
+
if (typeof input.body !== "string" && !isFileInputValue(input.media)) {
|
|
872
|
+
issues.push("input.body or input.media is required for an X Direct Message");
|
|
873
|
+
}
|
|
874
|
+
if (input.media_alt_text !== undefined && !isFileInputValue(input.media)) {
|
|
875
|
+
issues.push("input.media_alt_text requires input.media");
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
if (recipe.provider === "x" && recipe.action === "posts.publish") {
|
|
879
|
+
const hasPoll = Array.isArray(input.poll_options) || input.poll_duration_minutes !== undefined;
|
|
880
|
+
if (hasPoll && (!Array.isArray(input.poll_options) || typeof input.poll_duration_minutes !== "number")) {
|
|
881
|
+
issues.push("input.poll_options and input.poll_duration_minutes must be supplied together");
|
|
882
|
+
}
|
|
883
|
+
if (hasPoll && Array.isArray(input.media)) issues.push("input.media and poll inputs are mutually exclusive");
|
|
884
|
+
if (typeof input.body !== "string" && !Array.isArray(input.media) && !hasPoll) {
|
|
885
|
+
issues.push("input.body, input.media, or complete poll inputs are required for an X post");
|
|
886
|
+
}
|
|
887
|
+
if (input.made_with_ai === true && !Array.isArray(input.media)) {
|
|
888
|
+
issues.push("input.made_with_ai can be true only when reviewed media is attached");
|
|
889
|
+
}
|
|
890
|
+
if (input.media_alt_texts !== undefined) {
|
|
891
|
+
if (!Array.isArray(input.media)) issues.push("input.media_alt_texts requires input.media");
|
|
892
|
+
else if (!Array.isArray(input.media_alt_texts) || input.media_alt_texts.length !== input.media.length) {
|
|
893
|
+
issues.push("input.media_alt_texts must align one-to-one with input.media");
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
xId("community_id", input.community_id);
|
|
897
|
+
}
|
|
898
|
+
if (recipe.provider === "x" && recipe.action === "replies.create") {
|
|
899
|
+
xId("target_post_id", input.target_post_id);
|
|
900
|
+
if (typeof input.body !== "string" && !Array.isArray(input.media)) {
|
|
901
|
+
issues.push("input.body or input.media is required for an X reply");
|
|
902
|
+
}
|
|
903
|
+
if (input.media_alt_texts !== undefined) {
|
|
904
|
+
if (!Array.isArray(input.media)) issues.push("input.media_alt_texts requires input.media");
|
|
905
|
+
else if (!Array.isArray(input.media_alt_texts) || input.media_alt_texts.length !== input.media.length) {
|
|
906
|
+
issues.push("input.media_alt_texts must align one-to-one with input.media");
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
if (recipe.provider === "x" && recipe.action === "threads.publish") {
|
|
911
|
+
const media = input.media;
|
|
912
|
+
const indices = input.media_item_indices;
|
|
913
|
+
if (Array.isArray(media) !== Array.isArray(indices)) {
|
|
914
|
+
issues.push("input.media and input.media_item_indices must be supplied together");
|
|
915
|
+
}
|
|
916
|
+
if (Array.isArray(media) && Array.isArray(indices) && media.length !== indices.length) {
|
|
917
|
+
issues.push("input.media_item_indices must align one-to-one with input.media");
|
|
918
|
+
}
|
|
919
|
+
if (Array.isArray(indices) && Array.isArray(input.items)) {
|
|
920
|
+
for (const [index, value] of indices.entries()) {
|
|
921
|
+
if (typeof value === "number" && (!Number.isSafeInteger(value) || value < 1 || value > input.items.length)) {
|
|
922
|
+
issues.push(`input.media_item_indices[${index}] must name an existing one-based thread item`);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
if (input.media_alt_texts !== undefined) {
|
|
927
|
+
if (!Array.isArray(media)) issues.push("input.media_alt_texts requires input.media");
|
|
928
|
+
else if (!Array.isArray(input.media_alt_texts) || input.media_alt_texts.length !== media.length) {
|
|
929
|
+
issues.push("input.media_alt_texts must align one-to-one with input.media");
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
if (recipe.provider === "x" && (recipe.action === "posts.repost" || recipe.action === "content.save")) {
|
|
934
|
+
xId("post_id", input.post_id);
|
|
935
|
+
}
|
|
936
|
+
if (recipe.provider === "x") {
|
|
937
|
+
for (const name of ["limit", "poll_duration_minutes"] as const) {
|
|
938
|
+
const value = input[name];
|
|
939
|
+
if (typeof value === "number" && !Number.isSafeInteger(value)) issues.push(`input.${name} must be a safe integer`);
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
if (recipe.provider === "x"
|
|
943
|
+
&& (recipe.action === "articles.draft.save" || recipe.action === "articles.publish")
|
|
944
|
+
&& input.cover_alt_text !== undefined && !isFileInputValue(input.cover)) {
|
|
945
|
+
issues.push("input.cover_alt_text requires input.cover");
|
|
946
|
+
}
|
|
947
|
+
return issues;
|
|
948
|
+
}
|