@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,1159 @@
|
|
|
1
|
+
import { types as nodeTypes } from "node:util";
|
|
2
|
+
|
|
3
|
+
import type { OperationInput } from "../model";
|
|
4
|
+
import { OmniMaterializerDriftError } from "../omni-model";
|
|
5
|
+
import type {
|
|
6
|
+
OmniAttachmentV1,
|
|
7
|
+
OmniParticipantV1,
|
|
8
|
+
ProviderConversationV1,
|
|
9
|
+
ProviderMaterializedPageV1,
|
|
10
|
+
ProviderMessageV1,
|
|
11
|
+
} from "../omni-model";
|
|
12
|
+
import { isCanonicalBeeperConversationId } from "./beeper-local";
|
|
13
|
+
|
|
14
|
+
type JsonRecord = Readonly<Record<string, unknown>>;
|
|
15
|
+
|
|
16
|
+
function drift(path: string, message: string): never {
|
|
17
|
+
throw new OmniMaterializerDriftError("beeper", path, message);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function record(value: unknown, path: string): JsonRecord {
|
|
21
|
+
if (
|
|
22
|
+
nodeTypes.isProxy(value)
|
|
23
|
+
|| typeof value !== "object"
|
|
24
|
+
|| value === null
|
|
25
|
+
|| Array.isArray(value)
|
|
26
|
+
|| (
|
|
27
|
+
Object.getPrototypeOf(value) !== Object.prototype
|
|
28
|
+
&& Object.getPrototypeOf(value) !== null
|
|
29
|
+
)
|
|
30
|
+
) return drift(path, "must be a plain object");
|
|
31
|
+
return value as JsonRecord;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function exactKeys(
|
|
35
|
+
value: JsonRecord,
|
|
36
|
+
required: readonly string[],
|
|
37
|
+
optional: readonly string[],
|
|
38
|
+
path: string,
|
|
39
|
+
): void {
|
|
40
|
+
const allowed = new Set([...required, ...optional]);
|
|
41
|
+
for (const key of Object.keys(value)) {
|
|
42
|
+
if (!allowed.has(key)) drift(path, `contains unreviewed property ${key}`);
|
|
43
|
+
}
|
|
44
|
+
for (const key of required) {
|
|
45
|
+
if (!Object.hasOwn(value, key)) drift(`${path}.${key}`, "is required");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function array(value: unknown, path: string, maximum: number): readonly unknown[] {
|
|
50
|
+
if (
|
|
51
|
+
nodeTypes.isProxy(value)
|
|
52
|
+
|| !Array.isArray(value)
|
|
53
|
+
|| Object.getPrototypeOf(value) !== Array.prototype
|
|
54
|
+
|| value.length > maximum
|
|
55
|
+
) return drift(path, `must be an array of at most ${maximum} items`);
|
|
56
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
57
|
+
if (!Object.hasOwn(value, index)) drift(`${path}[${index}]`, "must not be sparse");
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function string(value: unknown, path: string, maximum: number, allowEmpty = false): string {
|
|
63
|
+
if (
|
|
64
|
+
typeof value !== "string"
|
|
65
|
+
|| (!allowEmpty && value.length === 0)
|
|
66
|
+
|| Buffer.byteLength(value, "utf8") > maximum
|
|
67
|
+
|| value.includes("\0")
|
|
68
|
+
) return drift(path, "must be bounded text");
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function nullableString(value: unknown, path: string, maximum: number): string | null {
|
|
73
|
+
return value === null ? null : string(value, path, maximum, true);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function boolean(value: unknown, path: string): boolean {
|
|
77
|
+
if (typeof value !== "boolean") return drift(path, "must be boolean");
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function nullableBoolean(value: unknown, path: string): boolean | null {
|
|
82
|
+
return value === null ? null : boolean(value, path);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function integer(value: unknown, path: string, minimum: number, maximum: number): number {
|
|
86
|
+
if (
|
|
87
|
+
typeof value !== "number"
|
|
88
|
+
|| !Number.isSafeInteger(value)
|
|
89
|
+
|| value < minimum
|
|
90
|
+
|| value > maximum
|
|
91
|
+
) return drift(path, `must be an integer from ${minimum} through ${maximum}`);
|
|
92
|
+
return value;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function nullableInteger(
|
|
96
|
+
value: unknown,
|
|
97
|
+
path: string,
|
|
98
|
+
minimum: number,
|
|
99
|
+
maximum: number,
|
|
100
|
+
): number | null {
|
|
101
|
+
return value === null ? null : integer(value, path, minimum, maximum);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function timestamp(value: unknown, path: string): string {
|
|
105
|
+
const source = string(value, path, 64);
|
|
106
|
+
const milliseconds = Date.parse(source);
|
|
107
|
+
if (!Number.isFinite(milliseconds)) return drift(path, "must be a timestamp");
|
|
108
|
+
return new Date(milliseconds).toISOString();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function nullableTimestamp(value: unknown, path: string): string | null {
|
|
112
|
+
return value === null ? null : timestamp(value, path);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function encoded(value: string): string {
|
|
116
|
+
return Buffer.from(value, "utf8").toString("base64url");
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function hasWellFormedUnicode(value: string): boolean {
|
|
120
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
121
|
+
const code = value.charCodeAt(index);
|
|
122
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
123
|
+
const next = value.charCodeAt(index + 1);
|
|
124
|
+
if (!(next >= 0xdc00 && next <= 0xdfff)) return false;
|
|
125
|
+
index += 1;
|
|
126
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function beeperRawIdentifier(
|
|
134
|
+
value: unknown,
|
|
135
|
+
label: string,
|
|
136
|
+
maximum: number,
|
|
137
|
+
): string {
|
|
138
|
+
if (
|
|
139
|
+
typeof value !== "string"
|
|
140
|
+
|| value.length === 0
|
|
141
|
+
|| Buffer.byteLength(value, "utf8") > maximum
|
|
142
|
+
|| !hasWellFormedUnicode(value)
|
|
143
|
+
|| /[\u0000-\u001f\u007f-\u009f]/u.test(value)
|
|
144
|
+
) throw new Error(`${label} must be one bounded Beeper identifier`);
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function beeperConversationId(value: unknown, label: string): string {
|
|
149
|
+
const id = beeperRawIdentifier(value, label, 2_048);
|
|
150
|
+
if (!isCanonicalBeeperConversationId(id)) {
|
|
151
|
+
return drift(label, "must be one exact full Beeper/Matrix chat ID");
|
|
152
|
+
}
|
|
153
|
+
return id;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function beeperProviderId(
|
|
157
|
+
accountIdValue: unknown,
|
|
158
|
+
kind: "chat" | "message",
|
|
159
|
+
rawIdValue: unknown,
|
|
160
|
+
): string {
|
|
161
|
+
const accountId = beeperRawIdentifier(accountIdValue, "Beeper account ID", 512);
|
|
162
|
+
const rawId = beeperRawIdentifier(rawIdValue, `Beeper ${kind} ID`, 2_048);
|
|
163
|
+
return `beeper:${encoded(accountId)}:${kind}:${encoded(rawId)}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function rawBeeperProviderId(
|
|
167
|
+
accountIdValue: unknown,
|
|
168
|
+
providerIdValue: unknown,
|
|
169
|
+
kind: "chat" | "message",
|
|
170
|
+
): string {
|
|
171
|
+
const accountId = beeperRawIdentifier(accountIdValue, "Beeper account ID", 512);
|
|
172
|
+
const providerId = beeperRawIdentifier(
|
|
173
|
+
providerIdValue,
|
|
174
|
+
`normalized Beeper ${kind} ID`,
|
|
175
|
+
4_096,
|
|
176
|
+
);
|
|
177
|
+
const prefix = `beeper:${encoded(accountId)}:${kind}:`;
|
|
178
|
+
if (!providerId.startsWith(prefix)) {
|
|
179
|
+
throw new Error(`normalized Beeper ${kind} ID does not belong to the exact account`);
|
|
180
|
+
}
|
|
181
|
+
const source = providerId.slice(prefix.length);
|
|
182
|
+
if (!/^[A-Za-z0-9_-]+$/u.test(source)) {
|
|
183
|
+
throw new Error(`normalized Beeper ${kind} ID has a malformed encoding`);
|
|
184
|
+
}
|
|
185
|
+
let raw: string;
|
|
186
|
+
try {
|
|
187
|
+
raw = new TextDecoder("utf-8", { fatal: true }).decode(
|
|
188
|
+
Buffer.from(source, "base64url"),
|
|
189
|
+
);
|
|
190
|
+
} catch {
|
|
191
|
+
throw new Error(`normalized Beeper ${kind} ID is not canonical UTF-8`);
|
|
192
|
+
}
|
|
193
|
+
if (encoded(raw) !== source) {
|
|
194
|
+
throw new Error(`normalized Beeper ${kind} ID is not canonically encoded`);
|
|
195
|
+
}
|
|
196
|
+
return beeperRawIdentifier(raw, `Beeper ${kind} ID`, 2_048);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function normalizeBeeperConversationProviderId(
|
|
200
|
+
accountId: unknown,
|
|
201
|
+
conversationId: unknown,
|
|
202
|
+
): string {
|
|
203
|
+
return beeperProviderId(
|
|
204
|
+
accountId,
|
|
205
|
+
"chat",
|
|
206
|
+
beeperConversationId(conversationId, "Beeper conversation ID"),
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function rawBeeperConversationId(
|
|
211
|
+
accountId: unknown,
|
|
212
|
+
providerId: unknown,
|
|
213
|
+
): string {
|
|
214
|
+
return beeperConversationId(
|
|
215
|
+
rawBeeperProviderId(accountId, providerId, "chat"),
|
|
216
|
+
"Beeper conversation ID",
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function normalizeBeeperMessageProviderId(
|
|
221
|
+
accountId: unknown,
|
|
222
|
+
messageId: unknown,
|
|
223
|
+
): string {
|
|
224
|
+
return beeperProviderId(accountId, "message", messageId);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function rawBeeperMessageId(
|
|
228
|
+
accountId: unknown,
|
|
229
|
+
providerId: unknown,
|
|
230
|
+
): string {
|
|
231
|
+
return rawBeeperProviderId(accountId, providerId, "message");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function userProviderId(accountId: string, userId: string): string {
|
|
235
|
+
return `beeper:${encoded(accountId)}:user:${encoded(userId)}`;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function subject(value: unknown, path: string): string {
|
|
239
|
+
const source = string(value, path, 512);
|
|
240
|
+
if (!/^beeper:local:[a-f0-9]{64}$/u.test(source)) {
|
|
241
|
+
return drift(path, "must be a hashed bound Beeper local subject");
|
|
242
|
+
}
|
|
243
|
+
return source;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function listInput(input: OperationInput): Readonly<{ accountId: string | null; limit: number }> {
|
|
247
|
+
const source = record(input, "messaging.list input");
|
|
248
|
+
exactKeys(source, [], ["account_id", "limit"], "messaging.list input");
|
|
249
|
+
return Object.freeze({
|
|
250
|
+
accountId: source.account_id === undefined
|
|
251
|
+
? null
|
|
252
|
+
: string(source.account_id, "messaging.list input.account_id", 512),
|
|
253
|
+
limit: source.limit === undefined
|
|
254
|
+
? 200
|
|
255
|
+
: integer(source.limit, "messaging.list input.limit", 1, 200),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function readInputV2(input: OperationInput): Readonly<{
|
|
260
|
+
accountId: string;
|
|
261
|
+
conversationId: string;
|
|
262
|
+
beforeCursor: string | null;
|
|
263
|
+
afterCursor: string | null;
|
|
264
|
+
limit: number;
|
|
265
|
+
}> {
|
|
266
|
+
const source = record(input, "messaging.read input");
|
|
267
|
+
exactKeys(
|
|
268
|
+
source,
|
|
269
|
+
["account_id", "conversation_id"],
|
|
270
|
+
["before_cursor", "after_cursor", "limit"],
|
|
271
|
+
"messaging.read input",
|
|
272
|
+
);
|
|
273
|
+
const beforeCursor = source.before_cursor === undefined
|
|
274
|
+
? null
|
|
275
|
+
: string(source.before_cursor, "messaging.read input.before_cursor", 2_048);
|
|
276
|
+
const afterCursor = source.after_cursor === undefined
|
|
277
|
+
? null
|
|
278
|
+
: string(source.after_cursor, "messaging.read input.after_cursor", 2_048);
|
|
279
|
+
if (beforeCursor !== null && afterCursor !== null) {
|
|
280
|
+
drift("messaging.read input", "accepts only one cursor direction");
|
|
281
|
+
}
|
|
282
|
+
return Object.freeze({
|
|
283
|
+
accountId: string(source.account_id, "messaging.read input.account_id", 512),
|
|
284
|
+
conversationId: beeperConversationId(
|
|
285
|
+
source.conversation_id,
|
|
286
|
+
"messaging.read input.conversation_id",
|
|
287
|
+
),
|
|
288
|
+
beforeCursor,
|
|
289
|
+
afterCursor,
|
|
290
|
+
limit: source.limit === undefined
|
|
291
|
+
? 200
|
|
292
|
+
: integer(source.limit, "messaging.read input.limit", 1, 200),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function readInput(input: OperationInput): Readonly<{
|
|
297
|
+
accountId: string;
|
|
298
|
+
conversationId: string;
|
|
299
|
+
beforeCursor: string | null;
|
|
300
|
+
afterCursor: string | null;
|
|
301
|
+
sender: string | null;
|
|
302
|
+
limit: number;
|
|
303
|
+
}> {
|
|
304
|
+
const source = record(input, "messaging.read input");
|
|
305
|
+
exactKeys(
|
|
306
|
+
source,
|
|
307
|
+
["account_id", "conversation_id"],
|
|
308
|
+
["before_cursor", "after_cursor", "sender", "limit"],
|
|
309
|
+
"messaging.read input",
|
|
310
|
+
);
|
|
311
|
+
const beforeCursor = source.before_cursor === undefined
|
|
312
|
+
? null
|
|
313
|
+
: beeperRawIdentifier(
|
|
314
|
+
source.before_cursor,
|
|
315
|
+
"messaging.read input.before_cursor",
|
|
316
|
+
2_048,
|
|
317
|
+
);
|
|
318
|
+
const afterCursor = source.after_cursor === undefined
|
|
319
|
+
? null
|
|
320
|
+
: beeperRawIdentifier(
|
|
321
|
+
source.after_cursor,
|
|
322
|
+
"messaging.read input.after_cursor",
|
|
323
|
+
2_048,
|
|
324
|
+
);
|
|
325
|
+
if (beforeCursor !== null && afterCursor !== null) {
|
|
326
|
+
drift("messaging.read input", "accepts only one cursor direction");
|
|
327
|
+
}
|
|
328
|
+
const sender = source.sender === undefined
|
|
329
|
+
? null
|
|
330
|
+
: beeperRawIdentifier(source.sender, "messaging.read input.sender", 2_048);
|
|
331
|
+
if (
|
|
332
|
+
sender !== null
|
|
333
|
+
&& sender !== "me"
|
|
334
|
+
&& sender !== "others"
|
|
335
|
+
&& sender.startsWith("-")
|
|
336
|
+
) drift("messaging.read input.sender", "must be me, others, or one bounded opaque non-flag user ID");
|
|
337
|
+
return Object.freeze({
|
|
338
|
+
accountId: string(source.account_id, "messaging.read input.account_id", 512),
|
|
339
|
+
conversationId: beeperConversationId(
|
|
340
|
+
source.conversation_id,
|
|
341
|
+
"messaging.read input.conversation_id",
|
|
342
|
+
),
|
|
343
|
+
beforeCursor,
|
|
344
|
+
afterCursor,
|
|
345
|
+
sender,
|
|
346
|
+
limit: source.limit === undefined
|
|
347
|
+
? 200
|
|
348
|
+
: integer(source.limit, "messaging.read input.limit", 1, 200),
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function exactConversationInput(input: OperationInput): Readonly<{
|
|
353
|
+
accountId: string;
|
|
354
|
+
conversationId: string;
|
|
355
|
+
}> {
|
|
356
|
+
const source = record(input, "conversations.read input");
|
|
357
|
+
exactKeys(
|
|
358
|
+
source,
|
|
359
|
+
["account_id", "conversation_id"],
|
|
360
|
+
["max_participants"],
|
|
361
|
+
"conversations.read input",
|
|
362
|
+
);
|
|
363
|
+
if (source.max_participants !== undefined) {
|
|
364
|
+
integer(
|
|
365
|
+
source.max_participants,
|
|
366
|
+
"conversations.read input.max_participants",
|
|
367
|
+
1,
|
|
368
|
+
500,
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
return Object.freeze({
|
|
372
|
+
accountId: string(
|
|
373
|
+
source.account_id,
|
|
374
|
+
"conversations.read input.account_id",
|
|
375
|
+
512,
|
|
376
|
+
),
|
|
377
|
+
conversationId: beeperConversationId(
|
|
378
|
+
source.conversation_id,
|
|
379
|
+
"conversations.read input.conversation_id",
|
|
380
|
+
),
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function participant(value: unknown, path: string, accountId: string): OmniParticipantV1 {
|
|
385
|
+
const source = record(value, path);
|
|
386
|
+
exactKeys(source, [
|
|
387
|
+
"id",
|
|
388
|
+
"fullName",
|
|
389
|
+
"username",
|
|
390
|
+
"phoneNumber",
|
|
391
|
+
"email",
|
|
392
|
+
"isSelf",
|
|
393
|
+
"cannotMessage",
|
|
394
|
+
"isAdmin",
|
|
395
|
+
"isNetworkBot",
|
|
396
|
+
"isPending",
|
|
397
|
+
], [], path);
|
|
398
|
+
const id = string(source.id, `${path}.id`, 2_048);
|
|
399
|
+
const displayName = nullableString(source.fullName, `${path}.fullName`, 2_048);
|
|
400
|
+
const username = nullableString(source.username, `${path}.username`, 2_048);
|
|
401
|
+
nullableString(source.phoneNumber, `${path}.phoneNumber`, 128);
|
|
402
|
+
nullableString(source.email, `${path}.email`, 2_048);
|
|
403
|
+
nullableBoolean(source.isSelf, `${path}.isSelf`);
|
|
404
|
+
nullableBoolean(source.cannotMessage, `${path}.cannotMessage`);
|
|
405
|
+
nullableBoolean(source.isAdmin, `${path}.isAdmin`);
|
|
406
|
+
nullableBoolean(source.isNetworkBot, `${path}.isNetworkBot`);
|
|
407
|
+
nullableBoolean(source.isPending, `${path}.isPending`);
|
|
408
|
+
return Object.freeze({
|
|
409
|
+
providerId: userProviderId(accountId, id),
|
|
410
|
+
displayName,
|
|
411
|
+
handle: username,
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function conversation(value: unknown, path: string): ProviderConversationV1 {
|
|
416
|
+
const source = record(value, path);
|
|
417
|
+
exactKeys(source, [
|
|
418
|
+
"id",
|
|
419
|
+
"localChatId",
|
|
420
|
+
"accountId",
|
|
421
|
+
"network",
|
|
422
|
+
"title",
|
|
423
|
+
"type",
|
|
424
|
+
"description",
|
|
425
|
+
"descriptionObserved",
|
|
426
|
+
"hasAvatar",
|
|
427
|
+
"avatarObserved",
|
|
428
|
+
"lastReadMessageSortKey",
|
|
429
|
+
"lastActivity",
|
|
430
|
+
"unreadCount",
|
|
431
|
+
"unreadMentionsCount",
|
|
432
|
+
"isMarkedUnread",
|
|
433
|
+
"isArchived",
|
|
434
|
+
"isLowPriority",
|
|
435
|
+
"isMuted",
|
|
436
|
+
"isPinned",
|
|
437
|
+
"isReadOnly",
|
|
438
|
+
"messageExpirySeconds",
|
|
439
|
+
"messageExpiryObserved",
|
|
440
|
+
"draft",
|
|
441
|
+
"draftObserved",
|
|
442
|
+
"reminder",
|
|
443
|
+
"reminderObserved",
|
|
444
|
+
"participants",
|
|
445
|
+
], [], path);
|
|
446
|
+
const accountId = string(source.accountId, `${path}.accountId`, 512);
|
|
447
|
+
const id = string(source.id, `${path}.id`, 2_048);
|
|
448
|
+
nullableString(source.localChatId, `${path}.localChatId`, 2_048);
|
|
449
|
+
string(source.network, `${path}.network`, 512);
|
|
450
|
+
const title = string(source.title, `${path}.title`, 4_096, true);
|
|
451
|
+
const type = string(source.type, `${path}.type`, 32);
|
|
452
|
+
if (type !== "single" && type !== "group") drift(`${path}.type`, "must be single or group");
|
|
453
|
+
const description = nullableString(source.description, `${path}.description`, 65_536);
|
|
454
|
+
boolean(source.descriptionObserved, `${path}.descriptionObserved`);
|
|
455
|
+
boolean(source.hasAvatar, `${path}.hasAvatar`);
|
|
456
|
+
boolean(source.avatarObserved, `${path}.avatarObserved`);
|
|
457
|
+
nullableString(source.lastReadMessageSortKey, `${path}.lastReadMessageSortKey`, 2_048);
|
|
458
|
+
const orderedAt = nullableTimestamp(source.lastActivity, `${path}.lastActivity`);
|
|
459
|
+
const unreadCount = integer(source.unreadCount, `${path}.unreadCount`, 0, 100_000_000);
|
|
460
|
+
nullableInteger(source.unreadMentionsCount, `${path}.unreadMentionsCount`, 0, 100_000_000);
|
|
461
|
+
const markedUnread = nullableBoolean(source.isMarkedUnread, `${path}.isMarkedUnread`);
|
|
462
|
+
const archived = nullableBoolean(source.isArchived, `${path}.isArchived`);
|
|
463
|
+
nullableBoolean(source.isLowPriority, `${path}.isLowPriority`);
|
|
464
|
+
nullableBoolean(source.isMuted, `${path}.isMuted`);
|
|
465
|
+
nullableBoolean(source.isPinned, `${path}.isPinned`);
|
|
466
|
+
nullableBoolean(source.isReadOnly, `${path}.isReadOnly`);
|
|
467
|
+
nullableInteger(source.messageExpirySeconds, `${path}.messageExpirySeconds`, 0, Number.MAX_SAFE_INTEGER);
|
|
468
|
+
boolean(source.messageExpiryObserved, `${path}.messageExpiryObserved`);
|
|
469
|
+
if (source.draft !== null) {
|
|
470
|
+
const draft = record(source.draft, `${path}.draft`);
|
|
471
|
+
exactKeys(draft, ["text", "attachments"], [], `${path}.draft`);
|
|
472
|
+
string(draft.text, `${path}.draft.text`, 65_536, true);
|
|
473
|
+
array(draft.attachments, `${path}.draft.attachments`, 32).forEach((item, index) => {
|
|
474
|
+
const attachment = record(item, `${path}.draft.attachments[${index}]`);
|
|
475
|
+
exactKeys(attachment, ["type", "fileName", "fileSizeBytes", "mimeType"], [], `${path}.draft.attachments[${index}]`);
|
|
476
|
+
const type = string(attachment.type, `${path}.draft.attachments[${index}].type`, 32);
|
|
477
|
+
if (type !== "file" && type !== "gif" && type !== "recorded_audio") {
|
|
478
|
+
drift(`${path}.draft.attachments[${index}].type`, "is unsupported");
|
|
479
|
+
}
|
|
480
|
+
nullableString(attachment.fileName, `${path}.draft.attachments[${index}].fileName`, 4_096);
|
|
481
|
+
nullableInteger(attachment.fileSizeBytes, `${path}.draft.attachments[${index}].fileSizeBytes`, 0, Number.MAX_SAFE_INTEGER);
|
|
482
|
+
nullableString(attachment.mimeType, `${path}.draft.attachments[${index}].mimeType`, 512);
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
boolean(source.draftObserved, `${path}.draftObserved`);
|
|
486
|
+
if (source.reminder !== null) {
|
|
487
|
+
const reminder = record(source.reminder, `${path}.reminder`);
|
|
488
|
+
exactKeys(reminder, ["when", "dismissOnMessage"], [], `${path}.reminder`);
|
|
489
|
+
nullableString(reminder.when, `${path}.reminder.when`, 512);
|
|
490
|
+
nullableBoolean(reminder.dismissOnMessage, `${path}.reminder.dismissOnMessage`);
|
|
491
|
+
}
|
|
492
|
+
boolean(source.reminderObserved, `${path}.reminderObserved`);
|
|
493
|
+
const participants = record(source.participants, `${path}.participants`);
|
|
494
|
+
exactKeys(participants, ["items", "total", "hasMore"], [], `${path}.participants`);
|
|
495
|
+
const items = array(participants.items, `${path}.participants.items`, 500)
|
|
496
|
+
.map((item, index) => participant(item, `${path}.participants.items[${index}]`, accountId));
|
|
497
|
+
integer(participants.total, `${path}.participants.total`, 0, 100_000_000);
|
|
498
|
+
boolean(participants.hasMore, `${path}.participants.hasMore`);
|
|
499
|
+
return Object.freeze({
|
|
500
|
+
kind: "conversation",
|
|
501
|
+
conversationKind: type,
|
|
502
|
+
providerId: normalizeBeeperConversationProviderId(accountId, id),
|
|
503
|
+
providerRevision: null,
|
|
504
|
+
orderedAt,
|
|
505
|
+
detail: "summary",
|
|
506
|
+
title,
|
|
507
|
+
summary: description,
|
|
508
|
+
participants: Object.freeze(items),
|
|
509
|
+
unread: markedUnread ?? unreadCount > 0,
|
|
510
|
+
unreadCount,
|
|
511
|
+
archived,
|
|
512
|
+
pending: null,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function attachment(value: unknown, path: string): OmniAttachmentV1 {
|
|
517
|
+
const source = record(value, path);
|
|
518
|
+
exactKeys(source, [
|
|
519
|
+
"type",
|
|
520
|
+
"durationSeconds",
|
|
521
|
+
"fileName",
|
|
522
|
+
"fileSizeBytes",
|
|
523
|
+
"mimeType",
|
|
524
|
+
"width",
|
|
525
|
+
"height",
|
|
526
|
+
"isGif",
|
|
527
|
+
"isSticker",
|
|
528
|
+
"isVoiceNote",
|
|
529
|
+
"transcription",
|
|
530
|
+
], [], path);
|
|
531
|
+
const type = string(source.type, `${path}.type`, 32);
|
|
532
|
+
nullableInteger(source.width, `${path}.width`, 0, 1_000_000);
|
|
533
|
+
nullableInteger(source.height, `${path}.height`, 0, 1_000_000);
|
|
534
|
+
nullableBoolean(source.isGif, `${path}.isGif`);
|
|
535
|
+
const isSticker = nullableBoolean(source.isSticker, `${path}.isSticker`) === true;
|
|
536
|
+
nullableBoolean(source.isVoiceNote, `${path}.isVoiceNote`);
|
|
537
|
+
if (source.durationSeconds !== null) {
|
|
538
|
+
if (typeof source.durationSeconds !== "number" || !Number.isFinite(source.durationSeconds)) {
|
|
539
|
+
drift(`${path}.durationSeconds`, "must be null or a bounded number");
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
if (source.transcription !== null) record(source.transcription, `${path}.transcription`);
|
|
543
|
+
const kind: OmniAttachmentV1["kind"] = isSticker
|
|
544
|
+
? "sticker"
|
|
545
|
+
: type === "img"
|
|
546
|
+
? "image"
|
|
547
|
+
: type === "video"
|
|
548
|
+
? "video"
|
|
549
|
+
: type === "audio"
|
|
550
|
+
? "audio"
|
|
551
|
+
: "unknown";
|
|
552
|
+
return Object.freeze({
|
|
553
|
+
kind,
|
|
554
|
+
mimeType: nullableString(source.mimeType, `${path}.mimeType`, 512),
|
|
555
|
+
name: nullableString(source.fileName, `${path}.fileName`, 4_096),
|
|
556
|
+
sizeBytes: nullableInteger(
|
|
557
|
+
source.fileSizeBytes,
|
|
558
|
+
`${path}.fileSizeBytes`,
|
|
559
|
+
0,
|
|
560
|
+
Number.MAX_SAFE_INTEGER,
|
|
561
|
+
),
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function message(
|
|
566
|
+
value: unknown,
|
|
567
|
+
path: string,
|
|
568
|
+
expectedAccountId: string,
|
|
569
|
+
expectedConversationId: string,
|
|
570
|
+
): ProviderMessageV1 {
|
|
571
|
+
const source = record(value, path);
|
|
572
|
+
exactKeys(source, [
|
|
573
|
+
"id",
|
|
574
|
+
"accountId",
|
|
575
|
+
"conversationId",
|
|
576
|
+
"senderId",
|
|
577
|
+
"senderName",
|
|
578
|
+
"isSender",
|
|
579
|
+
"sortKey",
|
|
580
|
+
"timestamp",
|
|
581
|
+
"editedTimestamp",
|
|
582
|
+
"text",
|
|
583
|
+
"type",
|
|
584
|
+
"linkedMessageId",
|
|
585
|
+
"mentions",
|
|
586
|
+
"isDeleted",
|
|
587
|
+
"isHidden",
|
|
588
|
+
"isUnread",
|
|
589
|
+
"seen",
|
|
590
|
+
"attachments",
|
|
591
|
+
"reactions",
|
|
592
|
+
], [], path);
|
|
593
|
+
const accountId = string(source.accountId, `${path}.accountId`, 512);
|
|
594
|
+
const conversationId = string(source.conversationId, `${path}.conversationId`, 2_048);
|
|
595
|
+
if (accountId !== expectedAccountId || conversationId !== expectedConversationId) {
|
|
596
|
+
drift(path, "must bind the requested account and conversation");
|
|
597
|
+
}
|
|
598
|
+
const id = string(source.id, `${path}.id`, 2_048);
|
|
599
|
+
const senderId = string(source.senderId, `${path}.senderId`, 2_048);
|
|
600
|
+
const senderName = nullableString(source.senderName, `${path}.senderName`, 2_048);
|
|
601
|
+
const isSender = nullableBoolean(source.isSender, `${path}.isSender`);
|
|
602
|
+
string(source.sortKey, `${path}.sortKey`, 2_048);
|
|
603
|
+
const orderedAt = timestamp(source.timestamp, `${path}.timestamp`);
|
|
604
|
+
const editedTimestamp = nullableTimestamp(source.editedTimestamp, `${path}.editedTimestamp`);
|
|
605
|
+
const body = nullableString(source.text, `${path}.text`, 1_048_576);
|
|
606
|
+
nullableString(source.type, `${path}.type`, 128);
|
|
607
|
+
const replyId = nullableString(source.linkedMessageId, `${path}.linkedMessageId`, 2_048);
|
|
608
|
+
if (source.mentions !== null) array(source.mentions, `${path}.mentions`, 2_000);
|
|
609
|
+
const isDeleted = boolean(source.isDeleted, `${path}.isDeleted`);
|
|
610
|
+
const isHidden = boolean(source.isHidden, `${path}.isHidden`);
|
|
611
|
+
const unread = nullableBoolean(source.isUnread, `${path}.isUnread`);
|
|
612
|
+
if (source.seen !== null && typeof source.seen === "object") record(source.seen, `${path}.seen`);
|
|
613
|
+
const attachments = array(source.attachments, `${path}.attachments`, 1_000)
|
|
614
|
+
.map((item, index) => attachment(item, `${path}.attachments[${index}]`));
|
|
615
|
+
array(source.reactions, `${path}.reactions`, 10_000).forEach((item, index) =>
|
|
616
|
+
record(item, `${path}.reactions[${index}]`));
|
|
617
|
+
const state: ProviderMessageV1["state"] = isDeleted && isHidden
|
|
618
|
+
? "revoked-and-deleted-for-me"
|
|
619
|
+
: isDeleted
|
|
620
|
+
? "revoked"
|
|
621
|
+
: isHidden
|
|
622
|
+
? "deleted-for-me"
|
|
623
|
+
: "active";
|
|
624
|
+
return Object.freeze({
|
|
625
|
+
kind: "message",
|
|
626
|
+
providerId: normalizeBeeperMessageProviderId(accountId, id),
|
|
627
|
+
providerRevision: editedTimestamp,
|
|
628
|
+
orderedAt,
|
|
629
|
+
conversationProviderId: normalizeBeeperConversationProviderId(accountId, conversationId),
|
|
630
|
+
sender: Object.freeze({
|
|
631
|
+
providerId: userProviderId(accountId, senderId),
|
|
632
|
+
displayName: senderName,
|
|
633
|
+
handle: null,
|
|
634
|
+
}),
|
|
635
|
+
recipients: Object.freeze([]),
|
|
636
|
+
direction: isSender === null ? "unknown" : isSender ? "outgoing" : "incoming",
|
|
637
|
+
subject: null,
|
|
638
|
+
body,
|
|
639
|
+
unread,
|
|
640
|
+
replyToProviderId: replyId === null
|
|
641
|
+
? null
|
|
642
|
+
: normalizeBeeperMessageProviderId(accountId, replyId),
|
|
643
|
+
state,
|
|
644
|
+
attachments: Object.freeze(attachments),
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function validateEnvelope(
|
|
649
|
+
output: unknown,
|
|
650
|
+
operation: "messaging.list" | "messaging.read",
|
|
651
|
+
projection: "bounded-local-desktop-api" | "bounded-local-desktop-direct-iterator" =
|
|
652
|
+
"bounded-local-desktop-api",
|
|
653
|
+
): { readonly source: JsonRecord; readonly subject: string } {
|
|
654
|
+
const source = record(output, `${operation} output`);
|
|
655
|
+
if (source.provider !== "beeper" || source.operation !== operation) {
|
|
656
|
+
drift(`${operation} output`, "must identify the exact Beeper operation");
|
|
657
|
+
}
|
|
658
|
+
if (source.projection !== projection) {
|
|
659
|
+
drift(`${operation} output.projection`, `must be ${projection}`);
|
|
660
|
+
}
|
|
661
|
+
return Object.freeze({
|
|
662
|
+
source,
|
|
663
|
+
subject: subject(source.accountSubject, `${operation} output.accountSubject`),
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
export function materializeBeeperMessagingList(
|
|
668
|
+
input: OperationInput,
|
|
669
|
+
output: unknown,
|
|
670
|
+
): ProviderMaterializedPageV1 {
|
|
671
|
+
const parsed = listInput(input);
|
|
672
|
+
const { source, subject: accountSubject } = validateEnvelope(output, "messaging.list");
|
|
673
|
+
exactKeys(source, [
|
|
674
|
+
"provider",
|
|
675
|
+
"operation",
|
|
676
|
+
"accountSubject",
|
|
677
|
+
"projection",
|
|
678
|
+
"accounts",
|
|
679
|
+
"requestedAccountId",
|
|
680
|
+
"conversations",
|
|
681
|
+
"completeness",
|
|
682
|
+
], [], "messaging.list output");
|
|
683
|
+
array(source.accounts, "messaging.list output.accounts", 128);
|
|
684
|
+
if (source.requestedAccountId !== parsed.accountId) {
|
|
685
|
+
drift("messaging.list output.requestedAccountId", "must bind input.account_id");
|
|
686
|
+
}
|
|
687
|
+
const entities = array(
|
|
688
|
+
source.conversations,
|
|
689
|
+
"messaging.list output.conversations",
|
|
690
|
+
parsed.limit,
|
|
691
|
+
).map((item, index) => conversation(item, `messaging.list output.conversations[${index}]`));
|
|
692
|
+
const ids = entities.map((entity) => entity.providerId);
|
|
693
|
+
if (new Set(ids).size !== ids.length) {
|
|
694
|
+
drift("messaging.list output.conversations", "contains duplicate account-scoped IDs");
|
|
695
|
+
}
|
|
696
|
+
record(source.completeness, "messaging.list output.completeness");
|
|
697
|
+
return Object.freeze({
|
|
698
|
+
schemaVersion: 1,
|
|
699
|
+
partition: `${accountSubject}:conversations:${parsed.accountId ?? "all"}`,
|
|
700
|
+
completeness: Object.freeze({
|
|
701
|
+
kind: "bounded-local",
|
|
702
|
+
reason: "Beeper exposed a bounded local Desktop projection; CLI v0.6.2 does not expose chat-list continuation metadata.",
|
|
703
|
+
}),
|
|
704
|
+
cursor: Object.freeze({ direction: "none", request: null, nextInput: null }),
|
|
705
|
+
entities: Object.freeze(entities),
|
|
706
|
+
tombstones: Object.freeze([]),
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/** Strict projection for the provider-native exact-chat lookup used by route issuance. */
|
|
711
|
+
export function materializeBeeperExactConversation(
|
|
712
|
+
input: OperationInput,
|
|
713
|
+
output: unknown,
|
|
714
|
+
): ProviderConversationV1 {
|
|
715
|
+
const parsed = exactConversationInput(input);
|
|
716
|
+
const source = record(output, "conversations.read output");
|
|
717
|
+
exactKeys(source, [
|
|
718
|
+
"provider",
|
|
719
|
+
"operation",
|
|
720
|
+
"accountSubject",
|
|
721
|
+
"conversation",
|
|
722
|
+
], [], "conversations.read output");
|
|
723
|
+
if (source.provider !== "beeper" || source.operation !== "conversations.read") {
|
|
724
|
+
drift(
|
|
725
|
+
"conversations.read output",
|
|
726
|
+
"must identify the exact Beeper conversation operation",
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
subject(source.accountSubject, "conversations.read output.accountSubject");
|
|
730
|
+
const rawConversation = record(
|
|
731
|
+
source.conversation,
|
|
732
|
+
"conversations.read output.conversation",
|
|
733
|
+
);
|
|
734
|
+
if (
|
|
735
|
+
rawConversation.accountId !== parsed.accountId
|
|
736
|
+
|| rawConversation.id !== parsed.conversationId
|
|
737
|
+
) {
|
|
738
|
+
drift(
|
|
739
|
+
"conversations.read output.conversation",
|
|
740
|
+
"must bind the exact requested account and conversation",
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
return conversation(
|
|
744
|
+
source.conversation,
|
|
745
|
+
"conversations.read output.conversation",
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
function materializeBeeperMessagingReadLegacy(
|
|
750
|
+
input: OperationInput,
|
|
751
|
+
output: unknown,
|
|
752
|
+
cursorKind: "cli-message-id" | "provider-opaque",
|
|
753
|
+
): ProviderMaterializedPageV1 {
|
|
754
|
+
const parsed = readInputV2(input);
|
|
755
|
+
const { source, subject: accountSubject } = validateEnvelope(output, "messaging.read");
|
|
756
|
+
exactKeys(source, [
|
|
757
|
+
"provider",
|
|
758
|
+
"operation",
|
|
759
|
+
"accountSubject",
|
|
760
|
+
"projection",
|
|
761
|
+
"accountId",
|
|
762
|
+
"conversationId",
|
|
763
|
+
"requestCursor",
|
|
764
|
+
"requestDirection",
|
|
765
|
+
"messages",
|
|
766
|
+
"tombstones",
|
|
767
|
+
"continuation",
|
|
768
|
+
"completeness",
|
|
769
|
+
], [], "messaging.read output");
|
|
770
|
+
if (source.accountId !== parsed.accountId || source.conversationId !== parsed.conversationId) {
|
|
771
|
+
drift("messaging.read output", "must bind input account and conversation");
|
|
772
|
+
}
|
|
773
|
+
const requestCursor = parsed.beforeCursor ?? parsed.afterCursor;
|
|
774
|
+
if (source.requestCursor !== requestCursor) {
|
|
775
|
+
drift("messaging.read output.requestCursor", "must bind the requested cursor");
|
|
776
|
+
}
|
|
777
|
+
const requestDirection = parsed.afterCursor === null ? "before" : "after";
|
|
778
|
+
if (source.requestDirection !== requestDirection) {
|
|
779
|
+
drift("messaging.read output.requestDirection", "must bind the requested direction");
|
|
780
|
+
}
|
|
781
|
+
const rawMessages = array(
|
|
782
|
+
source.messages,
|
|
783
|
+
"messaging.read output.messages",
|
|
784
|
+
parsed.limit,
|
|
785
|
+
);
|
|
786
|
+
const entities = rawMessages.map((item, index) => message(
|
|
787
|
+
item,
|
|
788
|
+
`messaging.read output.messages[${index}]`,
|
|
789
|
+
parsed.accountId,
|
|
790
|
+
parsed.conversationId,
|
|
791
|
+
));
|
|
792
|
+
const ids = entities.map((entity) => entity.providerId);
|
|
793
|
+
if (new Set(ids).size !== ids.length) {
|
|
794
|
+
drift("messaging.read output.messages", "contains duplicate stable message IDs");
|
|
795
|
+
}
|
|
796
|
+
array(source.tombstones, "messaging.read output.tombstones", parsed.limit);
|
|
797
|
+
const completeness = record(source.completeness, "messaging.read output.completeness");
|
|
798
|
+
if (cursorKind === "cli-message-id") {
|
|
799
|
+
exactKeys(completeness, [
|
|
800
|
+
"localPageComplete",
|
|
801
|
+
"remoteConversationHistoryComplete",
|
|
802
|
+
"limitReached",
|
|
803
|
+
"warnings",
|
|
804
|
+
], [], "messaging.read output.completeness");
|
|
805
|
+
const limitReached = boolean(
|
|
806
|
+
completeness.limitReached,
|
|
807
|
+
"messaging.read output.completeness.limitReached",
|
|
808
|
+
);
|
|
809
|
+
if (limitReached !== (entities.length === parsed.limit)) {
|
|
810
|
+
drift(
|
|
811
|
+
"messaging.read output.completeness.limitReached",
|
|
812
|
+
"must match the returned page bound",
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
if (
|
|
816
|
+
boolean(
|
|
817
|
+
completeness.localPageComplete,
|
|
818
|
+
"messaging.read output.completeness.localPageComplete",
|
|
819
|
+
) !== !limitReached
|
|
820
|
+
) drift(
|
|
821
|
+
"messaging.read output.completeness.localPageComplete",
|
|
822
|
+
"must be coherent with limitReached",
|
|
823
|
+
);
|
|
824
|
+
if (boolean(
|
|
825
|
+
completeness.remoteConversationHistoryComplete,
|
|
826
|
+
"messaging.read output.completeness.remoteConversationHistoryComplete",
|
|
827
|
+
)) drift(
|
|
828
|
+
"messaging.read output.completeness.remoteConversationHistoryComplete",
|
|
829
|
+
"must not claim remote-history completeness",
|
|
830
|
+
);
|
|
831
|
+
const warnings = array(
|
|
832
|
+
completeness.warnings,
|
|
833
|
+
"messaging.read output.completeness.warnings",
|
|
834
|
+
32,
|
|
835
|
+
).map((item, index) => string(
|
|
836
|
+
item,
|
|
837
|
+
`messaging.read output.completeness.warnings[${index}]`,
|
|
838
|
+
256,
|
|
839
|
+
));
|
|
840
|
+
const continuationExpected = limitReached && requestDirection === "before";
|
|
841
|
+
if ((source.continuation !== null) !== continuationExpected) {
|
|
842
|
+
drift(
|
|
843
|
+
"messaging.read output.continuation",
|
|
844
|
+
"must exist only for a full newest-first before page",
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
if (
|
|
848
|
+
limitReached
|
|
849
|
+
&& requestDirection === "after"
|
|
850
|
+
&& !warnings.includes("beeper-cli-v0.6.2-after-window-has-no-replayable-continuation")
|
|
851
|
+
) drift(
|
|
852
|
+
"messaging.read output.completeness.warnings",
|
|
853
|
+
"must disclose that after pages have no replayable continuation",
|
|
854
|
+
);
|
|
855
|
+
const normalizedRequestCursor = requestCursor === null
|
|
856
|
+
? null
|
|
857
|
+
: normalizeBeeperMessageProviderId(parsed.accountId, requestCursor);
|
|
858
|
+
if (normalizedRequestCursor !== null && ids.includes(normalizedRequestCursor)) {
|
|
859
|
+
drift("messaging.read output.messages", "must exclude the prior request cursor");
|
|
860
|
+
}
|
|
861
|
+
if (continuationExpected) {
|
|
862
|
+
for (let index = 1; index < entities.length; index += 1) {
|
|
863
|
+
const previous = record(
|
|
864
|
+
rawMessages[index - 1],
|
|
865
|
+
`messaging.read output.messages[${index - 1}]`,
|
|
866
|
+
);
|
|
867
|
+
const current = record(
|
|
868
|
+
rawMessages[index],
|
|
869
|
+
`messaging.read output.messages[${index}]`,
|
|
870
|
+
);
|
|
871
|
+
if (
|
|
872
|
+
string(
|
|
873
|
+
previous.sortKey,
|
|
874
|
+
`messaging.read output.messages[${index - 1}].sortKey`,
|
|
875
|
+
1_024,
|
|
876
|
+
) < string(
|
|
877
|
+
current.sortKey,
|
|
878
|
+
`messaging.read output.messages[${index}].sortKey`,
|
|
879
|
+
1_024,
|
|
880
|
+
)
|
|
881
|
+
) drift(
|
|
882
|
+
"messaging.read output.messages",
|
|
883
|
+
"must preserve pinned newest-first order",
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
let nextInput: Readonly<Record<string, string | number>> | null = null;
|
|
889
|
+
if (source.continuation !== null) {
|
|
890
|
+
const continuation = record(source.continuation, "messaging.read output.continuation");
|
|
891
|
+
exactKeys(
|
|
892
|
+
continuation,
|
|
893
|
+
["direction", "cursor"],
|
|
894
|
+
[],
|
|
895
|
+
"messaging.read output.continuation",
|
|
896
|
+
);
|
|
897
|
+
if (continuation.direction !== requestDirection) {
|
|
898
|
+
drift("messaging.read output.continuation.direction", "must preserve direction");
|
|
899
|
+
}
|
|
900
|
+
const cursor = string(
|
|
901
|
+
continuation.cursor,
|
|
902
|
+
"messaging.read output.continuation.cursor",
|
|
903
|
+
2_048,
|
|
904
|
+
);
|
|
905
|
+
if (cursor === requestCursor) {
|
|
906
|
+
drift("messaging.read output.continuation.cursor", "must advance");
|
|
907
|
+
}
|
|
908
|
+
if (
|
|
909
|
+
cursorKind === "cli-message-id"
|
|
910
|
+
&& normalizeBeeperMessageProviderId(parsed.accountId, cursor)
|
|
911
|
+
!== ids[ids.length - 1]
|
|
912
|
+
) drift(
|
|
913
|
+
"messaging.read output.continuation.cursor",
|
|
914
|
+
"must equal the terminal returned message ID",
|
|
915
|
+
);
|
|
916
|
+
nextInput = Object.freeze({
|
|
917
|
+
account_id: parsed.accountId,
|
|
918
|
+
conversation_id: parsed.conversationId,
|
|
919
|
+
limit: parsed.limit,
|
|
920
|
+
...(requestDirection === "before"
|
|
921
|
+
? { before_cursor: cursor }
|
|
922
|
+
: { after_cursor: cursor }),
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
return Object.freeze({
|
|
926
|
+
schemaVersion: 1,
|
|
927
|
+
partition: `${accountSubject}:messages:${encoded(parsed.accountId)}:${encoded(parsed.conversationId)}`,
|
|
928
|
+
completeness: Object.freeze({
|
|
929
|
+
kind: "bounded-local",
|
|
930
|
+
reason: "Beeper exposed one bounded local message page; connected-account backfill and older edit, reaction, and deletion coverage may be incomplete.",
|
|
931
|
+
}),
|
|
932
|
+
cursor: Object.freeze({
|
|
933
|
+
direction: requestDirection === "before" ? "backward" : "forward",
|
|
934
|
+
request: requestCursor,
|
|
935
|
+
nextInput,
|
|
936
|
+
}),
|
|
937
|
+
entities: Object.freeze(entities),
|
|
938
|
+
tombstones: Object.freeze([]),
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
export function materializeBeeperMessagingReadV1(
|
|
943
|
+
input: OperationInput,
|
|
944
|
+
output: unknown,
|
|
945
|
+
): ProviderMaterializedPageV1 {
|
|
946
|
+
return materializeBeeperMessagingReadLegacy(input, output, "cli-message-id");
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
export function materializeBeeperMessagingReadV2(
|
|
950
|
+
input: OperationInput,
|
|
951
|
+
output: unknown,
|
|
952
|
+
): ProviderMaterializedPageV1 {
|
|
953
|
+
return materializeBeeperMessagingReadLegacy(input, output, "provider-opaque");
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
export function materializeBeeperMessagingRead(
|
|
957
|
+
input: OperationInput,
|
|
958
|
+
output: unknown,
|
|
959
|
+
): ProviderMaterializedPageV1 {
|
|
960
|
+
const parsed = readInput(input);
|
|
961
|
+
const { source, subject: accountSubject } = validateEnvelope(
|
|
962
|
+
output,
|
|
963
|
+
"messaging.read",
|
|
964
|
+
"bounded-local-desktop-direct-iterator",
|
|
965
|
+
);
|
|
966
|
+
exactKeys(source, [
|
|
967
|
+
"provider",
|
|
968
|
+
"operation",
|
|
969
|
+
"accountSubject",
|
|
970
|
+
"projection",
|
|
971
|
+
"accountId",
|
|
972
|
+
"conversationId",
|
|
973
|
+
"selfUserId",
|
|
974
|
+
"requestCursor",
|
|
975
|
+
"requestDirection",
|
|
976
|
+
"requestedSender",
|
|
977
|
+
"messages",
|
|
978
|
+
"tombstones",
|
|
979
|
+
"continuation",
|
|
980
|
+
"completeness",
|
|
981
|
+
], [], "messaging.read output");
|
|
982
|
+
if (source.accountId !== parsed.accountId || source.conversationId !== parsed.conversationId) {
|
|
983
|
+
drift("messaging.read output", "must bind input account and conversation");
|
|
984
|
+
}
|
|
985
|
+
const selfUserId = beeperRawIdentifier(
|
|
986
|
+
source.selfUserId,
|
|
987
|
+
"messaging.read output.selfUserId",
|
|
988
|
+
2_048,
|
|
989
|
+
);
|
|
990
|
+
const requestCursor = parsed.beforeCursor ?? parsed.afterCursor;
|
|
991
|
+
if (source.requestCursor !== requestCursor) {
|
|
992
|
+
drift("messaging.read output.requestCursor", "must bind the requested cursor");
|
|
993
|
+
}
|
|
994
|
+
const requestDirection = parsed.afterCursor === null ? "before" : "after";
|
|
995
|
+
if (source.requestDirection !== requestDirection) {
|
|
996
|
+
drift("messaging.read output.requestDirection", "must bind the requested direction");
|
|
997
|
+
}
|
|
998
|
+
if (source.requestedSender !== parsed.sender) {
|
|
999
|
+
drift("messaging.read output.requestedSender", "must bind the requested sender");
|
|
1000
|
+
}
|
|
1001
|
+
if (source.projection !== "bounded-local-desktop-direct-iterator") {
|
|
1002
|
+
drift("messaging.read output.projection", "must bind direct iterator contract v3");
|
|
1003
|
+
}
|
|
1004
|
+
const rawMessages = array(source.messages, "messaging.read output.messages", parsed.limit);
|
|
1005
|
+
const entities = rawMessages.map((item, index) => message(
|
|
1006
|
+
item,
|
|
1007
|
+
`messaging.read output.messages[${index}]`,
|
|
1008
|
+
parsed.accountId,
|
|
1009
|
+
parsed.conversationId,
|
|
1010
|
+
));
|
|
1011
|
+
const ids = entities.map((entity) => entity.providerId);
|
|
1012
|
+
if (new Set(ids).size !== ids.length) {
|
|
1013
|
+
drift("messaging.read output.messages", "contains duplicate stable message IDs");
|
|
1014
|
+
}
|
|
1015
|
+
rawMessages.forEach((item, index) => {
|
|
1016
|
+
const rawMessage = record(item, `messaging.read output.messages[${index}]`);
|
|
1017
|
+
const senderId = beeperRawIdentifier(
|
|
1018
|
+
rawMessage.senderId,
|
|
1019
|
+
`messaging.read output.messages[${index}].senderId`,
|
|
1020
|
+
2_048,
|
|
1021
|
+
);
|
|
1022
|
+
const isSender = nullableBoolean(
|
|
1023
|
+
rawMessage.isSender,
|
|
1024
|
+
`messaging.read output.messages[${index}].isSender`,
|
|
1025
|
+
);
|
|
1026
|
+
if (
|
|
1027
|
+
isSender !== null
|
|
1028
|
+
&& isSender !== (senderId === selfUserId)
|
|
1029
|
+
) drift(
|
|
1030
|
+
"messaging.read output.messages",
|
|
1031
|
+
"must bind direction to the exact self user ID",
|
|
1032
|
+
);
|
|
1033
|
+
if (parsed.sender !== null) {
|
|
1034
|
+
if (
|
|
1035
|
+
parsed.sender === "me"
|
|
1036
|
+
? isSender !== true
|
|
1037
|
+
: parsed.sender === "others"
|
|
1038
|
+
? isSender !== false
|
|
1039
|
+
: senderId !== parsed.sender
|
|
1040
|
+
) drift("messaging.read output.messages", "must satisfy the exact requested sender");
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
array(source.tombstones, "messaging.read output.tombstones", parsed.limit);
|
|
1044
|
+
const completeness = record(source.completeness, "messaging.read output.completeness");
|
|
1045
|
+
exactKeys(completeness, [
|
|
1046
|
+
"localPageComplete",
|
|
1047
|
+
"remoteConversationHistoryComplete",
|
|
1048
|
+
"limitReached",
|
|
1049
|
+
"warnings",
|
|
1050
|
+
], [], "messaging.read output.completeness");
|
|
1051
|
+
const limitReached = boolean(
|
|
1052
|
+
completeness.limitReached,
|
|
1053
|
+
"messaging.read output.completeness.limitReached",
|
|
1054
|
+
);
|
|
1055
|
+
if (limitReached !== (entities.length === parsed.limit)) {
|
|
1056
|
+
drift("messaging.read output.completeness.limitReached", "must match the returned page bound");
|
|
1057
|
+
}
|
|
1058
|
+
const localPageComplete = boolean(
|
|
1059
|
+
completeness.localPageComplete,
|
|
1060
|
+
"messaging.read output.completeness.localPageComplete",
|
|
1061
|
+
);
|
|
1062
|
+
if (boolean(
|
|
1063
|
+
completeness.remoteConversationHistoryComplete,
|
|
1064
|
+
"messaging.read output.completeness.remoteConversationHistoryComplete",
|
|
1065
|
+
)) drift(
|
|
1066
|
+
"messaging.read output.completeness.remoteConversationHistoryComplete",
|
|
1067
|
+
"must not claim remote-history completeness",
|
|
1068
|
+
);
|
|
1069
|
+
const warnings = array(
|
|
1070
|
+
completeness.warnings,
|
|
1071
|
+
"messaging.read output.completeness.warnings",
|
|
1072
|
+
32,
|
|
1073
|
+
).map((item, index) => string(
|
|
1074
|
+
item,
|
|
1075
|
+
`messaging.read output.completeness.warnings[${index}]`,
|
|
1076
|
+
256,
|
|
1077
|
+
));
|
|
1078
|
+
if (new Set(warnings).size !== warnings.length) {
|
|
1079
|
+
drift("messaging.read output.completeness.warnings", "must not repeat evidence");
|
|
1080
|
+
}
|
|
1081
|
+
for (const required of [
|
|
1082
|
+
"continuation-is-an-opaque-provider-page-boundary-cursor",
|
|
1083
|
+
"sender-filtering-is-local-to-the-bounded-direct-iterator",
|
|
1084
|
+
]) {
|
|
1085
|
+
if (!warnings.includes(required)) {
|
|
1086
|
+
drift("messaging.read output.completeness.warnings", `must retain ${required}`);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
if ((source.continuation !== null) === localPageComplete) {
|
|
1090
|
+
drift(
|
|
1091
|
+
"messaging.read output.continuation",
|
|
1092
|
+
"must exist exactly when the bounded direct iterator has more local history",
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
for (let index = 1; index < rawMessages.length; index += 1) {
|
|
1096
|
+
const previous = record(rawMessages[index - 1], `messaging.read output.messages[${index - 1}]`);
|
|
1097
|
+
const current = record(rawMessages[index], `messaging.read output.messages[${index}]`);
|
|
1098
|
+
const previousSortKey = string(
|
|
1099
|
+
previous.sortKey,
|
|
1100
|
+
`messaging.read output.messages[${index - 1}].sortKey`,
|
|
1101
|
+
1_024,
|
|
1102
|
+
);
|
|
1103
|
+
const currentSortKey = string(
|
|
1104
|
+
current.sortKey,
|
|
1105
|
+
`messaging.read output.messages[${index}].sortKey`,
|
|
1106
|
+
1_024,
|
|
1107
|
+
);
|
|
1108
|
+
if (
|
|
1109
|
+
requestDirection === "before"
|
|
1110
|
+
? previousSortKey < currentSortKey
|
|
1111
|
+
: previousSortKey > currentSortKey
|
|
1112
|
+
) drift("messaging.read output.messages", "must preserve the requested deterministic order");
|
|
1113
|
+
}
|
|
1114
|
+
let nextInput: Readonly<Record<string, string | number>> | null = null;
|
|
1115
|
+
if (source.continuation !== null) {
|
|
1116
|
+
const continuation = record(source.continuation, "messaging.read output.continuation");
|
|
1117
|
+
exactKeys(
|
|
1118
|
+
continuation,
|
|
1119
|
+
["direction", "cursor"],
|
|
1120
|
+
[],
|
|
1121
|
+
"messaging.read output.continuation",
|
|
1122
|
+
);
|
|
1123
|
+
if (continuation.direction !== requestDirection) {
|
|
1124
|
+
drift("messaging.read output.continuation.direction", "must preserve direction");
|
|
1125
|
+
}
|
|
1126
|
+
const cursor = beeperRawIdentifier(
|
|
1127
|
+
continuation.cursor,
|
|
1128
|
+
"messaging.read output.continuation.cursor",
|
|
1129
|
+
2_048,
|
|
1130
|
+
);
|
|
1131
|
+
if (cursor === requestCursor) {
|
|
1132
|
+
drift("messaging.read output.continuation.cursor", "must advance");
|
|
1133
|
+
}
|
|
1134
|
+
nextInput = Object.freeze({
|
|
1135
|
+
account_id: parsed.accountId,
|
|
1136
|
+
conversation_id: parsed.conversationId,
|
|
1137
|
+
limit: parsed.limit,
|
|
1138
|
+
...(parsed.sender === null ? {} : { sender: parsed.sender }),
|
|
1139
|
+
...(requestDirection === "before"
|
|
1140
|
+
? { before_cursor: cursor }
|
|
1141
|
+
: { after_cursor: cursor }),
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
return Object.freeze({
|
|
1145
|
+
schemaVersion: 1,
|
|
1146
|
+
partition: `${accountSubject}:messages:${encoded(parsed.accountId)}:${encoded(parsed.conversationId)}`,
|
|
1147
|
+
completeness: Object.freeze({
|
|
1148
|
+
kind: "bounded-local",
|
|
1149
|
+
reason: "Beeper exposed one bounded local message page; connected-account backfill and older edit, reaction, and deletion coverage may be incomplete.",
|
|
1150
|
+
}),
|
|
1151
|
+
cursor: Object.freeze({
|
|
1152
|
+
direction: requestDirection === "before" ? "backward" : "forward",
|
|
1153
|
+
request: requestCursor,
|
|
1154
|
+
nextInput,
|
|
1155
|
+
}),
|
|
1156
|
+
entities: Object.freeze(entities),
|
|
1157
|
+
tombstones: Object.freeze([]),
|
|
1158
|
+
});
|
|
1159
|
+
}
|