@hraness/ghostget 0.17.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +458 -0
- package/LICENSE +21 -0
- package/README.md +1359 -0
- package/bunfig.toml +2 -0
- package/dist/apple-photos-client.js +704 -0
- package/dist/beeper-client.js +4602 -0
- package/dist/client.js +1605 -0
- package/dist/index-26yq8q16.js +31 -0
- package/dist/index-9t13b4k7.js +508 -0
- package/dist/index-axwm9b3v.js +5 -0
- package/dist/index-gwk7rbyj.js +30 -0
- package/dist/index.js +701 -0
- package/dist/messaging.js +1130 -0
- package/dist/omni-client.js +1406 -0
- package/dist/whatsapp-client.js +355 -0
- package/docs/imessage-direct-provider.md +106 -0
- package/docs/rental-listings.md +94 -0
- package/package.json +600 -0
- package/skills/ghostget/SKILL.md +251 -0
- package/skills/ghostget/agents/openai.yaml +4 -0
- package/skills/ghostget/references/adapter-contract.md +181 -0
- package/skills/ghostget/references/apple-photos.md +115 -0
- package/skills/ghostget/references/article-drafts.md +235 -0
- package/skills/ghostget/references/code-owned-provider-scaffold.md +134 -0
- package/skills/ghostget/references/cross-posting-video.md +139 -0
- package/skills/ghostget/references/cross-posting.md +82 -0
- package/skills/ghostget/references/derivation.md +390 -0
- package/skills/ghostget/references/hraness-social-profile-stats.json +269 -0
- package/skills/ghostget/references/install.md +66 -0
- package/skills/ghostget/references/linkedin-adapter.md +442 -0
- package/skills/ghostget/references/messaging.md +168 -0
- package/skills/ghostget/references/meta-comet-contract.md +153 -0
- package/skills/ghostget/references/platform-patterns.md +122 -0
- package/skills/ghostget/references/provider-plugins.md +496 -0
- package/skills/ghostget/references/reddit-flair.md +78 -0
- package/skills/ghostget/references/safety-and-state.md +214 -0
- package/skills/ghostget/references/settlement-and-duplicate-cleanup.md +79 -0
- package/skills/ghostget/references/social-platform-routing.md +66 -0
- package/skills/ghostget/references/social-profile-stats.md +195 -0
- package/skills/ghostget/references/social-video-platform-routing.md +89 -0
- package/skills/ghostget/references/whatsapp-adapter.md +101 -0
- package/skills/ghostget/references/x-adapter.md +332 -0
- package/skills/ghostget/references/x-ai-disclosure.md +71 -0
- package/src/apple-photos-cli.ts +639 -0
- package/src/apple-photos-client-types.ts +168 -0
- package/src/apple-photos-client.ts +201 -0
- package/src/apple-photos-contact-evidence.ts +838 -0
- package/src/apple-photos-local-source.ts +1131 -0
- package/src/args.ts +2053 -0
- package/src/article-draft-document.ts +417 -0
- package/src/article-draft-embeds.ts +123 -0
- package/src/article-draft-images.ts +214 -0
- package/src/assets/adapter-template/wrench-adapter.json +40 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.json +1500 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v1.0.0.json +129 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v1.1.0.json +207 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.0.0.json +1463 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.1.0.json +1463 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.2.0.json +1475 -0
- package/src/assets/adapters/beeper/wrench-web-adapter.v2.3.0.json +1488 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.json +684 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.0.0.json +581 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.1.0.json +581 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.2.0.json +581 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.3.0.json +615 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.4.0.json +684 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.5.0.json +684 -0
- package/src/assets/adapters/bluesky/wrench-web-adapter.v1.6.0.json +684 -0
- package/src/assets/adapters/clasificados/wrench-web-adapter.json +54 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.json +750 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.v1.0.0.json +706 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.v1.1.0.json +703 -0
- package/src/assets/adapters/facebook/wrench-web-adapter.v1.2.0.json +750 -0
- package/src/assets/adapters/facebook-group/wrench-web-adapter.json +682 -0
- package/src/assets/adapters/facebook-group/wrench-web-adapter.v1.0.0.json +583 -0
- package/src/assets/adapters/facebook-group/wrench-web-adapter.v1.1.0.json +682 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.json +437 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.0.0.json +383 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.1.0.json +437 -0
- package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.1.1.json +437 -0
- package/src/assets/adapters/facebook-page/wrench-web-adapter.json +880 -0
- package/src/assets/adapters/facebook-page/wrench-web-adapter.v1.0.0.json +740 -0
- package/src/assets/adapters/facebook-page/wrench-web-adapter.v1.1.0.json +880 -0
- package/src/assets/adapters/github/wrench-web-adapter.json +69 -0
- package/src/assets/adapters/github/wrench-web-adapter.v1.0.0.json +42 -0
- package/src/assets/adapters/github/wrench-web-adapter.v1.1.0.json +69 -0
- package/src/assets/adapters/gmail/wrench-adapter.json +161 -0
- package/src/assets/adapters/gmail/wrench-adapter.v1.2.0.json +157 -0
- package/src/assets/adapters/hacker-news/wrench-web-adapter.json +315 -0
- package/src/assets/adapters/hacker-news/wrench-web-adapter.v1.0.0.json +315 -0
- package/src/assets/adapters/imessage/wrench-web-adapter.json +236 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.json +719 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.0.0.json +627 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.1.0.json +609 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.2.0.json +640 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.3.0.json +671 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.4.0.json +710 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.5.0.json +710 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.6.0.json +719 -0
- package/src/assets/adapters/instagram/wrench-web-adapter.v1.7.0.json +719 -0
- package/src/assets/adapters/linkedin/wrench-adapter.json +442 -0
- package/src/assets/adapters/linkedin/wrench-adapter.v0.4.0.json +124 -0
- package/src/assets/adapters/linkedin/wrench-adapter.v1.0.0.json +411 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.json +914 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.0.0.json +604 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.1.0.json +604 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.10.0.json +809 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.11.0.json +809 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.12.0.json +820 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.13.0.json +819 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.14.0.json +819 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.15.0.json +819 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.16.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.17.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.18.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.19.0.json +874 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.2.0.json +731 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.3.0.json +768 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.4.0.json +808 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.5.0.json +808 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.6.0.json +808 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.7.0.json +825 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.8.0.json +792 -0
- package/src/assets/adapters/linkedin/wrench-web-adapter.v1.9.0.json +809 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.json +855 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.0.0.json +577 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.1.0.json +604 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.10.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.11.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.12.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.2.0.json +653 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.3.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.4.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.5.0.json +712 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.6.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.7.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.8.0.json +713 -0
- package/src/assets/adapters/reddit/wrench-web-adapter.v1.9.0.json +713 -0
- package/src/assets/adapters/substack/wrench-web-adapter.json +936 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.0.0.json +817 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.1.0.json +812 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.2.0.json +812 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.3.0.json +902 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.4.0.json +936 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.5.0.json +936 -0
- package/src/assets/adapters/substack/wrench-web-adapter.v1.6.0.json +936 -0
- package/src/assets/adapters/threads/wrench-web-adapter.json +692 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.0.0.json +629 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.1.0.json +623 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.2.0.json +622 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.3.0.json +622 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.4.0.json +622 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.5.0.json +699 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.6.0.json +693 -0
- package/src/assets/adapters/threads/wrench-web-adapter.v1.7.0.json +692 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.json +762 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.0.0.json +681 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.1.0.json +708 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.2.0.json +762 -0
- package/src/assets/adapters/tiktok/wrench-web-adapter.v1.3.0.json +762 -0
- package/src/assets/adapters/twitch/wrench-web-adapter.json +42 -0
- package/src/assets/adapters/twitch/wrench-web-adapter.v1.0.0.json +42 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.json +400 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.0.0.json +355 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.1.0.json +386 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.2.0.json +386 -0
- package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.3.0.json +400 -0
- package/src/assets/adapters/x/wrench-adapter.json +749 -0
- package/src/assets/adapters/x/wrench-adapter.v1.0.0.json +700 -0
- package/src/assets/adapters/x/wrench-adapter.v1.1.0.json +707 -0
- package/src/assets/adapters/x/wrench-web-adapter.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.1.0.json +579 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.10.0.json +685 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.11.0.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.12.0.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.13.0.json +712 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.2.0.json +578 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.3.0.json +610 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.4.0.json +617 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.5.0.json +634 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.6.0.json +632 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.7.0.json +649 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.8.0.json +649 -0
- package/src/assets/adapters/x/wrench-web-adapter.v1.9.0.json +683 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.json +561 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.0.0.json +465 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.1.0.json +519 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.2.0.json +561 -0
- package/src/assets/adapters/youtube/wrench-web-adapter.v1.3.0.json +561 -0
- package/src/assets/code-owned-provider-template/plugin.test.ts.template +54 -0
- package/src/assets/code-owned-provider-template/plugin.ts.template +102 -0
- package/src/assets/code-owned-provider-template/runtime.internal.test.ts.template +44 -0
- package/src/assets/code-owned-provider-template/runtime.ts.template +59 -0
- package/src/assets/code-owned-provider-template/wrench-adapter.json.template +33 -0
- package/src/assets/fonts/nebula-sans/LICENSE.txt +96 -0
- package/src/assets/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
- package/src/assets/fonts/nebula-sans/PROVENANCE.md +14 -0
- package/src/assets.d.ts +4 -0
- package/src/auth.ts +1157 -0
- package/src/beeper-client-types.ts +169 -0
- package/src/beeper-client.ts +250 -0
- package/src/beeper-contact-interactions-cli.ts +180 -0
- package/src/beeper-contact-interactions.ts +1612 -0
- package/src/beeper-message-bundle-v1.ts +257 -0
- package/src/beeper-message-like-me-cli.ts +84 -0
- package/src/beeper-message-like-me-export.ts +2274 -0
- package/src/beeper-message-like-me-recovery.ts +931 -0
- package/src/beeper-message-like-me-source.ts +4034 -0
- package/src/browser-admission.ts +928 -0
- package/src/browser-snapshots.ts +260 -0
- package/src/browser.ts +3873 -0
- package/src/canonical-json.ts +60 -0
- package/src/catalog-cli.ts +638 -0
- package/src/cli.ts +208 -0
- package/src/client-types.ts +301 -0
- package/src/client.ts +2726 -0
- package/src/confirmed-write-failure.ts +44 -0
- package/src/confirmed-write-model.ts +72 -0
- package/src/confirmed-write-platform.ts +1055 -0
- package/src/confirmed-write-program.ts +224 -0
- package/src/confirmed-write-runtime.ts +16 -0
- package/src/cursor-token.ts +228 -0
- package/src/derivation-dnr-readiness.ts +367 -0
- package/src/derivation-file-chooser.ts +384 -0
- package/src/derivation-network-boundary.ts +835 -0
- package/src/derivation-network-guard.ts +782 -0
- package/src/derivation-network-proxy-helper.ts +440 -0
- package/src/derivation-network-proxy.ts +385 -0
- package/src/derive-command-helper.ts +693 -0
- package/src/derive-fixtures.ts +343 -0
- package/src/derive-review.ts +980 -0
- package/src/derive.ts +4754 -0
- package/src/fixtures/beeper-message-like-me-v1/accounts.ndjson +2 -0
- package/src/fixtures/beeper-message-like-me-v1/conversations.ndjson +1 -0
- package/src/fixtures/beeper-message-like-me-v1/manifest.json +1 -0
- package/src/fixtures/beeper-message-like-me-v1/messages.ndjson +2 -0
- package/src/fixtures/beeper-message-like-me-v1/participants.ndjson +3 -0
- package/src/fixtures/beeper-message-like-me-v1/reactions.ndjson +1 -0
- package/src/fixtures/beeper-message-like-me-v1/tombstones.ndjson +1 -0
- package/src/ghostget.ts +3672 -0
- package/src/gmail-capture.ts +1244 -0
- package/src/har-internal.ts +1242 -0
- package/src/har.ts +743 -0
- package/src/index.ts +218 -0
- package/src/invocation-read-platform.ts +55 -0
- package/src/invocation-read-program.ts +93 -0
- package/src/invocation-read-runtime.ts +11 -0
- package/src/linked-device-lifecycle-admission.ts +450 -0
- package/src/linked-device-lifecycle-journal.ts +1560 -0
- package/src/linked-device-lifecycle-runtime.ts +1602 -0
- package/src/local-cli-admission.ts +135 -0
- package/src/local-cli-contracts.ts +271 -0
- package/src/local-cli-execution.ts +98 -0
- package/src/local-cli-surface-contract.ts +1510 -0
- package/src/local-cli-tool-identity.ts +336 -0
- package/src/media/archive.ts +2325 -0
- package/src/media/args.ts +303 -0
- package/src/media/cli.ts +287 -0
- package/src/media/doctor.ts +475 -0
- package/src/media/ffmpeg.ts +1330 -0
- package/src/media/http-capture.ts +711 -0
- package/src/media/http-probe.ts +593 -0
- package/src/media/http.ts +387 -0
- package/src/media/index.ts +10 -0
- package/src/media/local-transcription.ts +613 -0
- package/src/media/lock.ts +321 -0
- package/src/media/manifest.ts +1712 -0
- package/src/media/metadata.ts +906 -0
- package/src/media/process.ts +723 -0
- package/src/media/revision.ts +166 -0
- package/src/media/runtime-closure.ts +1063 -0
- package/src/media/source-router.ts +187 -0
- package/src/media/transcriber-config.ts +1122 -0
- package/src/media/transcript-persistence-model.ts +15 -0
- package/src/media/transcript-persistence-platform.ts +89 -0
- package/src/media/transcript-persistence-program.ts +39 -0
- package/src/media/transcript-persistence-runtime.ts +24 -0
- package/src/media/transcript.ts +616 -0
- package/src/media/utf8-order.ts +30 -0
- package/src/media/whisper-cpp.ts +464 -0
- package/src/media/whisper-language.ts +58 -0
- package/src/media/yt-dlp.ts +439 -0
- package/src/message-like-me-agentic-messaging.ts +882 -0
- package/src/messaging-action-store.ts +793 -0
- package/src/messaging-runtime.ts +2136 -0
- package/src/messaging-store.ts +539 -0
- package/src/messaging-types.ts +1663 -0
- package/src/messaging.ts +334 -0
- package/src/model.ts +2671 -0
- package/src/oauth-google.ts +888 -0
- package/src/omni-client-types.ts +306 -0
- package/src/omni-client.ts +2225 -0
- package/src/omni-limits.ts +34 -0
- package/src/omni-model.ts +1614 -0
- package/src/omni-request.ts +379 -0
- package/src/omni-runtime.ts +1632 -0
- package/src/operation-deadline.ts +189 -0
- package/src/path-helper.ts +1656 -0
- package/src/pinned-https.ts +194 -0
- package/src/plan-assets.ts +832 -0
- package/src/platform-catalog.ts +1832 -0
- package/src/plugins/beeper-linked-device/plugin.ts +310 -0
- package/src/plugins/bluesky-web/plugin.ts +178 -0
- package/src/plugins/clasificados-web/plugin.ts +66 -0
- package/src/plugins/github-web/plugin.ts +80 -0
- package/src/plugins/gmail-official/plugin.ts +110 -0
- package/src/plugins/hacker-news-web/plugin.ts +52 -0
- package/src/plugins/imessage-direct/plugin.ts +195 -0
- package/src/plugins/imessage-direct/vendor/0001-fix-keep-AppleScript-send-payloads-out-of-child-argv.patch +1430 -0
- package/src/plugins/imessage-direct/vendor/0002-feat-rpc-add-exact-chat-lookup.patch +225 -0
- package/src/plugins/imessage-direct/vendor/provenance.json +80 -0
- package/src/plugins/linkedin-official/plugin.ts +84 -0
- package/src/plugins/linkedin-web/plugin.ts +412 -0
- package/src/plugins/meta-web/plugin.ts +317 -0
- package/src/plugins/reddit-web/flair.ts +492 -0
- package/src/plugins/reddit-web/plugin.ts +477 -0
- package/src/plugins/substack-web/plugin.ts +124 -0
- package/src/plugins/tiktok-web/plugin.ts +67 -0
- package/src/plugins/twitch-web/plugin.ts +54 -0
- package/src/plugins/whatsapp-linked-device/plugin.ts +105 -0
- package/src/plugins/x-official/plugin.ts +64 -0
- package/src/plugins/x-web/plugin.ts +488 -0
- package/src/plugins/youtube-web/plugin.ts +101 -0
- package/src/portable-run-recovery.ts +466 -0
- package/src/process-identity.ts +501 -0
- package/src/profile-clone-helper.ts +104 -0
- package/src/provider-catalog-views.ts +51 -0
- package/src/provider-context.ts +69 -0
- package/src/provider-contract-definitions-x.ts +45 -0
- package/src/provider-contract-definitions.ts +948 -0
- package/src/provider-contract-input-gmail.ts +176 -0
- package/src/provider-contract-input-linkedin.ts +77 -0
- package/src/provider-contract-input-x.ts +304 -0
- package/src/provider-contract-planning.ts +23 -0
- package/src/provider-contract-semantic-identity.ts +54 -0
- package/src/provider-contracts.ts +251 -0
- package/src/provider-http.ts +723 -0
- package/src/provider-plugin-auth.ts +36 -0
- package/src/provider-plugin-builtins.ts +229 -0
- package/src/provider-plugin-cleanup-barrier.ts +229 -0
- package/src/provider-plugin-cleanup-execution.ts +79 -0
- package/src/provider-plugin-cleanup-resource.ts +470 -0
- package/src/provider-plugin-contract-identity.ts +707 -0
- package/src/provider-plugin-host.ts +1972 -0
- package/src/provider-plugin-identifiers.ts +60 -0
- package/src/provider-plugin-invocation-lease.ts +889 -0
- package/src/provider-plugin-lifecycle-kernel.ts +566 -0
- package/src/provider-plugin-lifecycle.ts +1455 -0
- package/src/provider-plugin-module-analysis.ts +336 -0
- package/src/provider-plugin-package.ts +2530 -0
- package/src/provider-plugin-portable-authority.ts +106 -0
- package/src/provider-plugin-portable-catalog.ts +94 -0
- package/src/provider-plugin-portable-identity.ts +315 -0
- package/src/provider-plugin-portable-runtime.ts +2057 -0
- package/src/provider-plugin-protocol.ts +1625 -0
- package/src/provider-plugin-registry.ts +3342 -0
- package/src/provider-plugin-store.ts +2155 -0
- package/src/provider-plugin-test-fixture.ts +5 -0
- package/src/provider-plugin.ts +4907 -0
- package/src/provider-plugins.generated.ts +43 -0
- package/src/provider-plugins.ts +8 -0
- package/src/provider-subject.ts +41 -0
- package/src/provider.ts +315 -0
- package/src/providers/beeper-local-runtime.ts +5426 -0
- package/src/providers/beeper-local.ts +2739 -0
- package/src/providers/beeper-messaging.ts +462 -0
- package/src/providers/beeper-omni.ts +1159 -0
- package/src/providers/bluesky-web-runtime.ts +2895 -0
- package/src/providers/bluesky-web.ts +1614 -0
- package/src/providers/clasificados-web-runtime.ts +255 -0
- package/src/providers/clasificados-web.ts +503 -0
- package/src/providers/contact-projection.ts +301 -0
- package/src/providers/github-read-model.ts +94 -0
- package/src/providers/github-read-platform.ts +165 -0
- package/src/providers/github-read-program.ts +97 -0
- package/src/providers/github-web-runtime.ts +320 -0
- package/src/providers/github-web.ts +403 -0
- package/src/providers/gmail-api.ts +2834 -0
- package/src/providers/gmail-omni.ts +830 -0
- package/src/providers/gmail.ts +1130 -0
- package/src/providers/hacker-news-web-runtime.ts +220 -0
- package/src/providers/hacker-news-web.ts +809 -0
- package/src/providers/imessage-direct-install.ts +354 -0
- package/src/providers/imessage-direct-messaging.ts +532 -0
- package/src/providers/imessage-direct-omni.ts +400 -0
- package/src/providers/imessage-direct-runtime.ts +1498 -0
- package/src/providers/imessage-direct.ts +351 -0
- package/src/providers/instagram-video-foundations.ts +913 -0
- package/src/providers/instagram-web-profile-browser.ts +600 -0
- package/src/providers/iso-bmff.ts +338 -0
- package/src/providers/linkedin-company-platform.ts +42 -0
- package/src/providers/linkedin-company-program.ts +88 -0
- package/src/providers/linkedin-contact-failure.ts +84 -0
- package/src/providers/linkedin-contact-platform.ts +45 -0
- package/src/providers/linkedin-contact-program.ts +91 -0
- package/src/providers/linkedin-profile-activity-failure.ts +76 -0
- package/src/providers/linkedin-profile-activity-platform.ts +41 -0
- package/src/providers/linkedin-profile-activity-program.ts +67 -0
- package/src/providers/linkedin-read-failure.ts +52 -0
- package/src/providers/linkedin-self-platform.ts +53 -0
- package/src/providers/linkedin-self-program.ts +122 -0
- package/src/providers/linkedin-web-article-browser.ts +794 -0
- package/src/providers/linkedin-web-bootstrap.ts +146 -0
- package/src/providers/linkedin-web-contact.ts +830 -0
- package/src/providers/linkedin-web-feed-browser.ts +832 -0
- package/src/providers/linkedin-web-feed.ts +864 -0
- package/src/providers/linkedin-web-post-browser.ts +653 -0
- package/src/providers/linkedin-web-profile-browser.ts +683 -0
- package/src/providers/linkedin-web-runtime.ts +2388 -0
- package/src/providers/linkedin-web.ts +3305 -0
- package/src/providers/linkedin.ts +1885 -0
- package/src/providers/meta-bootstrap.ts +1043 -0
- package/src/providers/meta-facebook-group.ts +690 -0
- package/src/providers/meta-marketplace-relay.ts +747 -0
- package/src/providers/meta-omni.ts +373 -0
- package/src/providers/meta-relay-bundle.ts +1049 -0
- package/src/providers/meta-web-descriptors.ts +1740 -0
- package/src/providers/meta-web-runtime.ts +3388 -0
- package/src/providers/meta-web.ts +2931 -0
- package/src/providers/read-failure.ts +139 -0
- package/src/providers/reddit-omni.ts +462 -0
- package/src/providers/reddit-web-runtime.ts +1929 -0
- package/src/providers/reddit-web.ts +1611 -0
- package/src/providers/rental-listings.ts +460 -0
- package/src/providers/substack-omni.ts +406 -0
- package/src/providers/substack-video-mp4.ts +33 -0
- package/src/providers/substack-web-runtime.ts +2812 -0
- package/src/providers/substack-web.ts +1502 -0
- package/src/providers/tiktok-video-mp4.ts +33 -0
- package/src/providers/tiktok-web-runtime.ts +770 -0
- package/src/providers/tiktok-web.ts +2786 -0
- package/src/providers/twitch-web-runtime.ts +226 -0
- package/src/providers/twitch-web.ts +274 -0
- package/src/providers/whatsapp-account-identity.ts +55 -0
- package/src/providers/whatsapp-contact-projection-helper.ts +874 -0
- package/src/providers/whatsapp-contact-projection-protocol.ts +403 -0
- package/src/providers/whatsapp-interaction-projection-helper.ts +1452 -0
- package/src/providers/whatsapp-interaction-projection-protocol.ts +462 -0
- package/src/providers/whatsapp-message-export-projection-protocol.ts +532 -0
- package/src/providers/whatsapp-messaging.ts +132 -0
- package/src/providers/whatsapp-omni.ts +586 -0
- package/src/providers/whatsapp-web-runtime.ts +3108 -0
- package/src/providers/whatsapp-web.ts +1037 -0
- package/src/providers/x-image-provenance.ts +523 -0
- package/src/providers/x-made-with-ai.ts +106 -0
- package/src/providers/x-transaction-id.ts +413 -0
- package/src/providers/x-web-runtime.ts +3485 -0
- package/src/providers/x-web.ts +2332 -0
- package/src/providers/x.ts +1889 -0
- package/src/providers/youtube-web-runtime.ts +1400 -0
- package/src/providers/youtube-web.ts +1221 -0
- package/src/read-admission-runtime.ts +91 -0
- package/src/read-client.ts +521 -0
- package/src/read-effect-platform.ts +7 -0
- package/src/read-effect-runtime.ts +14 -0
- package/src/read-effect.ts +52 -0
- package/src/read-projection-admission.ts +866 -0
- package/src/read-projections.ts +3151 -0
- package/src/recovery.ts +1492 -0
- package/src/reviewed-template.ts +146 -0
- package/src/run-journal.ts +1272 -0
- package/src/runtime.ts +5116 -0
- package/src/scripts/generate-provider-plugin-catalog.ts +121 -0
- package/src/scripts/install-whatsapp-protocol.sh +232 -0
- package/src/scripts/recover-install-stages.ts +1491 -0
- package/src/scripts/resolve-state-home.ts +30 -0
- package/src/scripts/scaffold-web-provider.ts +973 -0
- package/src/scripts/sync-bundled-adapters.ts +695 -0
- package/src/session-secrets.ts +1183 -0
- package/src/state-helper.bunfig.toml +2 -0
- package/src/state-helper.ts +2914 -0
- package/src/storage.ts +3842 -0
- package/src/transport-policy.ts +2 -0
- package/src/usage.ts +182 -0
- package/src/version.ts +2 -0
- package/src/web-session-authentication-policy.ts +136 -0
- package/src/web-session-cleanup-admission.ts +2344 -0
- package/src/web-session-cleanup-join.ts +99 -0
- package/src/web-session-client.ts +996 -0
- package/src/web-session-contract-definitions.ts +820 -0
- package/src/web-session-contract-planning.ts +42 -0
- package/src/web-session-contracts.ts +228 -0
- package/src/web-session-cookies.ts +500 -0
- package/src/web-session-execution.ts +385 -0
- package/src/web-session-http.ts +373 -0
- package/src/web-session-read-errors.ts +19 -0
- package/src/web-session-read-runtime.ts +124 -0
- package/src/web-session-recovery.ts +818 -0
- package/src/web-session-sites.ts +58 -0
- package/src/web-session-template.ts +994 -0
- package/src/web-session.ts +93 -0
- package/src/whatsapp-client-binding.ts +21 -0
- package/src/whatsapp-client-types.ts +75 -0
- package/src/whatsapp-client.ts +395 -0
- package/src/whatsapp-export-coordinate.ts +17 -0
- package/src/whatsapp-message-bundle-v2.ts +195 -0
- package/src/whatsapp-message-like-me-cli.ts +195 -0
- package/src/whatsapp-message-like-me-export.ts +1072 -0
- package/src/whatsapp-message-like-me-source.ts +1012 -0
- package/tsconfig.json +67 -0
|
@@ -0,0 +1,830 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { types as nodeTypes } from "node:util";
|
|
3
|
+
|
|
4
|
+
import type { OperationInput } from "../model";
|
|
5
|
+
import { OmniMaterializerDriftError } from "../omni-model";
|
|
6
|
+
import type {
|
|
7
|
+
OmniAttachmentV1,
|
|
8
|
+
OmniParticipantV1,
|
|
9
|
+
ProviderConversationV1,
|
|
10
|
+
ProviderMaterializedPageV1,
|
|
11
|
+
ProviderMessageV1,
|
|
12
|
+
} from "../omni-model";
|
|
13
|
+
import { isGmailAccountSubject } from "../provider-subject";
|
|
14
|
+
import { parseGmailThreadUrl } from "./gmail-api";
|
|
15
|
+
|
|
16
|
+
type JsonRecord = Readonly<Record<string, unknown>>;
|
|
17
|
+
type GmailListView = "inbox" | "search";
|
|
18
|
+
|
|
19
|
+
const GMAIL_OMNI_BODY_BYTES = 256 * 1024;
|
|
20
|
+
const GMAIL_AGGREGATE_BODY_BYTES = 7 * 1024 * 1024;
|
|
21
|
+
const fatalUtf8Decoder = new TextDecoder("utf-8", {
|
|
22
|
+
fatal: true,
|
|
23
|
+
ignoreBOM: true,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
type ParsedListInput = Readonly<{
|
|
27
|
+
view: GmailListView;
|
|
28
|
+
query: string | null;
|
|
29
|
+
cursor: string | null;
|
|
30
|
+
limit: number;
|
|
31
|
+
includeSpamTrash: boolean;
|
|
32
|
+
}>;
|
|
33
|
+
|
|
34
|
+
type ParsedParticipant = Readonly<{
|
|
35
|
+
email: string;
|
|
36
|
+
displayName: null;
|
|
37
|
+
}>;
|
|
38
|
+
|
|
39
|
+
type ParsedThreadSummary = Readonly<{
|
|
40
|
+
id: string;
|
|
41
|
+
historyId: string | null;
|
|
42
|
+
snippet: string | null;
|
|
43
|
+
subject: string | null;
|
|
44
|
+
orderedAt: string | null;
|
|
45
|
+
messageCount: number;
|
|
46
|
+
participants: readonly ParsedParticipant[];
|
|
47
|
+
unread: boolean;
|
|
48
|
+
archived: boolean | null;
|
|
49
|
+
}>;
|
|
50
|
+
|
|
51
|
+
type ParsedAttachment = Readonly<{
|
|
52
|
+
partId: string;
|
|
53
|
+
contentDisposition: "attachment" | "inline" | null;
|
|
54
|
+
attachmentId: string | null;
|
|
55
|
+
messageId: string;
|
|
56
|
+
filename: string;
|
|
57
|
+
mimeType: string;
|
|
58
|
+
size: number;
|
|
59
|
+
}>;
|
|
60
|
+
|
|
61
|
+
type ParsedMessage = Readonly<{
|
|
62
|
+
id: string;
|
|
63
|
+
threadId: string | null;
|
|
64
|
+
historyId: string | null;
|
|
65
|
+
internalDate: string | null;
|
|
66
|
+
labelIds: readonly string[];
|
|
67
|
+
from: string | null;
|
|
68
|
+
to: string | null;
|
|
69
|
+
cc: string | null;
|
|
70
|
+
bcc: string | null;
|
|
71
|
+
subject: string | null;
|
|
72
|
+
messageId: string | null;
|
|
73
|
+
inReplyTo: string | null;
|
|
74
|
+
text: string | null;
|
|
75
|
+
bodyTruncated: boolean;
|
|
76
|
+
originalBodyBytes: number;
|
|
77
|
+
attachments: readonly ParsedAttachment[];
|
|
78
|
+
}>;
|
|
79
|
+
|
|
80
|
+
function drift(path: string, message: string): never {
|
|
81
|
+
throw new OmniMaterializerDriftError("gmail", path, message);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function record(value: unknown, path: string): JsonRecord {
|
|
85
|
+
if (
|
|
86
|
+
nodeTypes.isProxy(value)
|
|
87
|
+
|| typeof value !== "object"
|
|
88
|
+
|| value === null
|
|
89
|
+
|| Array.isArray(value)
|
|
90
|
+
|| (
|
|
91
|
+
Object.getPrototypeOf(value) !== Object.prototype
|
|
92
|
+
&& Object.getPrototypeOf(value) !== null
|
|
93
|
+
)
|
|
94
|
+
) return drift(path, "must be a plain object");
|
|
95
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
96
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
97
|
+
if (typeof key !== "string") return drift(path, "must not have symbol properties");
|
|
98
|
+
const descriptor = descriptors[key];
|
|
99
|
+
if (
|
|
100
|
+
descriptor === undefined
|
|
101
|
+
|| !("value" in descriptor)
|
|
102
|
+
|| !descriptor.enumerable
|
|
103
|
+
) return drift(`${path}.${key}`, "must be an enumerable data property");
|
|
104
|
+
}
|
|
105
|
+
return value as JsonRecord;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function exactKeys(
|
|
109
|
+
value: JsonRecord,
|
|
110
|
+
required: readonly string[],
|
|
111
|
+
optional: readonly string[],
|
|
112
|
+
path: string,
|
|
113
|
+
): void {
|
|
114
|
+
const allowed = new Set([...required, ...optional]);
|
|
115
|
+
for (const key of Object.keys(value)) {
|
|
116
|
+
if (!allowed.has(key)) return drift(path, "contains an unreviewed property");
|
|
117
|
+
}
|
|
118
|
+
for (const key of required) {
|
|
119
|
+
if (!Object.hasOwn(value, key)) return drift(`${path}.${key}`, "is required");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function array(value: unknown, path: string, maximum: number): readonly unknown[] {
|
|
124
|
+
if (
|
|
125
|
+
nodeTypes.isProxy(value)
|
|
126
|
+
|| !Array.isArray(value)
|
|
127
|
+
|| Object.getPrototypeOf(value) !== Array.prototype
|
|
128
|
+
|| value.length > maximum
|
|
129
|
+
) return drift(path, `must be an array of at most ${maximum} items`);
|
|
130
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
131
|
+
const expected = new Set<PropertyKey>([
|
|
132
|
+
"length",
|
|
133
|
+
...Array.from({ length: value.length }, (_unused, index) => String(index)),
|
|
134
|
+
]);
|
|
135
|
+
if (
|
|
136
|
+
Reflect.ownKeys(descriptors).length !== expected.size
|
|
137
|
+
|| Reflect.ownKeys(descriptors).some((key) => !expected.has(key))
|
|
138
|
+
) return drift(path, "must be a dense array without named properties");
|
|
139
|
+
const result: unknown[] = [];
|
|
140
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
141
|
+
const descriptor = descriptors[String(index)];
|
|
142
|
+
if (
|
|
143
|
+
descriptor === undefined
|
|
144
|
+
|| !("value" in descriptor)
|
|
145
|
+
|| !descriptor.enumerable
|
|
146
|
+
) return drift(`${path}[${index}]`, "must be an enumerable data property");
|
|
147
|
+
result.push(descriptor.value);
|
|
148
|
+
}
|
|
149
|
+
return Object.freeze(result);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function hasUnsafeControl(value: string, allowLayout = false): boolean {
|
|
153
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
154
|
+
const code = value.charCodeAt(index);
|
|
155
|
+
if (
|
|
156
|
+
(code <= 0x1f && !(allowLayout && (code === 0x09 || code === 0x0a || code === 0x0d)))
|
|
157
|
+
|| code === 0x7f
|
|
158
|
+
|| (code >= 0x80 && code <= 0x9f)
|
|
159
|
+
|| code === 0x061c
|
|
160
|
+
|| code === 0x200e
|
|
161
|
+
|| code === 0x200f
|
|
162
|
+
|| (code >= 0x2028 && code <= 0x202e)
|
|
163
|
+
|| (code >= 0x2066 && code <= 0x2069)
|
|
164
|
+
) return true;
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isWellFormedText(value: string): boolean {
|
|
170
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
171
|
+
const code = value.charCodeAt(index);
|
|
172
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
173
|
+
const next = value.charCodeAt(index + 1);
|
|
174
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) return false;
|
|
175
|
+
index += 1;
|
|
176
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) return false;
|
|
177
|
+
}
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function string(
|
|
182
|
+
value: unknown,
|
|
183
|
+
path: string,
|
|
184
|
+
maximum: number,
|
|
185
|
+
allowEmpty = false,
|
|
186
|
+
allowLayout = false,
|
|
187
|
+
): string {
|
|
188
|
+
if (
|
|
189
|
+
typeof value !== "string"
|
|
190
|
+
|| !isWellFormedText(value)
|
|
191
|
+
|| Buffer.byteLength(value, "utf8") > maximum
|
|
192
|
+
|| (!allowEmpty && value.length === 0)
|
|
193
|
+
|| hasUnsafeControl(value, allowLayout)
|
|
194
|
+
) return drift(path, "must be bounded text without unsafe controls");
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function inputString(value: unknown, path: string, maximum: number): string {
|
|
199
|
+
if (
|
|
200
|
+
typeof value !== "string"
|
|
201
|
+
|| value.length === 0
|
|
202
|
+
|| value.length > maximum
|
|
203
|
+
|| !isWellFormedText(value)
|
|
204
|
+
|| hasUnsafeControl(value)
|
|
205
|
+
) return drift(path, "must be bounded input text without unsafe controls");
|
|
206
|
+
return value;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function nullableString(
|
|
210
|
+
value: unknown,
|
|
211
|
+
path: string,
|
|
212
|
+
maximum: number,
|
|
213
|
+
allowEmpty = false,
|
|
214
|
+
allowLayout = false,
|
|
215
|
+
): string | null {
|
|
216
|
+
return value === null
|
|
217
|
+
? null
|
|
218
|
+
: string(value, path, maximum, allowEmpty, allowLayout);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function integer(value: unknown, path: string, minimum: number, maximum: number): number {
|
|
222
|
+
if (
|
|
223
|
+
typeof value !== "number"
|
|
224
|
+
|| !Number.isSafeInteger(value)
|
|
225
|
+
|| value < minimum
|
|
226
|
+
|| value > maximum
|
|
227
|
+
) return drift(path, `must be an integer from ${minimum} through ${maximum}`);
|
|
228
|
+
return value;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function boolean(value: unknown, path: string): boolean {
|
|
232
|
+
if (typeof value !== "boolean") return drift(path, "must be boolean");
|
|
233
|
+
return value;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function nullableBoolean(value: unknown, path: string): boolean | null {
|
|
237
|
+
return value === null ? null : boolean(value, path);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function gmailId(value: unknown, path: string): string {
|
|
241
|
+
const result = string(value, path, 256);
|
|
242
|
+
if (!/^[A-Za-z0-9_-]{1,256}$/u.test(result)) {
|
|
243
|
+
return drift(path, "must be an exact Gmail ID");
|
|
244
|
+
}
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function attachmentId(value: unknown, path: string): string | null {
|
|
249
|
+
return value === null ? null : string(value, path, 4_096);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function historyId(value: unknown, path: string): string | null {
|
|
253
|
+
if (value === null) return null;
|
|
254
|
+
const result = string(value, path, 64);
|
|
255
|
+
if (!/^[0-9]{1,64}$/u.test(result)) return drift(path, "must be a decimal history ID");
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function timestamp(value: unknown, path: string): string | null {
|
|
260
|
+
if (value === null) return null;
|
|
261
|
+
const result = string(value, path, 64);
|
|
262
|
+
const date = new Date(result);
|
|
263
|
+
if (!Number.isFinite(date.getTime()) || date.toISOString() !== result) {
|
|
264
|
+
return drift(path, "must be a canonical ISO timestamp or null");
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function accountSubject(value: unknown, path: string): string {
|
|
270
|
+
const result = string(value, path, 254);
|
|
271
|
+
if (!isGmailAccountSubject(result)) return drift(path, "must be an exact Gmail email subject");
|
|
272
|
+
return result;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function parseListInput(input: OperationInput): ParsedListInput {
|
|
276
|
+
const source = record(input, "messaging.list input");
|
|
277
|
+
exactKeys(source, ["view"], [
|
|
278
|
+
"query",
|
|
279
|
+
"cursor",
|
|
280
|
+
"limit",
|
|
281
|
+
"include_spam_trash",
|
|
282
|
+
], "messaging.list input");
|
|
283
|
+
if (source.view !== "inbox" && source.view !== "search") {
|
|
284
|
+
return drift("messaging.list input.view", "must be inbox or search");
|
|
285
|
+
}
|
|
286
|
+
const query = source.query === undefined
|
|
287
|
+
? null
|
|
288
|
+
: inputString(source.query, "messaging.list input.query", 512);
|
|
289
|
+
if (source.view === "search" && query === null) {
|
|
290
|
+
return drift("messaging.list input.query", "is required when view is search");
|
|
291
|
+
}
|
|
292
|
+
if (source.view === "inbox" && query !== null) {
|
|
293
|
+
return drift("messaging.list input.query", "is not accepted when view is inbox");
|
|
294
|
+
}
|
|
295
|
+
if (query !== null && query.trim().length === 0) {
|
|
296
|
+
return drift("messaging.list input.query", "must contain a non-whitespace expression");
|
|
297
|
+
}
|
|
298
|
+
const includeSpamTrash = source.include_spam_trash === undefined
|
|
299
|
+
? false
|
|
300
|
+
: boolean(source.include_spam_trash, "messaging.list input.include_spam_trash");
|
|
301
|
+
if (source.view === "inbox" && source.include_spam_trash !== undefined) {
|
|
302
|
+
return drift("messaging.list input.include_spam_trash", "is accepted only for search");
|
|
303
|
+
}
|
|
304
|
+
return Object.freeze({
|
|
305
|
+
view: source.view,
|
|
306
|
+
query,
|
|
307
|
+
cursor: source.cursor === undefined
|
|
308
|
+
? null
|
|
309
|
+
: inputString(source.cursor, "messaging.list input.cursor", 4_096),
|
|
310
|
+
limit: source.limit === undefined
|
|
311
|
+
? 50
|
|
312
|
+
: integer(source.limit, "messaging.list input.limit", 1, 100),
|
|
313
|
+
includeSpamTrash,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function parseReadInput(input: OperationInput): string {
|
|
318
|
+
const source = record(input, "messaging.read input");
|
|
319
|
+
exactKeys(source, ["thread_id"], [], "messaging.read input");
|
|
320
|
+
return gmailId(source.thread_id, "messaging.read input.thread_id");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function parseParticipant(value: unknown, path: string): ParsedParticipant {
|
|
324
|
+
const source = record(value, path);
|
|
325
|
+
exactKeys(source, ["email", "displayName"], [], path);
|
|
326
|
+
if (source.displayName !== null) return drift(`${path}.displayName`, "must be null in the reviewed projection");
|
|
327
|
+
return Object.freeze({
|
|
328
|
+
email: accountSubject(source.email, `${path}.email`).toLowerCase(),
|
|
329
|
+
displayName: null,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function parseThreadUrl(
|
|
334
|
+
value: unknown,
|
|
335
|
+
path: string,
|
|
336
|
+
account: string,
|
|
337
|
+
expectedThreadId: string,
|
|
338
|
+
expectedView: "all" | "inbox",
|
|
339
|
+
): void {
|
|
340
|
+
const raw = string(value, path, 8_192);
|
|
341
|
+
let parsed;
|
|
342
|
+
try {
|
|
343
|
+
parsed = parseGmailThreadUrl(new URL(raw));
|
|
344
|
+
} catch {
|
|
345
|
+
return drift(path, "must be an exact reviewed Gmail thread URL");
|
|
346
|
+
}
|
|
347
|
+
if (
|
|
348
|
+
parsed.threadId !== expectedThreadId
|
|
349
|
+
|| parsed.view !== expectedView
|
|
350
|
+
|| parsed.accountLocator.toLowerCase() !== account.toLowerCase()
|
|
351
|
+
|| parsed.canonicalUrl !== raw
|
|
352
|
+
) return drift(path, "must bind the output account, view, and thread ID");
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function parseThreadSummary(
|
|
356
|
+
value: unknown,
|
|
357
|
+
path: string,
|
|
358
|
+
account: string,
|
|
359
|
+
view: GmailListView,
|
|
360
|
+
): ParsedThreadSummary {
|
|
361
|
+
const source = record(value, path);
|
|
362
|
+
exactKeys(source, [
|
|
363
|
+
"id",
|
|
364
|
+
"historyId",
|
|
365
|
+
"snippet",
|
|
366
|
+
"subject",
|
|
367
|
+
"orderedAt",
|
|
368
|
+
"messageCount",
|
|
369
|
+
"participants",
|
|
370
|
+
"unread",
|
|
371
|
+
"archived",
|
|
372
|
+
"threadUrl",
|
|
373
|
+
"readInput",
|
|
374
|
+
], [], path);
|
|
375
|
+
const id = gmailId(source.id, `${path}.id`);
|
|
376
|
+
const readInput = record(source.readInput, `${path}.readInput`);
|
|
377
|
+
exactKeys(readInput, ["thread_id"], [], `${path}.readInput`);
|
|
378
|
+
if (gmailId(readInput.thread_id, `${path}.readInput.thread_id`) !== id) {
|
|
379
|
+
return drift(`${path}.readInput.thread_id`, "must bind the summary thread ID");
|
|
380
|
+
}
|
|
381
|
+
parseThreadUrl(
|
|
382
|
+
source.threadUrl,
|
|
383
|
+
`${path}.threadUrl`,
|
|
384
|
+
account,
|
|
385
|
+
id,
|
|
386
|
+
view === "inbox" ? "inbox" : "all",
|
|
387
|
+
);
|
|
388
|
+
const participants = Object.freeze(array(source.participants, `${path}.participants`, 256)
|
|
389
|
+
.map((entry, index) => parseParticipant(entry, `${path}.participants[${index}]`)));
|
|
390
|
+
const participantIds = participants.map((participant) => participant.email);
|
|
391
|
+
if (new Set(participantIds).size !== participantIds.length) {
|
|
392
|
+
return drift(`${path}.participants`, "contains duplicate canonical email identities");
|
|
393
|
+
}
|
|
394
|
+
return Object.freeze({
|
|
395
|
+
id,
|
|
396
|
+
historyId: historyId(source.historyId, `${path}.historyId`),
|
|
397
|
+
snippet: nullableString(source.snippet, `${path}.snippet`, 64 * 1024, true, true),
|
|
398
|
+
subject: nullableString(source.subject, `${path}.subject`, 8_192, true, true),
|
|
399
|
+
orderedAt: timestamp(source.orderedAt, `${path}.orderedAt`),
|
|
400
|
+
messageCount: integer(source.messageCount, `${path}.messageCount`, 1, Number.MAX_SAFE_INTEGER),
|
|
401
|
+
participants,
|
|
402
|
+
unread: boolean(source.unread, `${path}.unread`),
|
|
403
|
+
archived: nullableBoolean(source.archived, `${path}.archived`),
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function materializedParticipant(value: ParsedParticipant): OmniParticipantV1 {
|
|
408
|
+
return Object.freeze({
|
|
409
|
+
providerId: value.email,
|
|
410
|
+
displayName: value.displayName,
|
|
411
|
+
handle: value.email,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function materializedConversation(value: ParsedThreadSummary): ProviderConversationV1 {
|
|
416
|
+
return Object.freeze({
|
|
417
|
+
kind: "conversation",
|
|
418
|
+
conversationKind: "unknown",
|
|
419
|
+
providerId: value.id,
|
|
420
|
+
providerRevision: value.historyId,
|
|
421
|
+
orderedAt: value.orderedAt,
|
|
422
|
+
detail: "summary",
|
|
423
|
+
title: value.subject,
|
|
424
|
+
summary: value.snippet,
|
|
425
|
+
participants: Object.freeze(value.participants.map(materializedParticipant)),
|
|
426
|
+
unread: value.unread,
|
|
427
|
+
unreadCount: null,
|
|
428
|
+
archived: value.archived,
|
|
429
|
+
pending: null,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function listPartition(account: string, input: ParsedListInput): string {
|
|
434
|
+
if (input.view === "inbox") return `${account.toLowerCase()}:gmail:threads:inbox`;
|
|
435
|
+
const digest = createHash("sha256")
|
|
436
|
+
.update(input.query ?? "", "utf8")
|
|
437
|
+
.digest("hex");
|
|
438
|
+
return `${account.toLowerCase()}:gmail:threads:search:${input.includeSpamTrash ? "all" : "ordinary"}:${digest}`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** Strict Gmail thread-list normalization with replayable opaque pagination. */
|
|
442
|
+
export function materializeGmailMessagingList(
|
|
443
|
+
input: OperationInput,
|
|
444
|
+
output: unknown,
|
|
445
|
+
): ProviderMaterializedPageV1 {
|
|
446
|
+
const parsedInput = parseListInput(input);
|
|
447
|
+
const source = record(output, "messaging.list output");
|
|
448
|
+
exactKeys(source, [
|
|
449
|
+
"provider",
|
|
450
|
+
"operation",
|
|
451
|
+
"accountSubject",
|
|
452
|
+
"view",
|
|
453
|
+
"query",
|
|
454
|
+
"includeSpamTrash",
|
|
455
|
+
"threads",
|
|
456
|
+
"nextCursor",
|
|
457
|
+
"resultSizeEstimate",
|
|
458
|
+
], [], "messaging.list output");
|
|
459
|
+
if (source.provider !== "gmail") return drift("messaging.list output.provider", "must be gmail");
|
|
460
|
+
if (source.operation !== "messaging.list") {
|
|
461
|
+
return drift("messaging.list output.operation", "must be messaging.list");
|
|
462
|
+
}
|
|
463
|
+
const account = accountSubject(source.accountSubject, "messaging.list output.accountSubject");
|
|
464
|
+
if (source.view !== parsedInput.view) {
|
|
465
|
+
return drift("messaging.list output.view", "must bind messaging.list input.view");
|
|
466
|
+
}
|
|
467
|
+
if (source.query !== parsedInput.query) {
|
|
468
|
+
return drift("messaging.list output.query", "must bind messaging.list input.query");
|
|
469
|
+
}
|
|
470
|
+
if (source.includeSpamTrash !== parsedInput.includeSpamTrash) {
|
|
471
|
+
return drift(
|
|
472
|
+
"messaging.list output.includeSpamTrash",
|
|
473
|
+
"must bind messaging.list input.include_spam_trash",
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
const summaries = Object.freeze(array(
|
|
477
|
+
source.threads,
|
|
478
|
+
"messaging.list output.threads",
|
|
479
|
+
parsedInput.limit,
|
|
480
|
+
).map((entry, index) => parseThreadSummary(
|
|
481
|
+
entry,
|
|
482
|
+
`messaging.list output.threads[${index}]`,
|
|
483
|
+
account,
|
|
484
|
+
parsedInput.view,
|
|
485
|
+
)));
|
|
486
|
+
const ids = summaries.map((thread) => thread.id);
|
|
487
|
+
if (new Set(ids).size !== ids.length) {
|
|
488
|
+
return drift("messaging.list output.threads", "contains duplicate stable thread IDs");
|
|
489
|
+
}
|
|
490
|
+
const nextCursor = nullableString(
|
|
491
|
+
source.nextCursor,
|
|
492
|
+
"messaging.list output.nextCursor",
|
|
493
|
+
4_096,
|
|
494
|
+
);
|
|
495
|
+
if (nextCursor !== null && nextCursor === parsedInput.cursor) {
|
|
496
|
+
return drift("messaging.list output.nextCursor", "must advance beyond the input cursor");
|
|
497
|
+
}
|
|
498
|
+
integer(
|
|
499
|
+
source.resultSizeEstimate,
|
|
500
|
+
"messaging.list output.resultSizeEstimate",
|
|
501
|
+
0,
|
|
502
|
+
Number.MAX_SAFE_INTEGER,
|
|
503
|
+
);
|
|
504
|
+
const complete = parsedInput.cursor === null && nextCursor === null;
|
|
505
|
+
const nextInput = nextCursor === null
|
|
506
|
+
? null
|
|
507
|
+
: parsedInput.view === "inbox"
|
|
508
|
+
? Object.freeze({
|
|
509
|
+
view: "inbox" as const,
|
|
510
|
+
cursor: nextCursor,
|
|
511
|
+
limit: parsedInput.limit,
|
|
512
|
+
})
|
|
513
|
+
: Object.freeze({
|
|
514
|
+
view: "search" as const,
|
|
515
|
+
query: parsedInput.query ?? "",
|
|
516
|
+
cursor: nextCursor,
|
|
517
|
+
limit: parsedInput.limit,
|
|
518
|
+
include_spam_trash: parsedInput.includeSpamTrash,
|
|
519
|
+
});
|
|
520
|
+
return Object.freeze({
|
|
521
|
+
schemaVersion: 1,
|
|
522
|
+
partition: listPartition(account, parsedInput),
|
|
523
|
+
completeness: Object.freeze({
|
|
524
|
+
kind: complete ? "complete" : "page",
|
|
525
|
+
reason: complete
|
|
526
|
+
? "Gmail returned the complete requested thread collection in one root page."
|
|
527
|
+
: nextCursor === null
|
|
528
|
+
? "Gmail returned a terminal continuation page without standalone partition completeness."
|
|
529
|
+
: "Gmail returned an opaque cursor for an older thread-list page.",
|
|
530
|
+
}),
|
|
531
|
+
cursor: Object.freeze({
|
|
532
|
+
direction: "backward",
|
|
533
|
+
request: parsedInput.cursor,
|
|
534
|
+
nextInput,
|
|
535
|
+
}),
|
|
536
|
+
entities: Object.freeze(summaries.map(materializedConversation)),
|
|
537
|
+
tombstones: Object.freeze([]),
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function parseHeader(value: unknown, path: string): string | null {
|
|
542
|
+
return nullableString(value, path, 64 * 1024, true, true);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function parseBody(
|
|
546
|
+
value: unknown,
|
|
547
|
+
path: string,
|
|
548
|
+
): Readonly<{
|
|
549
|
+
text: string | null;
|
|
550
|
+
truncated: boolean;
|
|
551
|
+
originalBytes: number;
|
|
552
|
+
}> {
|
|
553
|
+
const source = record(value, path);
|
|
554
|
+
exactKeys(source, ["text", "source"], [], path);
|
|
555
|
+
if (
|
|
556
|
+
source.source !== "text/plain"
|
|
557
|
+
&& source.source !== "text/html"
|
|
558
|
+
&& source.source !== "none"
|
|
559
|
+
) return drift(`${path}.source`, "must be text/plain, text/html, or none");
|
|
560
|
+
if (source.text === null) {
|
|
561
|
+
if (source.source !== "none") {
|
|
562
|
+
return drift(path, "must bind body text to its MIME projection source");
|
|
563
|
+
}
|
|
564
|
+
return Object.freeze({ text: null, truncated: false, originalBytes: 0 });
|
|
565
|
+
}
|
|
566
|
+
if (
|
|
567
|
+
typeof source.text !== "string"
|
|
568
|
+
|| source.text.length > GMAIL_AGGREGATE_BODY_BYTES
|
|
569
|
+
|| !isWellFormedText(source.text)
|
|
570
|
+
|| hasUnsafeControl(source.text, true)
|
|
571
|
+
) return drift(`${path}.text`, "must be bounded text without unsafe controls");
|
|
572
|
+
if (source.source === "none") {
|
|
573
|
+
return drift(path, "must bind body text to its MIME projection source");
|
|
574
|
+
}
|
|
575
|
+
const originalBytes = Buffer.byteLength(source.text, "utf8");
|
|
576
|
+
if (originalBytes > GMAIL_AGGREGATE_BODY_BYTES) {
|
|
577
|
+
return drift(
|
|
578
|
+
`${path}.text`,
|
|
579
|
+
`must not exceed the ${GMAIL_AGGREGATE_BODY_BYTES}-byte Gmail aggregate body ceiling`,
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
const encoded = Buffer.from(source.text, "utf8");
|
|
583
|
+
if (fatalUtf8Decoder.decode(encoded) !== source.text) {
|
|
584
|
+
return drift(`${path}.text`, "must contain valid Unicode scalar text");
|
|
585
|
+
}
|
|
586
|
+
if (encoded.byteLength <= GMAIL_OMNI_BODY_BYTES) {
|
|
587
|
+
return Object.freeze({
|
|
588
|
+
text: source.text,
|
|
589
|
+
truncated: false,
|
|
590
|
+
originalBytes: encoded.byteLength,
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
let prefixBytes = GMAIL_OMNI_BODY_BYTES;
|
|
594
|
+
while (
|
|
595
|
+
prefixBytes > 0
|
|
596
|
+
&& (encoded[prefixBytes]! & 0xc0) === 0x80
|
|
597
|
+
) prefixBytes -= 1;
|
|
598
|
+
return Object.freeze({
|
|
599
|
+
text: fatalUtf8Decoder.decode(encoded.subarray(0, prefixBytes)),
|
|
600
|
+
truncated: true,
|
|
601
|
+
originalBytes: encoded.byteLength,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function parseAttachment(
|
|
606
|
+
value: unknown,
|
|
607
|
+
path: string,
|
|
608
|
+
messageIdValue: string,
|
|
609
|
+
): ParsedAttachment {
|
|
610
|
+
const source = record(value, path);
|
|
611
|
+
exactKeys(source, [
|
|
612
|
+
"partId",
|
|
613
|
+
"contentDisposition",
|
|
614
|
+
"attachmentId",
|
|
615
|
+
"messageId",
|
|
616
|
+
"filename",
|
|
617
|
+
"mimeType",
|
|
618
|
+
"size",
|
|
619
|
+
], [], path);
|
|
620
|
+
const messageId = gmailId(source.messageId, `${path}.messageId`);
|
|
621
|
+
if (messageId !== messageIdValue) {
|
|
622
|
+
return drift(`${path}.messageId`, "must bind its containing Gmail message");
|
|
623
|
+
}
|
|
624
|
+
const mimeType = string(source.mimeType, `${path}.mimeType`, 256);
|
|
625
|
+
if (!/^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+(?:\s*;.*)?$/u.test(mimeType)) {
|
|
626
|
+
return drift(`${path}.mimeType`, "must be a bounded MIME type");
|
|
627
|
+
}
|
|
628
|
+
if (
|
|
629
|
+
source.contentDisposition !== null
|
|
630
|
+
&& source.contentDisposition !== "attachment"
|
|
631
|
+
&& source.contentDisposition !== "inline"
|
|
632
|
+
) {
|
|
633
|
+
return drift(`${path}.contentDisposition`, "must be attachment, inline, or null");
|
|
634
|
+
}
|
|
635
|
+
return Object.freeze({
|
|
636
|
+
partId: string(source.partId, `${path}.partId`, 256),
|
|
637
|
+
contentDisposition: source.contentDisposition,
|
|
638
|
+
attachmentId: attachmentId(source.attachmentId, `${path}.attachmentId`),
|
|
639
|
+
messageId,
|
|
640
|
+
filename: string(source.filename, `${path}.filename`, 8_192, true, true),
|
|
641
|
+
mimeType,
|
|
642
|
+
size: integer(source.size, `${path}.size`, 0, Number.MAX_SAFE_INTEGER),
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function parseMessage(
|
|
647
|
+
value: unknown,
|
|
648
|
+
path: string,
|
|
649
|
+
expectedThreadId: string,
|
|
650
|
+
): ParsedMessage {
|
|
651
|
+
const source = record(value, path);
|
|
652
|
+
exactKeys(source, [
|
|
653
|
+
"id",
|
|
654
|
+
"threadId",
|
|
655
|
+
"historyId",
|
|
656
|
+
"internalDate",
|
|
657
|
+
"labelIds",
|
|
658
|
+
"snippet",
|
|
659
|
+
"from",
|
|
660
|
+
"to",
|
|
661
|
+
"cc",
|
|
662
|
+
"bcc",
|
|
663
|
+
"subject",
|
|
664
|
+
"date",
|
|
665
|
+
"messageId",
|
|
666
|
+
"inReplyTo",
|
|
667
|
+
"body",
|
|
668
|
+
"attachments",
|
|
669
|
+
], [], path);
|
|
670
|
+
const id = gmailId(source.id, `${path}.id`);
|
|
671
|
+
const threadId = source.threadId === null
|
|
672
|
+
? null
|
|
673
|
+
: gmailId(source.threadId, `${path}.threadId`);
|
|
674
|
+
if (threadId !== null && threadId !== expectedThreadId) {
|
|
675
|
+
return drift(`${path}.threadId`, "must bind its containing Gmail thread");
|
|
676
|
+
}
|
|
677
|
+
const labelIds = Object.freeze(array(source.labelIds, `${path}.labelIds`, 1_000)
|
|
678
|
+
.map((entry, index) => string(entry, `${path}.labelIds[${index}]`, 256)));
|
|
679
|
+
if (new Set(labelIds).size !== labelIds.length) {
|
|
680
|
+
return drift(`${path}.labelIds`, "contains duplicate label IDs");
|
|
681
|
+
}
|
|
682
|
+
// Validate the snippet even though full message bodies are authoritative.
|
|
683
|
+
nullableString(source.snippet, `${path}.snippet`, 64 * 1024, true, true);
|
|
684
|
+
const body = parseBody(source.body, `${path}.body`);
|
|
685
|
+
return Object.freeze({
|
|
686
|
+
id,
|
|
687
|
+
threadId: threadId ?? expectedThreadId,
|
|
688
|
+
historyId: historyId(source.historyId, `${path}.historyId`),
|
|
689
|
+
internalDate: timestamp(source.internalDate, `${path}.internalDate`),
|
|
690
|
+
labelIds,
|
|
691
|
+
from: parseHeader(source.from, `${path}.from`),
|
|
692
|
+
to: parseHeader(source.to, `${path}.to`),
|
|
693
|
+
cc: parseHeader(source.cc, `${path}.cc`),
|
|
694
|
+
bcc: parseHeader(source.bcc, `${path}.bcc`),
|
|
695
|
+
subject: parseHeader(source.subject, `${path}.subject`),
|
|
696
|
+
date: parseHeader(source.date, `${path}.date`),
|
|
697
|
+
messageId: parseHeader(source.messageId, `${path}.messageId`),
|
|
698
|
+
inReplyTo: parseHeader(source.inReplyTo, `${path}.inReplyTo`),
|
|
699
|
+
text: body.text,
|
|
700
|
+
bodyTruncated: body.truncated,
|
|
701
|
+
originalBodyBytes: body.originalBytes,
|
|
702
|
+
attachments: Object.freeze(array(source.attachments, `${path}.attachments`, 512)
|
|
703
|
+
.map((entry, index) => parseAttachment(entry, `${path}.attachments[${index}]`, id))),
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function extractEmails(value: string | null): readonly string[] {
|
|
708
|
+
if (value === null) return Object.freeze([]);
|
|
709
|
+
const matches = value.match(/[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Za-z0-9.-]+/gu) ?? [];
|
|
710
|
+
return Object.freeze([...new Set(matches
|
|
711
|
+
.filter((candidate) => isGmailAccountSubject(candidate))
|
|
712
|
+
.map((candidate) => candidate.toLowerCase()))]);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
function participant(email: string): OmniParticipantV1 {
|
|
716
|
+
return Object.freeze({ providerId: email, displayName: null, handle: email });
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function attachmentKind(mimeType: string): OmniAttachmentV1["kind"] {
|
|
720
|
+
if (mimeType.startsWith("audio/")) return "audio";
|
|
721
|
+
if (mimeType.startsWith("image/")) return "image";
|
|
722
|
+
if (mimeType.startsWith("video/")) return "video";
|
|
723
|
+
return "document";
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function materializedAttachment(value: ParsedAttachment): OmniAttachmentV1 {
|
|
727
|
+
return Object.freeze({
|
|
728
|
+
kind: attachmentKind(value.mimeType),
|
|
729
|
+
mimeType: value.mimeType,
|
|
730
|
+
name: value.filename,
|
|
731
|
+
sizeBytes: value.size,
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
function materializedMessage(value: ParsedMessage): ProviderMessageV1 {
|
|
736
|
+
const senderEmail = extractEmails(value.from)[0];
|
|
737
|
+
const recipientEmails = [...new Set([
|
|
738
|
+
...extractEmails(value.to),
|
|
739
|
+
...extractEmails(value.cc),
|
|
740
|
+
...extractEmails(value.bcc),
|
|
741
|
+
])];
|
|
742
|
+
return Object.freeze({
|
|
743
|
+
kind: "message",
|
|
744
|
+
providerId: value.id,
|
|
745
|
+
providerRevision: value.historyId,
|
|
746
|
+
orderedAt: value.internalDate,
|
|
747
|
+
conversationProviderId: value.threadId,
|
|
748
|
+
sender: senderEmail === undefined ? null : participant(senderEmail),
|
|
749
|
+
recipients: Object.freeze(recipientEmails.map(participant)),
|
|
750
|
+
direction: value.labelIds.includes("SENT") ? "outgoing" : "incoming",
|
|
751
|
+
subject: value.subject,
|
|
752
|
+
body: value.text,
|
|
753
|
+
bodyTruncated: value.bodyTruncated,
|
|
754
|
+
unread: value.labelIds.includes("UNREAD"),
|
|
755
|
+
replyToProviderId: null,
|
|
756
|
+
state: "active",
|
|
757
|
+
attachments: Object.freeze(value.attachments.map(materializedAttachment)),
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/** Strict full-thread normalization with message and attachment metadata only. */
|
|
762
|
+
export function materializeGmailMessagingRead(
|
|
763
|
+
input: OperationInput,
|
|
764
|
+
output: unknown,
|
|
765
|
+
): ProviderMaterializedPageV1 {
|
|
766
|
+
const requestedThreadId = parseReadInput(input);
|
|
767
|
+
const source = record(output, "messaging.read output");
|
|
768
|
+
exactKeys(source, [
|
|
769
|
+
"provider",
|
|
770
|
+
"operation",
|
|
771
|
+
"accountSubject",
|
|
772
|
+
"thread",
|
|
773
|
+
"threadUrl",
|
|
774
|
+
], [], "messaging.read output");
|
|
775
|
+
if (source.provider !== "gmail") return drift("messaging.read output.provider", "must be gmail");
|
|
776
|
+
if (source.operation !== "messaging.read") {
|
|
777
|
+
return drift("messaging.read output.operation", "must be messaging.read");
|
|
778
|
+
}
|
|
779
|
+
const account = accountSubject(source.accountSubject, "messaging.read output.accountSubject");
|
|
780
|
+
const thread = record(source.thread, "messaging.read output.thread");
|
|
781
|
+
exactKeys(thread, ["id", "snippet", "historyId", "messages"], [], "messaging.read output.thread");
|
|
782
|
+
const threadId = gmailId(thread.id, "messaging.read output.thread.id");
|
|
783
|
+
if (threadId !== requestedThreadId) {
|
|
784
|
+
return drift("messaging.read output.thread.id", "must bind messaging.read input.thread_id");
|
|
785
|
+
}
|
|
786
|
+
nullableString(thread.snippet, "messaging.read output.thread.snippet", 64 * 1024, true, true);
|
|
787
|
+
historyId(thread.historyId, "messaging.read output.thread.historyId");
|
|
788
|
+
parseThreadUrl(
|
|
789
|
+
source.threadUrl,
|
|
790
|
+
"messaging.read output.threadUrl",
|
|
791
|
+
account,
|
|
792
|
+
threadId,
|
|
793
|
+
"all",
|
|
794
|
+
);
|
|
795
|
+
let aggregateBodyBytes = 0;
|
|
796
|
+
const messages = Object.freeze(array(
|
|
797
|
+
thread.messages,
|
|
798
|
+
"messaging.read output.thread.messages",
|
|
799
|
+
1_000,
|
|
800
|
+
).map((entry, index) => {
|
|
801
|
+
const path = `messaging.read output.thread.messages[${index}]`;
|
|
802
|
+
const parsed = parseMessage(entry, path, threadId);
|
|
803
|
+
if (parsed.originalBodyBytes > GMAIL_AGGREGATE_BODY_BYTES - aggregateBodyBytes) {
|
|
804
|
+
return drift(
|
|
805
|
+
`${path}.body.text`,
|
|
806
|
+
`causes the thread to exceed the ${GMAIL_AGGREGATE_BODY_BYTES}-byte Gmail aggregate body ceiling`,
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
aggregateBodyBytes += parsed.originalBodyBytes;
|
|
810
|
+
return parsed;
|
|
811
|
+
}));
|
|
812
|
+
if (messages.length === 0) {
|
|
813
|
+
return drift("messaging.read output.thread.messages", "must contain at least one message");
|
|
814
|
+
}
|
|
815
|
+
const ids = messages.map((message) => message.id);
|
|
816
|
+
if (new Set(ids).size !== ids.length) {
|
|
817
|
+
return drift("messaging.read output.thread.messages", "contains duplicate stable message IDs");
|
|
818
|
+
}
|
|
819
|
+
return Object.freeze({
|
|
820
|
+
schemaVersion: 1,
|
|
821
|
+
partition: `${account.toLowerCase()}:gmail:thread:${threadId}`,
|
|
822
|
+
completeness: Object.freeze({
|
|
823
|
+
kind: "complete",
|
|
824
|
+
reason: "Gmail returned the complete full-MIME representation of the exact requested thread.",
|
|
825
|
+
}),
|
|
826
|
+
cursor: Object.freeze({ direction: "none", request: null, nextInput: null }),
|
|
827
|
+
entities: Object.freeze(messages.map(materializedMessage)),
|
|
828
|
+
tombstones: Object.freeze([]),
|
|
829
|
+
});
|
|
830
|
+
}
|