@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,1498 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { constants, createReadStream } from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
chmod,
|
|
5
|
+
lstat,
|
|
6
|
+
mkdir,
|
|
7
|
+
open,
|
|
8
|
+
realpath,
|
|
9
|
+
} from "node:fs/promises";
|
|
10
|
+
import { homedir, tmpdir } from "node:os";
|
|
11
|
+
import { isAbsolute, join } from "node:path";
|
|
12
|
+
import { types as nodeTypes } from "node:util";
|
|
13
|
+
|
|
14
|
+
import type { GhostgetAuth } from "../auth";
|
|
15
|
+
import { canonicalJson } from "../canonical-json";
|
|
16
|
+
import type { LocalCliRecipe, OperationInput } from "../model";
|
|
17
|
+
import type {
|
|
18
|
+
LocalCliExecution,
|
|
19
|
+
LocalCliExecutionOptions,
|
|
20
|
+
} from "../local-cli-execution";
|
|
21
|
+
import { OperationDeadline } from "../operation-deadline";
|
|
22
|
+
import type {
|
|
23
|
+
LocalCliPluginRuntimeStatusV1,
|
|
24
|
+
ProviderPluginReconciliationContextV1,
|
|
25
|
+
ProviderPluginReconciliationOptionsV1,
|
|
26
|
+
ProviderPluginReconciliationReadbackV1,
|
|
27
|
+
} from "../provider-plugin";
|
|
28
|
+
import type { ProviderPluginCleanupProofController } from "../provider-plugin-cleanup-execution";
|
|
29
|
+
import { startProviderPluginCleanupTrackedOperation } from "../provider-plugin-cleanup-execution";
|
|
30
|
+
import {
|
|
31
|
+
attachLocalCliCleanupProcessGroup,
|
|
32
|
+
captureLocalCliCleanupResource,
|
|
33
|
+
localCliCleanupProcessGroupStatus,
|
|
34
|
+
type LocalCliCleanupResourceIdentityV1,
|
|
35
|
+
} from "../provider-plugin-cleanup-resource";
|
|
36
|
+
import { removePrivateDirectoryTree } from "../storage";
|
|
37
|
+
import type { ProviderPluginCleanupResourcePublisher } from "../web-session-execution";
|
|
38
|
+
import {
|
|
39
|
+
IMSG_ACCOUNT_SELECTION,
|
|
40
|
+
IMSG_DIRECT_OPERATIONS,
|
|
41
|
+
IMSG_MAX_CHAT_SCAN,
|
|
42
|
+
IMSG_ORIGIN,
|
|
43
|
+
IMSG_REVIEWED_VERSION,
|
|
44
|
+
IMSG_SERVICE,
|
|
45
|
+
IMSG_SMS_FALLBACK,
|
|
46
|
+
IMSG_TOOL_PIN,
|
|
47
|
+
IMSG_TRANSPORT,
|
|
48
|
+
IMSG_UPSTREAM_VERSION,
|
|
49
|
+
boundedImsgString,
|
|
50
|
+
imsgOperationRequests,
|
|
51
|
+
imsgStatusRequest,
|
|
52
|
+
isImsgDirectOperation,
|
|
53
|
+
parseImsgDirectOperationInput,
|
|
54
|
+
type ImsgChatCoordinate,
|
|
55
|
+
type ImsgDirectOperationInput,
|
|
56
|
+
type ImsgDirectOperationName,
|
|
57
|
+
type ImsgMessagingSendInput,
|
|
58
|
+
type ImsgRpcRequest,
|
|
59
|
+
} from "./imessage-direct";
|
|
60
|
+
import {
|
|
61
|
+
imsgArtifactForCurrentRuntime,
|
|
62
|
+
resolvePinnedImsgBinary,
|
|
63
|
+
} from "./imessage-direct-install";
|
|
64
|
+
|
|
65
|
+
const MAX_STDERR_BYTES = 64 * 1024;
|
|
66
|
+
const MAX_STATUS_BYTES = 512 * 1024;
|
|
67
|
+
const SUBJECT_PROBE_TIMEOUT_MS = 30_000;
|
|
68
|
+
const OPERATION_LABEL = "direct iMessage local CLI operation";
|
|
69
|
+
|
|
70
|
+
type JsonRecord = Readonly<Record<string, unknown>>;
|
|
71
|
+
type ImsgAuth = Extract<GhostgetAuth, { readonly kind: "linked-device-store" }>;
|
|
72
|
+
|
|
73
|
+
export type ImsgChatProjection = Readonly<{
|
|
74
|
+
id: number;
|
|
75
|
+
guid: string;
|
|
76
|
+
service: typeof IMSG_SERVICE;
|
|
77
|
+
identifier: string;
|
|
78
|
+
title: string | null;
|
|
79
|
+
kind: "single" | "group";
|
|
80
|
+
participants: readonly string[];
|
|
81
|
+
lastMessageAt: string;
|
|
82
|
+
unreadCount: number | null;
|
|
83
|
+
observedAccountId: string | null;
|
|
84
|
+
observedAccountLogin: string | null;
|
|
85
|
+
observedLastAddressedHandle: string | null;
|
|
86
|
+
}>;
|
|
87
|
+
|
|
88
|
+
export type ImsgMessageProjection = Readonly<{
|
|
89
|
+
id: number;
|
|
90
|
+
guid: string;
|
|
91
|
+
chatId: number;
|
|
92
|
+
chatGuid: string;
|
|
93
|
+
sender: string | null;
|
|
94
|
+
senderName: string | null;
|
|
95
|
+
isFromMe: boolean;
|
|
96
|
+
text: string;
|
|
97
|
+
createdAt: string;
|
|
98
|
+
replyToGuid: string | null;
|
|
99
|
+
}>;
|
|
100
|
+
|
|
101
|
+
export type ImsgTransportOutcome =
|
|
102
|
+
| "accepted"
|
|
103
|
+
| "not_started"
|
|
104
|
+
| "may_have_completed"
|
|
105
|
+
| "still_in_flight"
|
|
106
|
+
| "unknown_post_dispatch";
|
|
107
|
+
|
|
108
|
+
export type ImsgIndeterminateTransportOutcome = Exclude<
|
|
109
|
+
ImsgTransportOutcome,
|
|
110
|
+
"accepted"
|
|
111
|
+
>;
|
|
112
|
+
|
|
113
|
+
export type ImsgRpcInvocation = Readonly<{
|
|
114
|
+
binary: string;
|
|
115
|
+
arguments: readonly ["rpc"];
|
|
116
|
+
stdin: string;
|
|
117
|
+
environment: Readonly<Record<string, string>>;
|
|
118
|
+
timeoutMs: number;
|
|
119
|
+
maxOutputBytes: number;
|
|
120
|
+
maxStderrBytes: number;
|
|
121
|
+
signal?: AbortSignal;
|
|
122
|
+
beforeSpawn?: () => Promise<void>;
|
|
123
|
+
afterSpawn?: (pid: number) => void;
|
|
124
|
+
}>;
|
|
125
|
+
|
|
126
|
+
export type ImsgRpcInvocationResult = Readonly<{
|
|
127
|
+
exitCode: number;
|
|
128
|
+
stdout: string;
|
|
129
|
+
stderr: string;
|
|
130
|
+
}>;
|
|
131
|
+
|
|
132
|
+
export type ImsgDirectRuntimeDependencies = Readonly<{
|
|
133
|
+
/** Test-only absolute executable seam. Production resolves the exact pin. */
|
|
134
|
+
binaryPath?: string;
|
|
135
|
+
/** Test-only default Messages directory seam. Production uses this account's home. */
|
|
136
|
+
expectedMessagesStorePath?: string;
|
|
137
|
+
run?: (invocation: ImsgRpcInvocation) => Promise<ImsgRpcInvocationResult>;
|
|
138
|
+
createOperationRoot?: () => Promise<string>;
|
|
139
|
+
removeOperationRoot?: (path: string) => Promise<void>;
|
|
140
|
+
}>;
|
|
141
|
+
|
|
142
|
+
class ImsgCleanupUnverifiedError extends Error {
|
|
143
|
+
constructor() {
|
|
144
|
+
super("direct iMessage process cleanup could not be proven; retry remains unsafe");
|
|
145
|
+
this.name = "ImsgCleanupUnverifiedError";
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
class ImsgRpcFailure extends Error {
|
|
150
|
+
readonly outcome: Exclude<ImsgTransportOutcome, "accepted" | "unknown_post_dispatch"> | null;
|
|
151
|
+
|
|
152
|
+
constructor(
|
|
153
|
+
outcome: Exclude<ImsgTransportOutcome, "accepted" | "unknown_post_dispatch"> | null,
|
|
154
|
+
) {
|
|
155
|
+
super("imsg RPC returned a categorical failure");
|
|
156
|
+
this.name = "ImsgRpcFailure";
|
|
157
|
+
this.outcome = outcome;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function record(value: unknown, label: string): JsonRecord {
|
|
162
|
+
if (
|
|
163
|
+
nodeTypes.isProxy(value)
|
|
164
|
+
|| typeof value !== "object"
|
|
165
|
+
|| value === null
|
|
166
|
+
|| Array.isArray(value)
|
|
167
|
+
|| (
|
|
168
|
+
Object.getPrototypeOf(value) !== Object.prototype
|
|
169
|
+
&& Object.getPrototypeOf(value) !== null
|
|
170
|
+
)
|
|
171
|
+
) throw new Error(`${label} must be a plain object`);
|
|
172
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
173
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
174
|
+
const descriptor = typeof key === "string" ? descriptors[key] : undefined;
|
|
175
|
+
if (
|
|
176
|
+
typeof key !== "string"
|
|
177
|
+
|| descriptor === undefined
|
|
178
|
+
|| !descriptor.enumerable
|
|
179
|
+
|| !("value" in descriptor)
|
|
180
|
+
) throw new Error(`${label} must contain only enumerable data fields`);
|
|
181
|
+
}
|
|
182
|
+
return value as JsonRecord;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function exactKeys(
|
|
186
|
+
value: JsonRecord,
|
|
187
|
+
required: readonly string[],
|
|
188
|
+
optional: readonly string[],
|
|
189
|
+
label: string,
|
|
190
|
+
): void {
|
|
191
|
+
const allowed = new Set([...required, ...optional]);
|
|
192
|
+
if (
|
|
193
|
+
required.some((key) => !Object.hasOwn(value, key))
|
|
194
|
+
|| Object.keys(value).some((key) => !allowed.has(key))
|
|
195
|
+
) throw new Error(`${label} has unsupported fields`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function array(value: unknown, label: string, maximum: number): readonly unknown[] {
|
|
199
|
+
if (
|
|
200
|
+
nodeTypes.isProxy(value)
|
|
201
|
+
|| !Array.isArray(value)
|
|
202
|
+
|| Object.getPrototypeOf(value) !== Array.prototype
|
|
203
|
+
|| value.length > maximum
|
|
204
|
+
) throw new Error(`${label} must be a bounded dense array`);
|
|
205
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
206
|
+
if (!Object.hasOwn(value, index)) throw new Error(`${label} must not be sparse`);
|
|
207
|
+
}
|
|
208
|
+
return value;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function integer(
|
|
212
|
+
value: unknown,
|
|
213
|
+
label: string,
|
|
214
|
+
minimum = 0,
|
|
215
|
+
maximum = Number.MAX_SAFE_INTEGER,
|
|
216
|
+
): number {
|
|
217
|
+
if (
|
|
218
|
+
typeof value !== "number"
|
|
219
|
+
|| !Number.isSafeInteger(value)
|
|
220
|
+
|| value < minimum
|
|
221
|
+
|| value > maximum
|
|
222
|
+
) throw new Error(`${label} must be a bounded integer`);
|
|
223
|
+
return value;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function nullableString(
|
|
227
|
+
value: unknown,
|
|
228
|
+
label: string,
|
|
229
|
+
maximum: number,
|
|
230
|
+
): string | null {
|
|
231
|
+
return value === undefined || value === null
|
|
232
|
+
? null
|
|
233
|
+
: boundedImsgString(value, label, maximum, { allowEmpty: true });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function timestamp(value: unknown, label: string): string {
|
|
237
|
+
const source = boundedImsgString(value, label, 64);
|
|
238
|
+
const milliseconds = Date.parse(source);
|
|
239
|
+
if (!Number.isFinite(milliseconds)) throw new Error(`${label} must be a timestamp`);
|
|
240
|
+
return new Date(milliseconds).toISOString();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function requireImsgAuth(auth: GhostgetAuth): ImsgAuth {
|
|
244
|
+
if (auth.kind !== "linked-device-store" || auth.provider !== "imessage") {
|
|
245
|
+
throw new Error("direct iMessage requires an imessage linked-device-store auth locator");
|
|
246
|
+
}
|
|
247
|
+
return auth;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function validateMessagesStore(
|
|
251
|
+
path: string,
|
|
252
|
+
expectedPath: string,
|
|
253
|
+
): Promise<Readonly<{ storePath: string; databasePath: string }>> {
|
|
254
|
+
if (!isAbsolute(path) || !isAbsolute(expectedPath)) {
|
|
255
|
+
throw new Error("Messages store paths must be absolute");
|
|
256
|
+
}
|
|
257
|
+
const canonical = await realpath(path);
|
|
258
|
+
const expectedCanonical = await realpath(expectedPath);
|
|
259
|
+
const stats = await lstat(path);
|
|
260
|
+
if (
|
|
261
|
+
canonical !== path
|
|
262
|
+
|| canonical !== expectedCanonical
|
|
263
|
+
|| !stats.isDirectory()
|
|
264
|
+
|| stats.isSymbolicLink()
|
|
265
|
+
|| stats.uid !== process.getuid?.()
|
|
266
|
+
|| (stats.mode & 0o022) !== 0
|
|
267
|
+
) throw new Error("Messages store must be the owned physical device-default Messages directory");
|
|
268
|
+
const databasePath = join(canonical, "chat.db");
|
|
269
|
+
const canonicalDatabasePath = await realpath(databasePath);
|
|
270
|
+
const databaseStats = await lstat(databasePath);
|
|
271
|
+
if (
|
|
272
|
+
canonicalDatabasePath !== databasePath
|
|
273
|
+
|| !databaseStats.isFile()
|
|
274
|
+
|| databaseStats.isSymbolicLink()
|
|
275
|
+
|| databaseStats.uid !== process.getuid?.()
|
|
276
|
+
|| (databaseStats.mode & 0o022) !== 0
|
|
277
|
+
) throw new Error("Messages database must be an owned physical non-writable-by-others file");
|
|
278
|
+
return Object.freeze({ storePath: canonical, databasePath: canonicalDatabasePath });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function subjectForStore(path: string): string {
|
|
282
|
+
const digest = createHash("sha256")
|
|
283
|
+
.update("wrench-imessage-device-default-v1\0", "utf8")
|
|
284
|
+
.update(path, "utf8")
|
|
285
|
+
.digest("hex");
|
|
286
|
+
return `imessage:device-default:${digest}`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function createOperationRoot(): Promise<string> {
|
|
290
|
+
const parent = await realpath(tmpdir());
|
|
291
|
+
for (let attempt = 0; attempt < 16; attempt += 1) {
|
|
292
|
+
const path = join(parent, `wrench-imessage-${randomBytes(16).toString("hex")}`);
|
|
293
|
+
try {
|
|
294
|
+
await mkdir(path, { mode: 0o700 });
|
|
295
|
+
return path;
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (
|
|
298
|
+
typeof error !== "object"
|
|
299
|
+
|| error === null
|
|
300
|
+
|| !("code" in error)
|
|
301
|
+
|| error.code !== "EEXIST"
|
|
302
|
+
) throw error;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
throw new Error("direct iMessage operation root allocation failed");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function sha256File(path: string): Promise<string> {
|
|
309
|
+
return await new Promise((resolve, reject) => {
|
|
310
|
+
const hash = createHash("sha256");
|
|
311
|
+
const stream = createReadStream(path);
|
|
312
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
313
|
+
stream.on("error", reject);
|
|
314
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function materializePinnedBinary(
|
|
319
|
+
sourcePath: string,
|
|
320
|
+
operationRoot: string,
|
|
321
|
+
expectedSha256: string,
|
|
322
|
+
): Promise<string> {
|
|
323
|
+
const source = await open(sourcePath, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
324
|
+
const destinationPath = join(operationRoot, "imsg");
|
|
325
|
+
let destination;
|
|
326
|
+
try {
|
|
327
|
+
const before = await source.stat();
|
|
328
|
+
if (
|
|
329
|
+
!before.isFile()
|
|
330
|
+
|| before.size < 1
|
|
331
|
+
|| before.size > 256 * 1024 * 1024
|
|
332
|
+
|| (before.mode & 0o022) !== 0
|
|
333
|
+
|| (before.mode & 0o111) === 0
|
|
334
|
+
|| (before.uid !== process.getuid?.() && before.uid !== 0)
|
|
335
|
+
) throw new Error("reviewed imsg executable source is unsafe");
|
|
336
|
+
destination = await open(
|
|
337
|
+
destinationPath,
|
|
338
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL,
|
|
339
|
+
0o500,
|
|
340
|
+
);
|
|
341
|
+
const hash = createHash("sha256");
|
|
342
|
+
const buffer = Buffer.allocUnsafe(1024 * 1024);
|
|
343
|
+
let offset = 0;
|
|
344
|
+
while (offset < before.size) {
|
|
345
|
+
const read = await source.read(
|
|
346
|
+
buffer,
|
|
347
|
+
0,
|
|
348
|
+
Math.min(buffer.byteLength, before.size - offset),
|
|
349
|
+
offset,
|
|
350
|
+
);
|
|
351
|
+
if (read.bytesRead < 1) throw new Error("reviewed imsg executable changed while copied");
|
|
352
|
+
const chunk = buffer.subarray(0, read.bytesRead);
|
|
353
|
+
hash.update(chunk);
|
|
354
|
+
let written = 0;
|
|
355
|
+
while (written < chunk.byteLength) {
|
|
356
|
+
const result = await destination.write(
|
|
357
|
+
chunk,
|
|
358
|
+
written,
|
|
359
|
+
chunk.byteLength - written,
|
|
360
|
+
offset + written,
|
|
361
|
+
);
|
|
362
|
+
if (result.bytesWritten < 1) throw new Error("reviewed imsg private copy failed");
|
|
363
|
+
written += result.bytesWritten;
|
|
364
|
+
}
|
|
365
|
+
offset += read.bytesRead;
|
|
366
|
+
}
|
|
367
|
+
const extra = await source.read(Buffer.allocUnsafe(1), 0, 1, offset);
|
|
368
|
+
const after = await source.stat();
|
|
369
|
+
if (
|
|
370
|
+
extra.bytesRead !== 0
|
|
371
|
+
|| before.dev !== after.dev
|
|
372
|
+
|| before.ino !== after.ino
|
|
373
|
+
|| before.size !== after.size
|
|
374
|
+
|| before.mtimeMs !== after.mtimeMs
|
|
375
|
+
|| before.ctimeMs !== after.ctimeMs
|
|
376
|
+
|| hash.digest("hex") !== expectedSha256
|
|
377
|
+
) throw new Error("reviewed imsg executable changed or failed its pin");
|
|
378
|
+
} finally {
|
|
379
|
+
await destination?.close();
|
|
380
|
+
await source.close();
|
|
381
|
+
}
|
|
382
|
+
await chmod(destinationPath, 0o500);
|
|
383
|
+
if (await sha256File(destinationPath) !== expectedSha256) {
|
|
384
|
+
throw new Error("reviewed imsg private copy failed its pin");
|
|
385
|
+
}
|
|
386
|
+
return destinationPath;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
async function readBoundedStream(
|
|
390
|
+
stream: ReadableStream<Uint8Array>,
|
|
391
|
+
maximum: number,
|
|
392
|
+
label: string,
|
|
393
|
+
): Promise<string> {
|
|
394
|
+
const reader = stream.getReader();
|
|
395
|
+
const chunks: Uint8Array[] = [];
|
|
396
|
+
let byteLength = 0;
|
|
397
|
+
try {
|
|
398
|
+
for (;;) {
|
|
399
|
+
const item = await reader.read();
|
|
400
|
+
if (item.done) break;
|
|
401
|
+
if (item.value.byteLength > maximum - byteLength) {
|
|
402
|
+
throw new Error(`${label} exceeded its byte bound`);
|
|
403
|
+
}
|
|
404
|
+
chunks.push(item.value.slice());
|
|
405
|
+
byteLength += item.value.byteLength;
|
|
406
|
+
}
|
|
407
|
+
} finally {
|
|
408
|
+
reader.releaseLock();
|
|
409
|
+
}
|
|
410
|
+
const output = new Uint8Array(byteLength);
|
|
411
|
+
let offset = 0;
|
|
412
|
+
for (const chunk of chunks) {
|
|
413
|
+
output.set(chunk, offset);
|
|
414
|
+
offset += chunk.byteLength;
|
|
415
|
+
}
|
|
416
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(output);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export async function runImsgRpc(
|
|
420
|
+
invocation: ImsgRpcInvocation,
|
|
421
|
+
): Promise<ImsgRpcInvocationResult> {
|
|
422
|
+
const isAborted = (): boolean => invocation.signal?.aborted === true;
|
|
423
|
+
if (isAborted()) {
|
|
424
|
+
throw new Error("imsg RPC invocation was cancelled");
|
|
425
|
+
}
|
|
426
|
+
await invocation.beforeSpawn?.();
|
|
427
|
+
if (isAborted()) {
|
|
428
|
+
throw new Error("imsg RPC invocation was cancelled");
|
|
429
|
+
}
|
|
430
|
+
const child = Bun.spawn([invocation.binary, ...invocation.arguments], {
|
|
431
|
+
env: { ...invocation.environment },
|
|
432
|
+
stdin: "pipe",
|
|
433
|
+
stdout: "pipe",
|
|
434
|
+
stderr: "pipe",
|
|
435
|
+
detached: true,
|
|
436
|
+
});
|
|
437
|
+
try {
|
|
438
|
+
invocation.afterSpawn?.(child.pid);
|
|
439
|
+
} catch (error) {
|
|
440
|
+
try {
|
|
441
|
+
process.kill(-child.pid, "SIGKILL");
|
|
442
|
+
} catch {
|
|
443
|
+
// The unadmitted process group already exited.
|
|
444
|
+
}
|
|
445
|
+
await child.exited;
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
448
|
+
let timedOut = false;
|
|
449
|
+
let cancelled = false;
|
|
450
|
+
let forceKill: ReturnType<typeof setTimeout> | null = null;
|
|
451
|
+
let terminationStarted = false;
|
|
452
|
+
const signalGroup = (signal: "SIGTERM" | "SIGKILL"): void => {
|
|
453
|
+
try {
|
|
454
|
+
process.kill(-child.pid, signal);
|
|
455
|
+
} catch {
|
|
456
|
+
// child.exited remains the reaping proof.
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
const terminate = (): void => {
|
|
460
|
+
if (!terminationStarted) {
|
|
461
|
+
terminationStarted = true;
|
|
462
|
+
signalGroup("SIGTERM");
|
|
463
|
+
}
|
|
464
|
+
forceKill ??= setTimeout(() => signalGroup("SIGKILL"), 1_000);
|
|
465
|
+
};
|
|
466
|
+
const onAbort = (): void => {
|
|
467
|
+
cancelled = true;
|
|
468
|
+
terminate();
|
|
469
|
+
};
|
|
470
|
+
invocation.signal?.addEventListener("abort", onAbort, { once: true });
|
|
471
|
+
const timeout = setTimeout(() => {
|
|
472
|
+
timedOut = true;
|
|
473
|
+
terminate();
|
|
474
|
+
}, invocation.timeoutMs);
|
|
475
|
+
const guarded = <T>(promise: Promise<T>): Promise<T> =>
|
|
476
|
+
promise.catch((error: unknown) => {
|
|
477
|
+
terminate();
|
|
478
|
+
throw error;
|
|
479
|
+
});
|
|
480
|
+
const stdin = guarded((async () => {
|
|
481
|
+
await child.stdin.write(invocation.stdin);
|
|
482
|
+
await child.stdin.end();
|
|
483
|
+
})());
|
|
484
|
+
const stdout = guarded(readBoundedStream(
|
|
485
|
+
child.stdout,
|
|
486
|
+
invocation.maxOutputBytes,
|
|
487
|
+
"imsg RPC stdout",
|
|
488
|
+
));
|
|
489
|
+
const stderr = guarded(readBoundedStream(
|
|
490
|
+
child.stderr,
|
|
491
|
+
invocation.maxStderrBytes,
|
|
492
|
+
"imsg RPC stderr",
|
|
493
|
+
));
|
|
494
|
+
try {
|
|
495
|
+
const [stdinResult, stdoutResult, stderrResult, exitResult] =
|
|
496
|
+
await Promise.allSettled([stdin, stdout, stderr, child.exited]);
|
|
497
|
+
if (exitResult.status === "rejected") throw new ImsgCleanupUnverifiedError();
|
|
498
|
+
if (
|
|
499
|
+
stdinResult.status === "rejected"
|
|
500
|
+
|| stdoutResult.status === "rejected"
|
|
501
|
+
|| stderrResult.status === "rejected"
|
|
502
|
+
) throw new Error("imsg RPC stream failed within its bound");
|
|
503
|
+
if (cancelled) throw new Error("imsg RPC invocation was cancelled");
|
|
504
|
+
if (timedOut) throw new Error("imsg RPC invocation timed out");
|
|
505
|
+
return Object.freeze({
|
|
506
|
+
exitCode: exitResult.value,
|
|
507
|
+
stdout: stdoutResult.value,
|
|
508
|
+
stderr: stderrResult.value,
|
|
509
|
+
});
|
|
510
|
+
} finally {
|
|
511
|
+
clearTimeout(timeout);
|
|
512
|
+
if (forceKill !== null) clearTimeout(forceKill);
|
|
513
|
+
invocation.signal?.removeEventListener("abort", onAbort);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function remainingTimeoutMs(
|
|
518
|
+
timeoutMs: number,
|
|
519
|
+
deadline: LocalCliExecutionOptions["operationDeadline"],
|
|
520
|
+
): number {
|
|
521
|
+
deadline?.throwIfUnavailable(OPERATION_LABEL);
|
|
522
|
+
const remaining = Math.min(timeoutMs, deadline?.remainingTimeMs() ?? timeoutMs);
|
|
523
|
+
if (remaining < 1) throw new Error("direct iMessage operation timed out");
|
|
524
|
+
return remaining;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function rpcEnvironment(operationRoot: string): Readonly<Record<string, string>> {
|
|
528
|
+
return Object.freeze({
|
|
529
|
+
PATH: "/usr/bin:/bin",
|
|
530
|
+
LANG: "en_US.UTF-8",
|
|
531
|
+
LC_ALL: "en_US.UTF-8",
|
|
532
|
+
TMPDIR: join(operationRoot, "tmp"),
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function rpcInput(requests: readonly ImsgRpcRequest[]): string {
|
|
537
|
+
return `${requests.map((request) => canonicalJson(request)).join("\n")}\n`;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function parseRpcError(value: unknown): never {
|
|
541
|
+
const error = record(value, "imsg RPC error");
|
|
542
|
+
exactKeys(error, ["code", "message"], ["data"], "imsg RPC error");
|
|
543
|
+
integer(error.code, "imsg RPC error.code", -32_768, 32_767);
|
|
544
|
+
boundedImsgString(error.message, "imsg RPC error.message", 4_096, {
|
|
545
|
+
allowEmpty: true,
|
|
546
|
+
allowNewlines: true,
|
|
547
|
+
});
|
|
548
|
+
if (error.data === undefined) throw new ImsgRpcFailure(null);
|
|
549
|
+
const data = record(error.data, "imsg RPC error.data");
|
|
550
|
+
exactKeys(
|
|
551
|
+
data,
|
|
552
|
+
["disposition", "retry_safe", "transport", "operation", "detail"],
|
|
553
|
+
[],
|
|
554
|
+
"imsg RPC error.data",
|
|
555
|
+
);
|
|
556
|
+
const disposition = boundedImsgString(
|
|
557
|
+
data.disposition,
|
|
558
|
+
"imsg RPC error.data.disposition",
|
|
559
|
+
64,
|
|
560
|
+
);
|
|
561
|
+
if (
|
|
562
|
+
disposition !== "not_started"
|
|
563
|
+
&& disposition !== "may_have_completed"
|
|
564
|
+
&& disposition !== "still_in_flight"
|
|
565
|
+
) throw new ImsgRpcFailure(null);
|
|
566
|
+
if (typeof data.retry_safe !== "boolean") throw new ImsgRpcFailure(null);
|
|
567
|
+
boundedImsgString(data.transport, "imsg RPC error.data.transport", 64);
|
|
568
|
+
boundedImsgString(data.operation, "imsg RPC error.data.operation", 128);
|
|
569
|
+
boundedImsgString(data.detail, "imsg RPC error.data.detail", 8_192, {
|
|
570
|
+
allowEmpty: true,
|
|
571
|
+
allowNewlines: true,
|
|
572
|
+
});
|
|
573
|
+
if (
|
|
574
|
+
(disposition === "not_started" && data.retry_safe !== true)
|
|
575
|
+
|| (disposition !== "not_started" && data.retry_safe !== false)
|
|
576
|
+
) throw new ImsgRpcFailure(null);
|
|
577
|
+
throw new ImsgRpcFailure(disposition);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function parseRpcResponses(
|
|
581
|
+
result: ImsgRpcInvocationResult,
|
|
582
|
+
requests: readonly ImsgRpcRequest[],
|
|
583
|
+
): ReadonlyMap<string, unknown> {
|
|
584
|
+
if (result.exitCode !== 0 || result.stderr.trim().length !== 0) {
|
|
585
|
+
throw new Error("imsg RPC process failed before reviewed output was obtained");
|
|
586
|
+
}
|
|
587
|
+
const lines = result.stdout.split("\n").filter((line) => line.length > 0);
|
|
588
|
+
if (lines.length !== requests.length) {
|
|
589
|
+
throw new Error("imsg RPC returned an unexpected response count");
|
|
590
|
+
}
|
|
591
|
+
const expected = new Set(requests.map((request) => request.id));
|
|
592
|
+
const responses = new Map<string, unknown>();
|
|
593
|
+
for (const [index, line] of lines.entries()) {
|
|
594
|
+
let parsed: unknown;
|
|
595
|
+
try {
|
|
596
|
+
parsed = JSON.parse(line) as unknown;
|
|
597
|
+
} catch {
|
|
598
|
+
throw new Error("imsg RPC returned malformed JSON");
|
|
599
|
+
}
|
|
600
|
+
const envelope = record(parsed, `imsg RPC response ${index}`);
|
|
601
|
+
const hasResult = Object.hasOwn(envelope, "result");
|
|
602
|
+
const hasError = Object.hasOwn(envelope, "error");
|
|
603
|
+
exactKeys(
|
|
604
|
+
envelope,
|
|
605
|
+
["jsonrpc", "id", hasResult ? "result" : "error"],
|
|
606
|
+
[],
|
|
607
|
+
`imsg RPC response ${index}`,
|
|
608
|
+
);
|
|
609
|
+
if (hasResult === hasError || envelope.jsonrpc !== "2.0") {
|
|
610
|
+
throw new Error("imsg RPC returned an invalid response envelope");
|
|
611
|
+
}
|
|
612
|
+
const id = boundedImsgString(envelope.id, `imsg RPC response ${index}.id`, 128);
|
|
613
|
+
if (!expected.has(id) || responses.has(id)) {
|
|
614
|
+
throw new Error("imsg RPC returned an unbound response ID");
|
|
615
|
+
}
|
|
616
|
+
if (hasError) parseRpcError(envelope.error);
|
|
617
|
+
responses.set(id, envelope.result);
|
|
618
|
+
}
|
|
619
|
+
return responses;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function parseStatus(value: unknown): string {
|
|
623
|
+
const source = record(value, "imsg status");
|
|
624
|
+
exactKeys(source, [
|
|
625
|
+
"version",
|
|
626
|
+
"protocol_version",
|
|
627
|
+
"database",
|
|
628
|
+
"bridge",
|
|
629
|
+
"contacts",
|
|
630
|
+
"methods",
|
|
631
|
+
"supported_methods",
|
|
632
|
+
], [], "imsg status");
|
|
633
|
+
if (source.version !== IMSG_UPSTREAM_VERSION || source.protocol_version !== 1) {
|
|
634
|
+
throw new Error("imsg status version or protocol changed from the reviewed contract");
|
|
635
|
+
}
|
|
636
|
+
const database = record(source.database, "imsg status.database");
|
|
637
|
+
exactKeys(database, ["ready"], ["path", "features", "error"], "imsg status.database");
|
|
638
|
+
if (database.ready !== true) throw new Error("Messages database is not currently readable");
|
|
639
|
+
const databasePath = nullableString(database.path, "imsg status.database.path", 4_096);
|
|
640
|
+
if (databasePath === null || databasePath.length === 0 || !isAbsolute(databasePath)) {
|
|
641
|
+
throw new Error("imsg status omitted its absolute Messages database path");
|
|
642
|
+
}
|
|
643
|
+
if (database.features !== undefined) record(database.features, "imsg status.database.features");
|
|
644
|
+
if (database.error !== undefined) {
|
|
645
|
+
boundedImsgString(database.error, "imsg status.database.error", 4_096, {
|
|
646
|
+
allowEmpty: true,
|
|
647
|
+
allowNewlines: true,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
record(source.bridge, "imsg status.bridge");
|
|
651
|
+
const contacts = record(source.contacts, "imsg status.contacts");
|
|
652
|
+
exactKeys(contacts, ["available"], [], "imsg status.contacts");
|
|
653
|
+
if (typeof contacts.available !== "boolean") {
|
|
654
|
+
throw new Error("imsg status contacts capability is malformed");
|
|
655
|
+
}
|
|
656
|
+
const methods = array(source.methods, "imsg status.methods", 256).map(
|
|
657
|
+
(method, index) => boundedImsgString(method, `imsg status.methods[${index}]`, 128),
|
|
658
|
+
);
|
|
659
|
+
array(source.supported_methods, "imsg status.supported_methods", 256).forEach(
|
|
660
|
+
(method, index) => boundedImsgString(
|
|
661
|
+
method,
|
|
662
|
+
`imsg status.supported_methods[${index}]`,
|
|
663
|
+
128,
|
|
664
|
+
),
|
|
665
|
+
);
|
|
666
|
+
for (const required of [
|
|
667
|
+
"status",
|
|
668
|
+
"chats.list",
|
|
669
|
+
"chats.get",
|
|
670
|
+
"messages.history",
|
|
671
|
+
"send",
|
|
672
|
+
"message.send_status",
|
|
673
|
+
]) {
|
|
674
|
+
if (!methods.includes(required)) {
|
|
675
|
+
throw new Error("imsg status did not expose the complete reviewed direct transport");
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return databasePath;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function parseChat(value: unknown, label: string): ImsgChatProjection | null {
|
|
682
|
+
const source = record(value, label);
|
|
683
|
+
exactKeys(source, [
|
|
684
|
+
"id",
|
|
685
|
+
"name",
|
|
686
|
+
"identifier",
|
|
687
|
+
"service",
|
|
688
|
+
"last_message_at",
|
|
689
|
+
"is_group",
|
|
690
|
+
], [
|
|
691
|
+
"guid",
|
|
692
|
+
"display_name",
|
|
693
|
+
"contact_name",
|
|
694
|
+
"participants",
|
|
695
|
+
"account_id",
|
|
696
|
+
"account_login",
|
|
697
|
+
"last_addressed_handle",
|
|
698
|
+
"unread_count",
|
|
699
|
+
], label);
|
|
700
|
+
const id = integer(source.id, `${label}.id`, 1);
|
|
701
|
+
const service = boundedImsgString(source.service, `${label}.service`, 128);
|
|
702
|
+
const guid = nullableString(source.guid, `${label}.guid`, 2_048);
|
|
703
|
+
const identifier = boundedImsgString(
|
|
704
|
+
source.identifier,
|
|
705
|
+
`${label}.identifier`,
|
|
706
|
+
2_048,
|
|
707
|
+
{ allowEmpty: true },
|
|
708
|
+
);
|
|
709
|
+
const name = boundedImsgString(source.name, `${label}.name`, 4_096, {
|
|
710
|
+
allowEmpty: true,
|
|
711
|
+
});
|
|
712
|
+
const displayName = nullableString(source.display_name, `${label}.display_name`, 4_096);
|
|
713
|
+
const contactName = nullableString(source.contact_name, `${label}.contact_name`, 4_096);
|
|
714
|
+
const lastMessageAt = timestamp(source.last_message_at, `${label}.last_message_at`);
|
|
715
|
+
if (typeof source.is_group !== "boolean") throw new Error(`${label}.is_group must be boolean`);
|
|
716
|
+
const participants = source.participants === undefined
|
|
717
|
+
? []
|
|
718
|
+
: array(source.participants, `${label}.participants`, 500).map((participant, index) =>
|
|
719
|
+
boundedImsgString(participant, `${label}.participants[${index}]`, 2_048));
|
|
720
|
+
const unreadCount = source.unread_count === undefined
|
|
721
|
+
? null
|
|
722
|
+
: integer(source.unread_count, `${label}.unread_count`);
|
|
723
|
+
const observedAccountId = nullableString(source.account_id, `${label}.account_id`, 2_048);
|
|
724
|
+
const observedAccountLogin = nullableString(
|
|
725
|
+
source.account_login,
|
|
726
|
+
`${label}.account_login`,
|
|
727
|
+
2_048,
|
|
728
|
+
);
|
|
729
|
+
const observedLastAddressedHandle = nullableString(
|
|
730
|
+
source.last_addressed_handle,
|
|
731
|
+
`${label}.last_addressed_handle`,
|
|
732
|
+
2_048,
|
|
733
|
+
);
|
|
734
|
+
if (service !== IMSG_SERVICE || guid === null || guid.length === 0) return null;
|
|
735
|
+
const title = displayName && displayName.length > 0
|
|
736
|
+
? displayName
|
|
737
|
+
: contactName && contactName.length > 0
|
|
738
|
+
? contactName
|
|
739
|
+
: name.length > 0
|
|
740
|
+
? name
|
|
741
|
+
: identifier.length > 0 ? identifier : null;
|
|
742
|
+
return Object.freeze({
|
|
743
|
+
id,
|
|
744
|
+
guid,
|
|
745
|
+
service: IMSG_SERVICE,
|
|
746
|
+
identifier,
|
|
747
|
+
title,
|
|
748
|
+
kind: source.is_group ? "group" : "single",
|
|
749
|
+
participants: Object.freeze(participants),
|
|
750
|
+
lastMessageAt,
|
|
751
|
+
unreadCount,
|
|
752
|
+
observedAccountId,
|
|
753
|
+
observedAccountLogin,
|
|
754
|
+
observedLastAddressedHandle,
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function parseChats(value: unknown): readonly ImsgChatProjection[] {
|
|
759
|
+
const source = record(value, "imsg chats.list result");
|
|
760
|
+
exactKeys(source, ["chats"], [], "imsg chats.list result");
|
|
761
|
+
const chats = array(source.chats, "imsg chats.list result.chats", IMSG_MAX_CHAT_SCAN)
|
|
762
|
+
.map((chat, index) => parseChat(chat, `imsg chats.list result.chats[${index}]`))
|
|
763
|
+
.filter((chat): chat is ImsgChatProjection => chat !== null);
|
|
764
|
+
const coordinates = chats.map((chat) => `${chat.id}\0${chat.guid}`);
|
|
765
|
+
if (new Set(coordinates).size !== coordinates.length) {
|
|
766
|
+
throw new Error("imsg chats.list repeated an exact chat coordinate");
|
|
767
|
+
}
|
|
768
|
+
return Object.freeze(chats);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function exactChat(
|
|
772
|
+
value: unknown,
|
|
773
|
+
target: ImsgChatCoordinate,
|
|
774
|
+
): ImsgChatProjection {
|
|
775
|
+
const source = record(value, "imsg chats.get result");
|
|
776
|
+
exactKeys(source, ["chat"], [], "imsg chats.get result");
|
|
777
|
+
const chat = parseChat(source.chat, "imsg chats.get result.chat");
|
|
778
|
+
if (
|
|
779
|
+
chat === null
|
|
780
|
+
|| chat.id !== target.observedChatRowId
|
|
781
|
+
|| chat.guid !== target.chatGuid
|
|
782
|
+
|| chat.service !== target.service
|
|
783
|
+
) {
|
|
784
|
+
throw new Error("the exact iMessage chat coordinate is not live");
|
|
785
|
+
}
|
|
786
|
+
return chat;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
function parseMessage(
|
|
790
|
+
value: unknown,
|
|
791
|
+
label: string,
|
|
792
|
+
target: ImsgChatCoordinate,
|
|
793
|
+
): ImsgMessageProjection {
|
|
794
|
+
const source = record(value, label);
|
|
795
|
+
exactKeys(source, [
|
|
796
|
+
"id",
|
|
797
|
+
"chat_id",
|
|
798
|
+
"guid",
|
|
799
|
+
"sender",
|
|
800
|
+
"is_from_me",
|
|
801
|
+
"text",
|
|
802
|
+
"created_at",
|
|
803
|
+
"attachments",
|
|
804
|
+
"reactions",
|
|
805
|
+
"chat_identifier",
|
|
806
|
+
"chat_guid",
|
|
807
|
+
"chat_name",
|
|
808
|
+
"participants",
|
|
809
|
+
"is_group",
|
|
810
|
+
], [
|
|
811
|
+
"reply_to_guid",
|
|
812
|
+
"thread_originator_guid",
|
|
813
|
+
"thread_originator_part",
|
|
814
|
+
"reply_to_text",
|
|
815
|
+
"reply_to_sender",
|
|
816
|
+
"sender_name",
|
|
817
|
+
"destination_caller_id",
|
|
818
|
+
"balloon_bundle_id",
|
|
819
|
+
"url_preview",
|
|
820
|
+
"poll",
|
|
821
|
+
"is_reaction",
|
|
822
|
+
"reaction_type",
|
|
823
|
+
"reaction_emoji",
|
|
824
|
+
"is_reaction_add",
|
|
825
|
+
"reacted_to_guid",
|
|
826
|
+
"is_read",
|
|
827
|
+
"date_read",
|
|
828
|
+
], label);
|
|
829
|
+
const id = integer(source.id, `${label}.id`, 1);
|
|
830
|
+
const chatId = integer(source.chat_id, `${label}.chat_id`, 1);
|
|
831
|
+
const guid = boundedImsgString(source.guid, `${label}.guid`, 2_048);
|
|
832
|
+
const chatGuid = boundedImsgString(source.chat_guid, `${label}.chat_guid`, 2_048);
|
|
833
|
+
if (chatId !== target.observedChatRowId || chatGuid !== target.chatGuid) {
|
|
834
|
+
throw new Error(`${label} did not bind the exact requested chat`);
|
|
835
|
+
}
|
|
836
|
+
const sender = nullableString(source.sender, `${label}.sender`, 2_048);
|
|
837
|
+
const senderName = nullableString(source.sender_name, `${label}.sender_name`, 2_048);
|
|
838
|
+
if (typeof source.is_from_me !== "boolean") {
|
|
839
|
+
throw new Error(`${label}.is_from_me must be boolean`);
|
|
840
|
+
}
|
|
841
|
+
const text = boundedImsgString(source.text, `${label}.text`, 4 * 1024 * 1024, {
|
|
842
|
+
allowEmpty: true,
|
|
843
|
+
allowNewlines: true,
|
|
844
|
+
});
|
|
845
|
+
const createdAt = timestamp(source.created_at, `${label}.created_at`);
|
|
846
|
+
const replyToGuid = nullableString(source.reply_to_guid, `${label}.reply_to_guid`, 2_048);
|
|
847
|
+
if (array(source.attachments, `${label}.attachments`, 64).length !== 0) {
|
|
848
|
+
throw new Error(`${label}.attachments must remain empty when attachment reads are disabled`);
|
|
849
|
+
}
|
|
850
|
+
array(source.reactions, `${label}.reactions`, 10_000);
|
|
851
|
+
boundedImsgString(source.chat_identifier, `${label}.chat_identifier`, 2_048, {
|
|
852
|
+
allowEmpty: true,
|
|
853
|
+
});
|
|
854
|
+
boundedImsgString(source.chat_name, `${label}.chat_name`, 4_096, { allowEmpty: true });
|
|
855
|
+
array(source.participants, `${label}.participants`, 500).forEach((participant, index) =>
|
|
856
|
+
boundedImsgString(participant, `${label}.participants[${index}]`, 2_048));
|
|
857
|
+
if (typeof source.is_group !== "boolean") throw new Error(`${label}.is_group must be boolean`);
|
|
858
|
+
return Object.freeze({
|
|
859
|
+
id,
|
|
860
|
+
guid,
|
|
861
|
+
chatId,
|
|
862
|
+
chatGuid,
|
|
863
|
+
sender: sender === "" ? null : sender,
|
|
864
|
+
senderName: senderName === "" ? null : senderName,
|
|
865
|
+
isFromMe: source.is_from_me,
|
|
866
|
+
text,
|
|
867
|
+
createdAt,
|
|
868
|
+
replyToGuid: replyToGuid === "" ? null : replyToGuid,
|
|
869
|
+
});
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
function parseMessages(
|
|
873
|
+
value: unknown,
|
|
874
|
+
target: ImsgChatCoordinate,
|
|
875
|
+
limit: number,
|
|
876
|
+
): readonly ImsgMessageProjection[] {
|
|
877
|
+
const source = record(value, "imsg messages.history result");
|
|
878
|
+
exactKeys(source, ["messages"], [], "imsg messages.history result");
|
|
879
|
+
const messages = array(source.messages, "imsg messages.history result.messages", limit)
|
|
880
|
+
.map((message, index) => parseMessage(
|
|
881
|
+
message,
|
|
882
|
+
`imsg messages.history result.messages[${index}]`,
|
|
883
|
+
target,
|
|
884
|
+
));
|
|
885
|
+
const ids = messages.map((message) => message.guid);
|
|
886
|
+
if (new Set(ids).size !== ids.length) {
|
|
887
|
+
throw new Error("imsg messages.history repeated a message GUID");
|
|
888
|
+
}
|
|
889
|
+
return Object.freeze(messages);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function parseSendAccepted(
|
|
893
|
+
value: unknown,
|
|
894
|
+
input: ImsgMessagingSendInput,
|
|
895
|
+
): Readonly<Record<string, unknown>> {
|
|
896
|
+
const source = record(value, "imsg send result");
|
|
897
|
+
exactKeys(source, [
|
|
898
|
+
"ok",
|
|
899
|
+
"transport",
|
|
900
|
+
"id",
|
|
901
|
+
"guid",
|
|
902
|
+
"message_id",
|
|
903
|
+
"chat_guid",
|
|
904
|
+
"service",
|
|
905
|
+
], [], "imsg send result");
|
|
906
|
+
const id = integer(source.id, "imsg send result.id", 1);
|
|
907
|
+
const guid = boundedImsgString(source.guid, "imsg send result.guid", 2_048);
|
|
908
|
+
if (
|
|
909
|
+
source.ok !== true
|
|
910
|
+
|| source.transport !== IMSG_TRANSPORT
|
|
911
|
+
|| source.message_id !== guid
|
|
912
|
+
|| source.chat_guid !== input.chatGuid
|
|
913
|
+
|| source.service !== IMSG_SERVICE
|
|
914
|
+
) throw new Error("imsg send did not return exact independently observed acceptance evidence");
|
|
915
|
+
return Object.freeze({
|
|
916
|
+
provider: "imessage",
|
|
917
|
+
operation: "messaging.send",
|
|
918
|
+
accountSelection: IMSG_ACCOUNT_SELECTION,
|
|
919
|
+
service: IMSG_SERVICE,
|
|
920
|
+
transport: IMSG_TRANSPORT,
|
|
921
|
+
smsFallback: IMSG_SMS_FALLBACK,
|
|
922
|
+
transportOutcome: "accepted",
|
|
923
|
+
acceptanceEvidence: "matching-outgoing-chat-db-row",
|
|
924
|
+
chatGuid: input.chatGuid,
|
|
925
|
+
chatRowId: input.observedChatRowId,
|
|
926
|
+
messageGuid: guid,
|
|
927
|
+
messageRowId: id,
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function parseDelivery(value: unknown, expectedGuid: string): Readonly<Record<string, unknown>> {
|
|
932
|
+
const source = record(value, "imsg message.send_status result");
|
|
933
|
+
exactKeys(source, [
|
|
934
|
+
"ok",
|
|
935
|
+
"guid",
|
|
936
|
+
"send_state",
|
|
937
|
+
"service",
|
|
938
|
+
"checked_at",
|
|
939
|
+
"status_fields",
|
|
940
|
+
], ["delivered_at"], "imsg message.send_status result");
|
|
941
|
+
if (source.ok !== true || source.guid !== expectedGuid) {
|
|
942
|
+
throw new Error("imsg send-status read did not bind the exact GUID");
|
|
943
|
+
}
|
|
944
|
+
const state = boundedImsgString(source.send_state, "imsg send-status state", 64);
|
|
945
|
+
const checkedAt = timestamp(source.checked_at, "imsg send-status checked_at");
|
|
946
|
+
const service = source.service === null
|
|
947
|
+
? null
|
|
948
|
+
: boundedImsgString(source.service, "imsg send-status service", 128);
|
|
949
|
+
if (service !== null && service !== IMSG_SERVICE) {
|
|
950
|
+
throw new Error("imsg send-status observed a non-iMessage service");
|
|
951
|
+
}
|
|
952
|
+
const present = source.status_fields !== null;
|
|
953
|
+
if (present) record(source.status_fields, "imsg send-status status_fields");
|
|
954
|
+
if (source.delivered_at !== undefined) {
|
|
955
|
+
timestamp(source.delivered_at, "imsg send-status delivered_at");
|
|
956
|
+
}
|
|
957
|
+
return Object.freeze({
|
|
958
|
+
provider: "imessage",
|
|
959
|
+
operation: "messaging.delivery.read",
|
|
960
|
+
accountSelection: IMSG_ACCOUNT_SELECTION,
|
|
961
|
+
service: service ?? IMSG_SERVICE,
|
|
962
|
+
messageGuid: expectedGuid,
|
|
963
|
+
present,
|
|
964
|
+
sendState: state,
|
|
965
|
+
checkedAt,
|
|
966
|
+
});
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function operationOutput(
|
|
970
|
+
action: ImsgDirectOperationName,
|
|
971
|
+
input: ImsgDirectOperationInput,
|
|
972
|
+
responses: ReadonlyMap<string, unknown>,
|
|
973
|
+
subject: string,
|
|
974
|
+
): unknown {
|
|
975
|
+
if (action === "messaging.list") {
|
|
976
|
+
return Object.freeze({
|
|
977
|
+
provider: "imessage",
|
|
978
|
+
operation: action,
|
|
979
|
+
accountSubject: subject,
|
|
980
|
+
accountSelection: IMSG_ACCOUNT_SELECTION,
|
|
981
|
+
service: IMSG_SERVICE,
|
|
982
|
+
transport: IMSG_TRANSPORT,
|
|
983
|
+
smsFallback: IMSG_SMS_FALLBACK,
|
|
984
|
+
projection: "bounded-local-chat-db",
|
|
985
|
+
conversations: parseChats(responses.get("operation")),
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
const target = input as ImsgChatCoordinate;
|
|
989
|
+
if (action === "conversations.read") {
|
|
990
|
+
return Object.freeze({
|
|
991
|
+
provider: "imessage",
|
|
992
|
+
operation: action,
|
|
993
|
+
accountSubject: subject,
|
|
994
|
+
accountSelection: IMSG_ACCOUNT_SELECTION,
|
|
995
|
+
service: IMSG_SERVICE,
|
|
996
|
+
transport: IMSG_TRANSPORT,
|
|
997
|
+
smsFallback: IMSG_SMS_FALLBACK,
|
|
998
|
+
conversation: exactChat(responses.get("operation"), target),
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
if (action === "messaging.read") {
|
|
1002
|
+
const exact = exactChat(responses.get("route"), target);
|
|
1003
|
+
const limit = (input as ImsgDirectOperationInput & { readonly limit: number }).limit;
|
|
1004
|
+
return Object.freeze({
|
|
1005
|
+
provider: "imessage",
|
|
1006
|
+
operation: action,
|
|
1007
|
+
accountSubject: subject,
|
|
1008
|
+
accountSelection: IMSG_ACCOUNT_SELECTION,
|
|
1009
|
+
service: IMSG_SERVICE,
|
|
1010
|
+
transport: IMSG_TRANSPORT,
|
|
1011
|
+
smsFallback: IMSG_SMS_FALLBACK,
|
|
1012
|
+
projection: "bounded-local-chat-db",
|
|
1013
|
+
conversation: exact,
|
|
1014
|
+
messages: parseMessages(responses.get("operation"), target, limit),
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
if (action === "messaging.send") {
|
|
1018
|
+
return parseSendAccepted(responses.get("operation"), input as ImsgMessagingSendInput);
|
|
1019
|
+
}
|
|
1020
|
+
const delivery = input as ImsgDirectOperationInput & { readonly messageGuid: string };
|
|
1021
|
+
return parseDelivery(responses.get("operation"), delivery.messageGuid);
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
async function withRuntime<T>(
|
|
1025
|
+
auth: ImsgAuth,
|
|
1026
|
+
timeoutMs: number,
|
|
1027
|
+
maxOutputBytes: number,
|
|
1028
|
+
dependencies: ImsgDirectRuntimeDependencies | undefined,
|
|
1029
|
+
environment: Readonly<Record<string, string | undefined>>,
|
|
1030
|
+
deadline: LocalCliExecutionOptions["operationDeadline"],
|
|
1031
|
+
publishCleanupResource: ProviderPluginCleanupResourcePublisher | undefined,
|
|
1032
|
+
cleanup: ProviderPluginCleanupProofController,
|
|
1033
|
+
durableCleanupAdmissionRequired: boolean,
|
|
1034
|
+
operation: (context: Readonly<{
|
|
1035
|
+
subject: string;
|
|
1036
|
+
run: (
|
|
1037
|
+
requests: readonly ImsgRpcRequest[],
|
|
1038
|
+
beforeSpawn?: () => Promise<void>,
|
|
1039
|
+
) => Promise<ReadonlyMap<string, unknown>>;
|
|
1040
|
+
}>) => Promise<T>,
|
|
1041
|
+
): Promise<T> {
|
|
1042
|
+
if (durableCleanupAdmissionRequired && publishCleanupResource === undefined) {
|
|
1043
|
+
const error = new ImsgCleanupUnverifiedError();
|
|
1044
|
+
cleanup.unsafe(error);
|
|
1045
|
+
throw error;
|
|
1046
|
+
}
|
|
1047
|
+
const makeRoot = dependencies?.createOperationRoot ?? createOperationRoot;
|
|
1048
|
+
let operationRoot: string | undefined;
|
|
1049
|
+
let cleanupResource: LocalCliCleanupResourceIdentityV1 | undefined;
|
|
1050
|
+
let productionSpawnAttempted = false;
|
|
1051
|
+
try {
|
|
1052
|
+
const expectedStorePath = dependencies?.expectedMessagesStorePath
|
|
1053
|
+
?? join(homedir(), "Library", "Messages");
|
|
1054
|
+
const store = await validateMessagesStore(auth.path, expectedStorePath);
|
|
1055
|
+
const subject = subjectForStore(store.storePath);
|
|
1056
|
+
if (auth.subject !== undefined && auth.subject !== subject) {
|
|
1057
|
+
throw new Error("current Messages device-default realm does not match the bound auth subject");
|
|
1058
|
+
}
|
|
1059
|
+
operationRoot = await makeRoot();
|
|
1060
|
+
if (!isAbsolute(operationRoot)) throw new Error("imsg operation root must be absolute");
|
|
1061
|
+
await mkdir(operationRoot, { recursive: true, mode: 0o700 });
|
|
1062
|
+
await chmod(operationRoot, 0o700);
|
|
1063
|
+
await mkdir(join(operationRoot, "tmp"), { mode: 0o700 });
|
|
1064
|
+
cleanupResource = captureLocalCliCleanupResource(operationRoot);
|
|
1065
|
+
publishCleanupResource?.(cleanupResource);
|
|
1066
|
+
const afterSpawn = dependencies?.run === undefined && publishCleanupResource !== undefined
|
|
1067
|
+
? (pid: number): void => {
|
|
1068
|
+
productionSpawnAttempted = true;
|
|
1069
|
+
cleanupResource = attachLocalCliCleanupProcessGroup(cleanupResource!, pid);
|
|
1070
|
+
publishCleanupResource(cleanupResource);
|
|
1071
|
+
}
|
|
1072
|
+
: undefined;
|
|
1073
|
+
const binarySource = dependencies?.binaryPath
|
|
1074
|
+
?? await resolvePinnedImsgBinary(environment);
|
|
1075
|
+
if (dependencies?.binaryPath !== undefined && !isAbsolute(binarySource)) {
|
|
1076
|
+
throw new Error("test imsg binary path must be absolute");
|
|
1077
|
+
}
|
|
1078
|
+
const binary = dependencies?.binaryPath !== undefined
|
|
1079
|
+
? binarySource
|
|
1080
|
+
: await materializePinnedBinary(
|
|
1081
|
+
binarySource,
|
|
1082
|
+
operationRoot,
|
|
1083
|
+
imsgArtifactForCurrentRuntime().executableSha256,
|
|
1084
|
+
);
|
|
1085
|
+
const runner = dependencies?.run ?? runImsgRpc;
|
|
1086
|
+
const run = async (
|
|
1087
|
+
requests: readonly ImsgRpcRequest[],
|
|
1088
|
+
beforeSpawn?: () => Promise<void>,
|
|
1089
|
+
): Promise<ReadonlyMap<string, unknown>> => {
|
|
1090
|
+
const result = await runner(Object.freeze({
|
|
1091
|
+
binary,
|
|
1092
|
+
arguments: Object.freeze(["rpc"] as const),
|
|
1093
|
+
stdin: rpcInput(requests),
|
|
1094
|
+
environment: rpcEnvironment(operationRoot!),
|
|
1095
|
+
timeoutMs: remainingTimeoutMs(timeoutMs, deadline),
|
|
1096
|
+
maxOutputBytes,
|
|
1097
|
+
maxStderrBytes: MAX_STDERR_BYTES,
|
|
1098
|
+
...(deadline?.signal === undefined ? {} : { signal: deadline.signal }),
|
|
1099
|
+
...(beforeSpawn === undefined ? {} : { beforeSpawn }),
|
|
1100
|
+
...(afterSpawn === undefined ? {} : { afterSpawn }),
|
|
1101
|
+
}));
|
|
1102
|
+
return parseRpcResponses(result, requests);
|
|
1103
|
+
};
|
|
1104
|
+
const status = await run([imsgStatusRequest()], undefined);
|
|
1105
|
+
const reportedDatabasePath = parseStatus(status.get("status"));
|
|
1106
|
+
if (
|
|
1107
|
+
await realpath(reportedDatabasePath) !== reportedDatabasePath
|
|
1108
|
+
|| reportedDatabasePath !== store.databasePath
|
|
1109
|
+
) throw new Error("imsg status reported a different Messages database than the bound subject");
|
|
1110
|
+
return await operation(Object.freeze({ subject, run }));
|
|
1111
|
+
} finally {
|
|
1112
|
+
try {
|
|
1113
|
+
if (operationRoot === undefined) {
|
|
1114
|
+
cleanup.verified();
|
|
1115
|
+
} else if (dependencies?.removeOperationRoot !== undefined) {
|
|
1116
|
+
await dependencies.removeOperationRoot(operationRoot);
|
|
1117
|
+
} else if (cleanupResource !== undefined) {
|
|
1118
|
+
const groups = cleanupResource.processGroups ?? [];
|
|
1119
|
+
if (
|
|
1120
|
+
(productionSpawnAttempted && groups.length === 0)
|
|
1121
|
+
|| (groups.length > 0
|
|
1122
|
+
&& localCliCleanupProcessGroupStatus(cleanupResource) !== "quiescent")
|
|
1123
|
+
) throw new ImsgCleanupUnverifiedError();
|
|
1124
|
+
const removed = removePrivateDirectoryTree(operationRoot, {
|
|
1125
|
+
device: cleanupResource.root.device,
|
|
1126
|
+
inode: cleanupResource.root.inode,
|
|
1127
|
+
birthtimeNs: cleanupResource.root.birthtimeNs,
|
|
1128
|
+
});
|
|
1129
|
+
if (!removed) throw new ImsgCleanupUnverifiedError();
|
|
1130
|
+
}
|
|
1131
|
+
cleanup.verified();
|
|
1132
|
+
} catch {
|
|
1133
|
+
const error = new ImsgCleanupUnverifiedError();
|
|
1134
|
+
cleanup.unsafe(error);
|
|
1135
|
+
if (!durableCleanupAdmissionRequired) throw error;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
export async function inspectImsgDirectRuntime(
|
|
1141
|
+
environment: Readonly<Record<string, string | undefined>>,
|
|
1142
|
+
): Promise<LocalCliPluginRuntimeStatusV1> {
|
|
1143
|
+
let artifact;
|
|
1144
|
+
try {
|
|
1145
|
+
artifact = imsgArtifactForCurrentRuntime();
|
|
1146
|
+
await resolvePinnedImsgBinary(environment);
|
|
1147
|
+
return Object.freeze({
|
|
1148
|
+
ready: true,
|
|
1149
|
+
platform: artifact.platform,
|
|
1150
|
+
arch: artifact.arch,
|
|
1151
|
+
version: IMSG_REVIEWED_VERSION,
|
|
1152
|
+
executableSha256: artifact.executableSha256,
|
|
1153
|
+
reason: null,
|
|
1154
|
+
});
|
|
1155
|
+
} catch {
|
|
1156
|
+
return Object.freeze({
|
|
1157
|
+
ready: false,
|
|
1158
|
+
platform: process.platform,
|
|
1159
|
+
arch: process.arch,
|
|
1160
|
+
version: null,
|
|
1161
|
+
executableSha256: null,
|
|
1162
|
+
reason: "the exact reviewed direct-iMessage executable is unavailable or failed integrity verification",
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
export async function probeImsgDirectSubject(
|
|
1168
|
+
authValue: GhostgetAuth,
|
|
1169
|
+
options: Readonly<{
|
|
1170
|
+
signal?: AbortSignal;
|
|
1171
|
+
environment?: Readonly<Record<string, string | undefined>>;
|
|
1172
|
+
registerCleanupBarrier?: LocalCliExecutionOptions["registerCleanupBarrier"];
|
|
1173
|
+
dependencies?: ImsgDirectRuntimeDependencies;
|
|
1174
|
+
}> = {},
|
|
1175
|
+
): Promise<string> {
|
|
1176
|
+
const auth = requireImsgAuth(authValue);
|
|
1177
|
+
const deadline = new OperationDeadline(SUBJECT_PROBE_TIMEOUT_MS, {
|
|
1178
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
1179
|
+
});
|
|
1180
|
+
try {
|
|
1181
|
+
return await startProviderPluginCleanupTrackedOperation(
|
|
1182
|
+
options.registerCleanupBarrier,
|
|
1183
|
+
async (publishCleanupResource, cleanup) => withRuntime(
|
|
1184
|
+
auth,
|
|
1185
|
+
SUBJECT_PROBE_TIMEOUT_MS,
|
|
1186
|
+
MAX_STATUS_BYTES,
|
|
1187
|
+
options.dependencies,
|
|
1188
|
+
options.environment ?? process.env,
|
|
1189
|
+
deadline,
|
|
1190
|
+
publishCleanupResource,
|
|
1191
|
+
cleanup,
|
|
1192
|
+
options.registerCleanupBarrier !== undefined,
|
|
1193
|
+
async ({ subject }) => subject,
|
|
1194
|
+
),
|
|
1195
|
+
);
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
if (error instanceof ImsgCleanupUnverifiedError) throw error;
|
|
1198
|
+
throw new Error("direct iMessage status probe failed at a protected local boundary");
|
|
1199
|
+
} finally {
|
|
1200
|
+
deadline.dispose();
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function dispatchEvent(
|
|
1205
|
+
action: ImsgDirectOperationName,
|
|
1206
|
+
started: number,
|
|
1207
|
+
verified: number,
|
|
1208
|
+
) {
|
|
1209
|
+
return Object.freeze({
|
|
1210
|
+
id: action,
|
|
1211
|
+
index: 1,
|
|
1212
|
+
progress: Object.freeze({ planned: 1, started, verified }),
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function uncertainOutput(
|
|
1217
|
+
outcome: ImsgTransportOutcome,
|
|
1218
|
+
): Readonly<Record<string, unknown>> {
|
|
1219
|
+
return Object.freeze({
|
|
1220
|
+
provider: "imessage",
|
|
1221
|
+
operation: "messaging.send",
|
|
1222
|
+
accountSelection: IMSG_ACCOUNT_SELECTION,
|
|
1223
|
+
service: IMSG_SERVICE,
|
|
1224
|
+
transport: IMSG_TRANSPORT,
|
|
1225
|
+
smsFallback: IMSG_SMS_FALLBACK,
|
|
1226
|
+
transportOutcome: outcome,
|
|
1227
|
+
retryAuthorized: false,
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
export function parseImsgPrivateIndeterminateOutcome(
|
|
1232
|
+
value: unknown,
|
|
1233
|
+
): ImsgIndeterminateTransportOutcome {
|
|
1234
|
+
const source = record(value, "direct iMessage indeterminate output");
|
|
1235
|
+
exactKeys(source, [
|
|
1236
|
+
"provider",
|
|
1237
|
+
"operation",
|
|
1238
|
+
"accountSelection",
|
|
1239
|
+
"service",
|
|
1240
|
+
"transport",
|
|
1241
|
+
"smsFallback",
|
|
1242
|
+
"transportOutcome",
|
|
1243
|
+
"retryAuthorized",
|
|
1244
|
+
], [], "direct iMessage indeterminate output");
|
|
1245
|
+
const outcome = source.transportOutcome;
|
|
1246
|
+
if (
|
|
1247
|
+
source.provider !== "imessage"
|
|
1248
|
+
|| source.operation !== "messaging.send"
|
|
1249
|
+
|| source.accountSelection !== IMSG_ACCOUNT_SELECTION
|
|
1250
|
+
|| source.service !== IMSG_SERVICE
|
|
1251
|
+
|| source.transport !== IMSG_TRANSPORT
|
|
1252
|
+
|| source.smsFallback !== IMSG_SMS_FALLBACK
|
|
1253
|
+
|| source.retryAuthorized !== false
|
|
1254
|
+
|| outcome !== "not_started"
|
|
1255
|
+
&& outcome !== "may_have_completed"
|
|
1256
|
+
&& outcome !== "still_in_flight"
|
|
1257
|
+
&& outcome !== "unknown_post_dispatch"
|
|
1258
|
+
) throw new Error("direct iMessage indeterminate output is malformed");
|
|
1259
|
+
return outcome;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
export async function executeImsgDirectOperation(
|
|
1263
|
+
recipe: LocalCliRecipe,
|
|
1264
|
+
inputValue: OperationInput,
|
|
1265
|
+
authValue: GhostgetAuth,
|
|
1266
|
+
options: LocalCliExecutionOptions & Readonly<{
|
|
1267
|
+
dependencies?: ImsgDirectRuntimeDependencies;
|
|
1268
|
+
}> = {},
|
|
1269
|
+
): Promise<LocalCliExecution> {
|
|
1270
|
+
if (
|
|
1271
|
+
recipe.surface !== "imessage"
|
|
1272
|
+
|| recipe.contractVersion !== 1
|
|
1273
|
+
|| !isImsgDirectOperation(recipe.action)
|
|
1274
|
+
) throw new Error("direct iMessage local CLI recipe is not installed");
|
|
1275
|
+
const action = recipe.action;
|
|
1276
|
+
const contract = IMSG_DIRECT_OPERATIONS[action];
|
|
1277
|
+
const input = parseImsgDirectOperationInput(action, inputValue);
|
|
1278
|
+
const auth = requireImsgAuth(authValue);
|
|
1279
|
+
options.operationDeadline?.throwIfUnavailable(OPERATION_LABEL);
|
|
1280
|
+
let started = 0;
|
|
1281
|
+
let verified = 0;
|
|
1282
|
+
try {
|
|
1283
|
+
return await startProviderPluginCleanupTrackedOperation(
|
|
1284
|
+
options.registerCleanupBarrier,
|
|
1285
|
+
async (publishCleanupResource, cleanup) => withRuntime(
|
|
1286
|
+
auth,
|
|
1287
|
+
recipe.timeoutMs,
|
|
1288
|
+
recipe.maxOutputBytes,
|
|
1289
|
+
options.dependencies,
|
|
1290
|
+
options.environment ?? process.env,
|
|
1291
|
+
options.operationDeadline,
|
|
1292
|
+
publishCleanupResource,
|
|
1293
|
+
cleanup,
|
|
1294
|
+
options.registerCleanupBarrier !== undefined,
|
|
1295
|
+
async ({ run, subject }) => {
|
|
1296
|
+
const requests = imsgOperationRequests(action, input);
|
|
1297
|
+
const beforeSpawn = contract.effect === "write"
|
|
1298
|
+
? async (): Promise<void> => {
|
|
1299
|
+
await options.beforeDispatch?.(dispatchEvent(action, started, verified));
|
|
1300
|
+
started = 1;
|
|
1301
|
+
}
|
|
1302
|
+
: undefined;
|
|
1303
|
+
const responses = await run(requests, beforeSpawn);
|
|
1304
|
+
const output = operationOutput(action, input, responses, subject);
|
|
1305
|
+
if (contract.effect === "write") {
|
|
1306
|
+
if (started !== 1) throw new Error("imsg runner omitted dispatch spawn accounting");
|
|
1307
|
+
const accepted = record(output, "imsg accepted output");
|
|
1308
|
+
const target = Object.freeze({
|
|
1309
|
+
schemaVersion: 1 as const,
|
|
1310
|
+
identifier: canonicalJson({
|
|
1311
|
+
chatGuid: accepted.chatGuid,
|
|
1312
|
+
chatRowId: accepted.chatRowId,
|
|
1313
|
+
messageGuid: accepted.messageGuid,
|
|
1314
|
+
service: accepted.service,
|
|
1315
|
+
}),
|
|
1316
|
+
});
|
|
1317
|
+
await options.afterProviderAcceptedMutationTarget?.({
|
|
1318
|
+
id: action,
|
|
1319
|
+
index: 1,
|
|
1320
|
+
target,
|
|
1321
|
+
});
|
|
1322
|
+
await options.afterDispatchVerified?.(dispatchEvent(action, started, 1));
|
|
1323
|
+
verified = 1;
|
|
1324
|
+
}
|
|
1325
|
+
return Object.freeze({
|
|
1326
|
+
status: "succeeded" as const,
|
|
1327
|
+
output,
|
|
1328
|
+
finalUrl: IMSG_ORIGIN,
|
|
1329
|
+
dispatchStarted: started > 0,
|
|
1330
|
+
dispatch: Object.freeze({
|
|
1331
|
+
planned: contract.effect === "write" ? 1 : 0,
|
|
1332
|
+
started,
|
|
1333
|
+
verified,
|
|
1334
|
+
}),
|
|
1335
|
+
});
|
|
1336
|
+
},
|
|
1337
|
+
),
|
|
1338
|
+
);
|
|
1339
|
+
} catch (error) {
|
|
1340
|
+
if (error instanceof ImsgCleanupUnverifiedError) throw error;
|
|
1341
|
+
const postDispatch = started > 0;
|
|
1342
|
+
const outcome = error instanceof ImsgRpcFailure && error.outcome !== null
|
|
1343
|
+
? error.outcome
|
|
1344
|
+
: postDispatch ? "unknown_post_dispatch" : null;
|
|
1345
|
+
return Object.freeze({
|
|
1346
|
+
status: postDispatch ? "indeterminate" : "failed",
|
|
1347
|
+
output: outcome === null ? null : uncertainOutput(outcome),
|
|
1348
|
+
finalUrl: IMSG_ORIGIN,
|
|
1349
|
+
dispatchStarted: postDispatch,
|
|
1350
|
+
dispatch: Object.freeze({
|
|
1351
|
+
planned: contract.effect === "write" ? 1 : 0,
|
|
1352
|
+
started,
|
|
1353
|
+
verified,
|
|
1354
|
+
}),
|
|
1355
|
+
error: postDispatch
|
|
1356
|
+
? "direct iMessage dispatch lacks durable exact acceptance evidence; no retry is authorized"
|
|
1357
|
+
: "direct iMessage operation failed before dispatch",
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/**
|
|
1363
|
+
* Provider-owned one-bubble execution seam for the generic messaging kernel.
|
|
1364
|
+
* It deliberately delegates to the same at-most-once operation boundary and
|
|
1365
|
+
* rejects every non-send recipe before a process can start.
|
|
1366
|
+
*/
|
|
1367
|
+
export async function executeImsgDirectMessagingPart(
|
|
1368
|
+
recipe: LocalCliRecipe,
|
|
1369
|
+
inputValue: OperationInput,
|
|
1370
|
+
authValue: GhostgetAuth,
|
|
1371
|
+
options: LocalCliExecutionOptions & Readonly<{
|
|
1372
|
+
dependencies?: ImsgDirectRuntimeDependencies;
|
|
1373
|
+
afterIndeterminateOutcome?: (
|
|
1374
|
+
outcome: ImsgIndeterminateTransportOutcome,
|
|
1375
|
+
) => Promise<void>;
|
|
1376
|
+
}> = {},
|
|
1377
|
+
): Promise<LocalCliExecution> {
|
|
1378
|
+
if (recipe.action !== "messaging.send") {
|
|
1379
|
+
throw new Error("direct iMessage messaging-part execution requires messaging.send");
|
|
1380
|
+
}
|
|
1381
|
+
const { afterIndeterminateOutcome, ...executionOptions } = options;
|
|
1382
|
+
const result = await executeImsgDirectOperation(
|
|
1383
|
+
recipe,
|
|
1384
|
+
inputValue,
|
|
1385
|
+
authValue,
|
|
1386
|
+
executionOptions,
|
|
1387
|
+
);
|
|
1388
|
+
if (result.status === "indeterminate") {
|
|
1389
|
+
if (
|
|
1390
|
+
result.dispatchStarted !== true
|
|
1391
|
+
|| result.dispatch.started !== 1
|
|
1392
|
+
|| result.dispatch.verified !== 0
|
|
1393
|
+
|| result.output === null
|
|
1394
|
+
) throw new Error("direct iMessage indeterminate dispatch evidence is malformed");
|
|
1395
|
+
const outcome = parseImsgPrivateIndeterminateOutcome(result.output);
|
|
1396
|
+
await afterIndeterminateOutcome?.(outcome);
|
|
1397
|
+
}
|
|
1398
|
+
return result;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
function parseAcceptedTarget(
|
|
1402
|
+
context: ProviderPluginReconciliationContextV1 | undefined,
|
|
1403
|
+
): Readonly<{
|
|
1404
|
+
chatGuid: string;
|
|
1405
|
+
chatRowId: number;
|
|
1406
|
+
messageGuid: string;
|
|
1407
|
+
service: typeof IMSG_SERVICE;
|
|
1408
|
+
}> {
|
|
1409
|
+
if (context?.kind !== "provider-accepted-target-presence") {
|
|
1410
|
+
throw new Error("direct iMessage reconciliation requires exact accepted target evidence");
|
|
1411
|
+
}
|
|
1412
|
+
let parsed: unknown;
|
|
1413
|
+
try {
|
|
1414
|
+
parsed = JSON.parse(context.target.identifier) as unknown;
|
|
1415
|
+
} catch {
|
|
1416
|
+
throw new Error("direct iMessage accepted target is malformed");
|
|
1417
|
+
}
|
|
1418
|
+
if (canonicalJson(parsed) !== context.target.identifier) {
|
|
1419
|
+
throw new Error("direct iMessage accepted target is not canonical");
|
|
1420
|
+
}
|
|
1421
|
+
const source = record(parsed, "direct iMessage accepted target");
|
|
1422
|
+
exactKeys(
|
|
1423
|
+
source,
|
|
1424
|
+
["chatGuid", "chatRowId", "messageGuid", "service"],
|
|
1425
|
+
[],
|
|
1426
|
+
"direct iMessage accepted target",
|
|
1427
|
+
);
|
|
1428
|
+
if (source.service !== IMSG_SERVICE) {
|
|
1429
|
+
throw new Error("direct iMessage accepted target changed service");
|
|
1430
|
+
}
|
|
1431
|
+
return Object.freeze({
|
|
1432
|
+
chatGuid: boundedImsgString(source.chatGuid, "accepted target chat GUID", 2_048),
|
|
1433
|
+
chatRowId: integer(source.chatRowId, "accepted target chat row", 1),
|
|
1434
|
+
messageGuid: boundedImsgString(source.messageGuid, "accepted target message GUID", 2_048),
|
|
1435
|
+
service: IMSG_SERVICE,
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
export async function reconcileImsgDirectOperation(
|
|
1440
|
+
operation: string,
|
|
1441
|
+
inputValue: OperationInput,
|
|
1442
|
+
authValue: GhostgetAuth,
|
|
1443
|
+
context?: ProviderPluginReconciliationContextV1,
|
|
1444
|
+
options: ProviderPluginReconciliationOptionsV1 & Readonly<{
|
|
1445
|
+
dependencies?: ImsgDirectRuntimeDependencies;
|
|
1446
|
+
}> = {},
|
|
1447
|
+
): Promise<ProviderPluginReconciliationReadbackV1> {
|
|
1448
|
+
if (operation !== "messaging.send") {
|
|
1449
|
+
throw new Error("direct iMessage reconciliation is available only for accepted sends");
|
|
1450
|
+
}
|
|
1451
|
+
parseImsgDirectOperationInput("messaging.send", inputValue);
|
|
1452
|
+
const target = parseAcceptedTarget(context);
|
|
1453
|
+
const auth = requireImsgAuth(authValue);
|
|
1454
|
+
const deadline = new OperationDeadline(SUBJECT_PROBE_TIMEOUT_MS, {
|
|
1455
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
1456
|
+
});
|
|
1457
|
+
try {
|
|
1458
|
+
return await startProviderPluginCleanupTrackedOperation(
|
|
1459
|
+
options.registerCleanupBarrier,
|
|
1460
|
+
async (publishCleanupResource, cleanup) => withRuntime(
|
|
1461
|
+
auth,
|
|
1462
|
+
SUBJECT_PROBE_TIMEOUT_MS,
|
|
1463
|
+
MAX_STATUS_BYTES,
|
|
1464
|
+
options.dependencies,
|
|
1465
|
+
options.environment ?? process.env,
|
|
1466
|
+
deadline,
|
|
1467
|
+
publishCleanupResource,
|
|
1468
|
+
cleanup,
|
|
1469
|
+
options.registerCleanupBarrier !== undefined,
|
|
1470
|
+
async ({ run }) => {
|
|
1471
|
+
const responses = await run(imsgOperationRequests(
|
|
1472
|
+
"messaging.delivery.read",
|
|
1473
|
+
Object.freeze({
|
|
1474
|
+
chatGuid: target.chatGuid,
|
|
1475
|
+
observedChatRowId: target.chatRowId,
|
|
1476
|
+
service: target.service,
|
|
1477
|
+
messageGuid: target.messageGuid,
|
|
1478
|
+
}),
|
|
1479
|
+
));
|
|
1480
|
+
const delivery = record(
|
|
1481
|
+
parseDelivery(responses.get("operation"), target.messageGuid),
|
|
1482
|
+
"direct iMessage reconciliation delivery",
|
|
1483
|
+
);
|
|
1484
|
+
return Object.freeze({
|
|
1485
|
+
actualState: delivery.present === true,
|
|
1486
|
+
reason: delivery.present === true
|
|
1487
|
+
? "the exact independently observed outgoing GUID remains present in the local Messages database"
|
|
1488
|
+
: "the exact outgoing GUID has no current local Messages status row",
|
|
1489
|
+
});
|
|
1490
|
+
},
|
|
1491
|
+
),
|
|
1492
|
+
);
|
|
1493
|
+
} finally {
|
|
1494
|
+
deadline.dispose();
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
export const IMSG_DIRECT_ARTIFACT_IDENTITY = IMSG_TOOL_PIN;
|