@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,2136 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { isAbsolute, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import type { GhostgetAuth } from "./auth";
|
|
5
|
+
import { canonicalJson, sha256 } from "./canonical-json";
|
|
6
|
+
import {
|
|
7
|
+
parseMessageLikeMeSourceConversationCoordinateBindingV1,
|
|
8
|
+
ghostgetMessagingContextBindingSha256V2,
|
|
9
|
+
} from "./message-like-me-agentic-messaging";
|
|
10
|
+
import {
|
|
11
|
+
MESSAGING_CONTEXT_BINDING_CONTRACT_HASH,
|
|
12
|
+
MESSAGING_CONTEXT_BINDING_CONTRACT_ID,
|
|
13
|
+
parseMessagingContextBindingV2,
|
|
14
|
+
parseMessagingContextRequestV1,
|
|
15
|
+
parseMessagingRouteResolveRequestV2,
|
|
16
|
+
parseMessagingRoutesRequestV1,
|
|
17
|
+
parseMessagingTurnV1,
|
|
18
|
+
messagingTurnDigest,
|
|
19
|
+
type MessagingContextBindingV2,
|
|
20
|
+
type MessagingContextMessageV1,
|
|
21
|
+
type MessagingContextV1,
|
|
22
|
+
type MessagingPreviewV1,
|
|
23
|
+
type MessagingPrivateProviderOutcomeV1,
|
|
24
|
+
type MessagingReceiptBinding,
|
|
25
|
+
type MessagingRouteV2,
|
|
26
|
+
type MessagingRoutesV2,
|
|
27
|
+
type MessagingRunReceipt,
|
|
28
|
+
type MessagingRunV1,
|
|
29
|
+
} from "./messaging-types";
|
|
30
|
+
import {
|
|
31
|
+
loadMessagingContextRecord,
|
|
32
|
+
loadMessagingRouteRecord,
|
|
33
|
+
messagingRouteRecordHash,
|
|
34
|
+
saveMessagingContextRecord,
|
|
35
|
+
saveMessagingRouteRecord,
|
|
36
|
+
type MessagingContextRecordV1,
|
|
37
|
+
type MessagingRouteRecordV1,
|
|
38
|
+
} from "./messaging-store";
|
|
39
|
+
import {
|
|
40
|
+
isLocalCliOperation,
|
|
41
|
+
isProviderOperation,
|
|
42
|
+
isWebSessionOperation,
|
|
43
|
+
manifestHash,
|
|
44
|
+
type OperationInput,
|
|
45
|
+
} from "./model";
|
|
46
|
+
import { runLocalCliOperationWithDeadline } from "./local-cli-execution";
|
|
47
|
+
import { parseMaterializedPageV1, type ProviderMaterializedPageV1, type ProviderMessageV1 } from "./omni-model";
|
|
48
|
+
import {
|
|
49
|
+
OperationDeadline,
|
|
50
|
+
type OperationDeadlineClock,
|
|
51
|
+
} from "./operation-deadline";
|
|
52
|
+
import { withLocalCliProviderCleanupAdmission } from "./local-cli-admission";
|
|
53
|
+
import type {
|
|
54
|
+
ProviderPluginBindingV1,
|
|
55
|
+
ProviderPluginMessagingDefinitionV1,
|
|
56
|
+
ProviderPluginMessagingExpectedOwnPrefixProofV1,
|
|
57
|
+
} from "./provider-plugin";
|
|
58
|
+
import { requireProviderPluginAuth } from "./provider-plugin-auth";
|
|
59
|
+
import type {
|
|
60
|
+
ProviderPluginOperationResolutionV1,
|
|
61
|
+
ProviderPluginRegistry,
|
|
62
|
+
} from "./provider-plugin-registry";
|
|
63
|
+
import { providerPluginRegistry } from "./provider-plugins";
|
|
64
|
+
import {
|
|
65
|
+
bindMessagingCompositePreviewDigest,
|
|
66
|
+
createMessagingCompositeInvocationPlan,
|
|
67
|
+
executeReadInvocation,
|
|
68
|
+
invocationPlanDigest,
|
|
69
|
+
loadInvocationPlan,
|
|
70
|
+
prepareInvocation,
|
|
71
|
+
saveInvocationPlan,
|
|
72
|
+
type PreparedInvocation,
|
|
73
|
+
type StoredPlan,
|
|
74
|
+
} from "./runtime";
|
|
75
|
+
import {
|
|
76
|
+
initializeMessagingRun,
|
|
77
|
+
messagingExpectedOwnPrefix,
|
|
78
|
+
messagingReceiptBinding,
|
|
79
|
+
messagingRunReceipt,
|
|
80
|
+
readMessagingRun,
|
|
81
|
+
updateMessagingRun,
|
|
82
|
+
type MessagingRunSnapshotV1,
|
|
83
|
+
} from "./messaging-action-store";
|
|
84
|
+
import {
|
|
85
|
+
createPrivateJsonIfAbsent,
|
|
86
|
+
isGhostgetStatePath,
|
|
87
|
+
writePrivateJson,
|
|
88
|
+
writePrivateJsonIfUnchanged,
|
|
89
|
+
} from "./storage";
|
|
90
|
+
import {
|
|
91
|
+
runWebSessionOperationWithDeadline,
|
|
92
|
+
} from "./web-session-execution";
|
|
93
|
+
import {
|
|
94
|
+
withWebSessionCleanupAdmission,
|
|
95
|
+
type WebSessionCleanupAdmissionIdentity,
|
|
96
|
+
} from "./web-session-cleanup-admission";
|
|
97
|
+
|
|
98
|
+
const ROUTE_TTL_MS = 15 * 60_000;
|
|
99
|
+
const CONTEXT_TTL_MS = 5 * 60_000;
|
|
100
|
+
|
|
101
|
+
type Environment = Readonly<Record<string, string | undefined>>;
|
|
102
|
+
|
|
103
|
+
export type MessagingRuntimeOptions = {
|
|
104
|
+
readonly environment?: Environment;
|
|
105
|
+
readonly registry?: ProviderPluginRegistry;
|
|
106
|
+
readonly now?: Date;
|
|
107
|
+
readonly signal?: AbortSignal;
|
|
108
|
+
/** Internal deterministic-clock seam for the per-bubble total deadline. */
|
|
109
|
+
readonly deadlineClock?: OperationDeadlineClock;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
type MessagingResolution = ProviderPluginOperationResolutionV1 & {
|
|
113
|
+
readonly messaging: ProviderPluginMessagingDefinitionV1;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
function parseExpectedOwnPrefixProof(
|
|
117
|
+
value: unknown,
|
|
118
|
+
acceptedCount: number,
|
|
119
|
+
): ProviderPluginMessagingExpectedOwnPrefixProofV1 {
|
|
120
|
+
if (
|
|
121
|
+
typeof value !== "object"
|
|
122
|
+
|| value === null
|
|
123
|
+
|| Array.isArray(value)
|
|
124
|
+
|| Object.getPrototypeOf(value) !== Object.prototype
|
|
125
|
+
) throw new Error("messaging provider returned a malformed prefix proof");
|
|
126
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
127
|
+
if (Reflect.ownKeys(descriptors).some((key) => typeof key !== "string")) {
|
|
128
|
+
throw new Error("messaging provider returned a malformed prefix proof");
|
|
129
|
+
}
|
|
130
|
+
for (const descriptor of Object.values(descriptors)) {
|
|
131
|
+
if (!descriptor.enumerable || !("value" in descriptor)) {
|
|
132
|
+
throw new Error("messaging provider returned a malformed prefix proof");
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const source = value as Record<string, unknown>;
|
|
136
|
+
const keys = Object.keys(source).sort();
|
|
137
|
+
if (source.state === "drift" && keys.length === 1 && keys[0] === "state") {
|
|
138
|
+
return Object.freeze({ state: "drift" as const });
|
|
139
|
+
}
|
|
140
|
+
if (
|
|
141
|
+
source.state !== "proven"
|
|
142
|
+
|| keys.length !== 2
|
|
143
|
+
|| keys[0] !== "matchedAcceptedPrefixCount"
|
|
144
|
+
|| keys[1] !== "state"
|
|
145
|
+
|| typeof source.matchedAcceptedPrefixCount !== "number"
|
|
146
|
+
|| !Number.isSafeInteger(source.matchedAcceptedPrefixCount)
|
|
147
|
+
|| source.matchedAcceptedPrefixCount < 0
|
|
148
|
+
|| source.matchedAcceptedPrefixCount > acceptedCount
|
|
149
|
+
) throw new Error("messaging provider returned a malformed prefix proof");
|
|
150
|
+
return Object.freeze({
|
|
151
|
+
state: "proven" as const,
|
|
152
|
+
matchedAcceptedPrefixCount: source.matchedAcceptedPrefixCount,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export type MessagingPrivateOutputReceiptV1 = {
|
|
157
|
+
readonly schemaVersion: 1;
|
|
158
|
+
readonly format: "wrench.messaging-private-output-receipt";
|
|
159
|
+
readonly artifactFormat: string;
|
|
160
|
+
readonly artifactSha256: string;
|
|
161
|
+
readonly itemCount: number;
|
|
162
|
+
readonly generatedAt: string;
|
|
163
|
+
readonly expiresAt: string | null;
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
type MessagingPrivateOutputArtifact =
|
|
167
|
+
| MessagingRouteV2
|
|
168
|
+
| MessagingRoutesV2
|
|
169
|
+
| MessagingContextV1
|
|
170
|
+
| MessagingPreviewV1
|
|
171
|
+
| MessagingRunV1
|
|
172
|
+
| MessagingReceiptBinding;
|
|
173
|
+
|
|
174
|
+
export type MessagingPrivateOutputReservationV1 = {
|
|
175
|
+
readonly path: string;
|
|
176
|
+
readonly artifactFormat: MessagingPrivateOutputArtifact["format"];
|
|
177
|
+
readonly reservationId: string;
|
|
178
|
+
readonly reservedContentSha256: string;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export type MessagingPrivateOutputReservationPairV1 = {
|
|
182
|
+
readonly run: MessagingPrivateOutputReservationV1;
|
|
183
|
+
readonly receiptBinding: MessagingPrivateOutputReservationV1;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
function now(options: MessagingRuntimeOptions): Date {
|
|
187
|
+
const value = options.now ?? new Date();
|
|
188
|
+
if (!Number.isFinite(value.getTime())) throw new Error("messaging observation time is invalid");
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function opaque(prefix: "wmroute" | "wmcontext" | "wmreply"): string {
|
|
193
|
+
return `${prefix}_${randomBytes(16).toString("base64url")}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function operationResolution(
|
|
197
|
+
invocation: PreparedInvocation,
|
|
198
|
+
registry: ProviderPluginRegistry,
|
|
199
|
+
): ProviderPluginOperationResolutionV1 {
|
|
200
|
+
const operation = invocation.manifest.operations[invocation.operationId];
|
|
201
|
+
if (operation === undefined) throw new Error("messaging operation disappeared");
|
|
202
|
+
if (isLocalCliOperation(operation)) {
|
|
203
|
+
return registry.requireOperationDefinition(
|
|
204
|
+
"local-cli",
|
|
205
|
+
operation.localCli.surface,
|
|
206
|
+
operation.localCli.action,
|
|
207
|
+
operation.localCli.contractVersion,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
if (isWebSessionOperation(operation)) {
|
|
211
|
+
const binding = registry.requireSessionRoute(operation.webSession.site);
|
|
212
|
+
return registry.requireOperationDefinition(
|
|
213
|
+
binding.transport,
|
|
214
|
+
operation.webSession.site,
|
|
215
|
+
operation.webSession.action,
|
|
216
|
+
operation.webSession.contractVersion,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
if (isProviderOperation(operation)) {
|
|
220
|
+
return registry.requireOperationDefinition(
|
|
221
|
+
"provider-api",
|
|
222
|
+
operation.provider.provider,
|
|
223
|
+
operation.provider.action,
|
|
224
|
+
operation.provider.contractVersion,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
throw new Error("messaging requires one code-owned provider operation");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function messagingResolution(
|
|
231
|
+
invocation: PreparedInvocation,
|
|
232
|
+
registry: ProviderPluginRegistry,
|
|
233
|
+
expected: "messaging.list" | "messaging.read" | "action",
|
|
234
|
+
): MessagingResolution {
|
|
235
|
+
const resolution = operationResolution(invocation, registry);
|
|
236
|
+
const messaging = resolution.binding.messaging;
|
|
237
|
+
if (messaging === undefined) {
|
|
238
|
+
throw new Error("selected provider does not expose the messaging SPI");
|
|
239
|
+
}
|
|
240
|
+
const expectedOperation = expected === "action"
|
|
241
|
+
? messaging.action.state === "supported"
|
|
242
|
+
? messaging.action.operation
|
|
243
|
+
: null
|
|
244
|
+
: expected === "messaging.list"
|
|
245
|
+
? messaging.listOperation
|
|
246
|
+
: messaging.contextOperation;
|
|
247
|
+
if (expectedOperation === null || resolution.operation.name !== expectedOperation) {
|
|
248
|
+
throw new Error("selected operation does not match the provider messaging contract");
|
|
249
|
+
}
|
|
250
|
+
return Object.freeze({ ...resolution, messaging });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function requireBoundAuth(
|
|
254
|
+
binding: ProviderPluginBindingV1,
|
|
255
|
+
auth: PreparedInvocation["auth"],
|
|
256
|
+
): asserts auth is GhostgetAuth & { readonly subject: string } {
|
|
257
|
+
if (auth.kind === "public-web-session") {
|
|
258
|
+
throw new Error("messaging requires one private account-bound auth realm");
|
|
259
|
+
}
|
|
260
|
+
requireProviderPluginAuth(binding, auth);
|
|
261
|
+
if (auth.subject === undefined || !binding.subject.matches(auth.subject)) {
|
|
262
|
+
throw new Error("messaging requires one current account-bound auth realm");
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function requireRuntimeReady(
|
|
267
|
+
binding: ProviderPluginBindingV1,
|
|
268
|
+
auth: PreparedInvocation["auth"],
|
|
269
|
+
environment: Environment,
|
|
270
|
+
registry: ProviderPluginRegistry,
|
|
271
|
+
): Promise<void> {
|
|
272
|
+
requireBoundAuth(binding, auth);
|
|
273
|
+
if (binding.transport === "local-cli") {
|
|
274
|
+
const status = await withLocalCliProviderCleanupAdmission(
|
|
275
|
+
{
|
|
276
|
+
registry,
|
|
277
|
+
binding,
|
|
278
|
+
auth: null,
|
|
279
|
+
purpose: { kind: "inspect" },
|
|
280
|
+
environment,
|
|
281
|
+
},
|
|
282
|
+
(registerCleanupBarrier) => binding.inspect(environment, {
|
|
283
|
+
registerCleanupBarrier,
|
|
284
|
+
}),
|
|
285
|
+
);
|
|
286
|
+
if (!status.ready) throw new Error("messaging provider runtime is unavailable");
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (binding.transport === "linked-device") {
|
|
290
|
+
const lifecycle = binding.linkedDeviceLifecycle;
|
|
291
|
+
if (lifecycle === undefined) {
|
|
292
|
+
throw new Error("messaging linked-device runtime has no readiness inspection");
|
|
293
|
+
}
|
|
294
|
+
const status = await lifecycle.inspect(environment);
|
|
295
|
+
if (!status.ready) throw new Error("messaging provider runtime is unavailable");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function implementationIdentity(
|
|
300
|
+
binding: ProviderPluginBindingV1,
|
|
301
|
+
registry: ProviderPluginRegistry,
|
|
302
|
+
): string {
|
|
303
|
+
return sha256(canonicalJson({
|
|
304
|
+
surfaceId: binding.surfaceId,
|
|
305
|
+
transport: binding.transport,
|
|
306
|
+
closureHash: registry.implementationClosureHash(binding),
|
|
307
|
+
artifactSha256: registry.artifactSha256(binding),
|
|
308
|
+
...(binding.transport === "local-cli" ? { tool: binding.tool } : {}),
|
|
309
|
+
}));
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function messagingWebCleanupAdmissionIdentity(
|
|
313
|
+
invocation: PreparedInvocation,
|
|
314
|
+
resolution: MessagingResolution,
|
|
315
|
+
registry: ProviderPluginRegistry,
|
|
316
|
+
runId: string,
|
|
317
|
+
partIndex: number,
|
|
318
|
+
): WebSessionCleanupAdmissionIdentity {
|
|
319
|
+
return Object.freeze({
|
|
320
|
+
runId,
|
|
321
|
+
pluginId: resolution.plugin.id,
|
|
322
|
+
pluginVersion: resolution.plugin.version,
|
|
323
|
+
pluginImplementationHash: registry
|
|
324
|
+
.implementationHash(resolution.binding)
|
|
325
|
+
.toString("hex"),
|
|
326
|
+
adapterId: invocation.manifest.id,
|
|
327
|
+
adapterHash: manifestHash(invocation.manifest),
|
|
328
|
+
surfaceId: resolution.binding.surfaceId,
|
|
329
|
+
authId: invocation.auth.id,
|
|
330
|
+
authHash: sha256(canonicalJson(invocation.auth)),
|
|
331
|
+
transport: "web-session-api" as const,
|
|
332
|
+
executionIdentityHash: sha256(canonicalJson({
|
|
333
|
+
schemaVersion: 1,
|
|
334
|
+
kind: "messaging",
|
|
335
|
+
runId,
|
|
336
|
+
partIndex,
|
|
337
|
+
operation: invocation.operationId,
|
|
338
|
+
binding: implementationIdentity(resolution.binding, registry),
|
|
339
|
+
})),
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function identityFor(
|
|
344
|
+
invocation: PreparedInvocation,
|
|
345
|
+
resolution: MessagingResolution,
|
|
346
|
+
registry: ProviderPluginRegistry,
|
|
347
|
+
): Pick<MessagingRouteRecordV1, "adapter" | "plugin" | "binding" | "auth"> {
|
|
348
|
+
requireBoundAuth(resolution.binding, invocation.auth);
|
|
349
|
+
return Object.freeze({
|
|
350
|
+
adapter: Object.freeze({
|
|
351
|
+
id: invocation.manifest.id,
|
|
352
|
+
version: invocation.manifest.version,
|
|
353
|
+
hash: manifestHash(invocation.manifest),
|
|
354
|
+
}),
|
|
355
|
+
plugin: Object.freeze({
|
|
356
|
+
id: resolution.plugin.id,
|
|
357
|
+
version: resolution.plugin.version,
|
|
358
|
+
closureHash: registry.implementationClosureHash(resolution.binding),
|
|
359
|
+
}),
|
|
360
|
+
binding: Object.freeze({
|
|
361
|
+
surfaceId: resolution.binding.surfaceId,
|
|
362
|
+
transport: resolution.binding.transport,
|
|
363
|
+
implementationIdentity: implementationIdentity(resolution.binding, registry),
|
|
364
|
+
messagingContractId: resolution.messaging.contractId,
|
|
365
|
+
}),
|
|
366
|
+
auth: Object.freeze({
|
|
367
|
+
id: invocation.auth.id,
|
|
368
|
+
hash: sha256(canonicalJson(invocation.auth)),
|
|
369
|
+
subject: invocation.auth.subject,
|
|
370
|
+
}),
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function exactLivePage(
|
|
375
|
+
invocation: PreparedInvocation,
|
|
376
|
+
resolution: MessagingResolution,
|
|
377
|
+
output: unknown,
|
|
378
|
+
): ProviderMaterializedPageV1 {
|
|
379
|
+
const omni = resolution.operation.omni;
|
|
380
|
+
if (omni?.state !== "supported") {
|
|
381
|
+
throw new Error("messaging provider operation lost normalization support");
|
|
382
|
+
}
|
|
383
|
+
return parseMaterializedPageV1(omni.materialize(invocation.input, output));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function routeFingerprint(record: MessagingRouteRecordV1): string {
|
|
387
|
+
return sha256(canonicalJson({
|
|
388
|
+
adapter: record.adapter,
|
|
389
|
+
plugin: record.plugin,
|
|
390
|
+
binding: record.binding,
|
|
391
|
+
auth: record.auth,
|
|
392
|
+
target: record.target,
|
|
393
|
+
resolution: record.resolution,
|
|
394
|
+
network: record.network,
|
|
395
|
+
sourceConversationCoordinate: record.sourceConversationCoordinate,
|
|
396
|
+
conversationProviderId: record.conversationProviderId,
|
|
397
|
+
}));
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function assertRouteIdentity(
|
|
401
|
+
record: MessagingRouteRecordV1,
|
|
402
|
+
invocation: PreparedInvocation,
|
|
403
|
+
resolution: MessagingResolution,
|
|
404
|
+
registry: ProviderPluginRegistry,
|
|
405
|
+
observation: Date,
|
|
406
|
+
): void {
|
|
407
|
+
if (Date.parse(record.expiresAt) <= observation.getTime()) {
|
|
408
|
+
throw new Error("messaging route is unavailable or expired");
|
|
409
|
+
}
|
|
410
|
+
if (
|
|
411
|
+
canonicalJson(invocation.input) !== canonicalJson(record.list.input)
|
|
412
|
+
|| record.list.inputHash !== sha256(canonicalJson(record.list.input))
|
|
413
|
+
) {
|
|
414
|
+
throw new Error("messaging route list input changed; discover a new route");
|
|
415
|
+
}
|
|
416
|
+
const identity = identityFor(invocation, resolution, registry);
|
|
417
|
+
const expected = sha256(canonicalJson({
|
|
418
|
+
adapter: identity.adapter,
|
|
419
|
+
plugin: identity.plugin,
|
|
420
|
+
binding: identity.binding,
|
|
421
|
+
auth: identity.auth,
|
|
422
|
+
target: resolution.messaging.parseTarget(record.target),
|
|
423
|
+
resolution: record.resolution,
|
|
424
|
+
network: resolution.messaging.network,
|
|
425
|
+
sourceConversationCoordinate: record.sourceConversationCoordinate,
|
|
426
|
+
conversationProviderId: record.conversationProviderId,
|
|
427
|
+
}));
|
|
428
|
+
if (expected !== routeFingerprint(record)) {
|
|
429
|
+
throw new Error("messaging route identity changed; discover a new route");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function latestRevision(
|
|
434
|
+
surfaceId: string,
|
|
435
|
+
conversationProviderId: string,
|
|
436
|
+
messages: readonly ProviderMessageV1[],
|
|
437
|
+
exactDataRevision: string,
|
|
438
|
+
): string {
|
|
439
|
+
if (messages.length === 0) {
|
|
440
|
+
return sha256(canonicalJson({
|
|
441
|
+
surfaceId,
|
|
442
|
+
conversationProviderId,
|
|
443
|
+
empty: true,
|
|
444
|
+
exactDataRevision,
|
|
445
|
+
}));
|
|
446
|
+
}
|
|
447
|
+
const latest = [...messages].sort((left, right) =>
|
|
448
|
+
(left.orderedAt ?? "").localeCompare(right.orderedAt ?? "")
|
|
449
|
+
|| left.providerId.localeCompare(right.providerId)).at(-1)!;
|
|
450
|
+
return sha256(canonicalJson({
|
|
451
|
+
surfaceId,
|
|
452
|
+
conversationProviderId,
|
|
453
|
+
providerId: latest.providerId,
|
|
454
|
+
providerRevision: latest.providerRevision,
|
|
455
|
+
orderedAt: latest.orderedAt,
|
|
456
|
+
}));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function messagingBaseMessages(
|
|
460
|
+
messages: readonly ProviderMessageV1[],
|
|
461
|
+
): readonly {
|
|
462
|
+
readonly providerMessageId: string;
|
|
463
|
+
readonly providerRevision: string | null;
|
|
464
|
+
readonly orderedAt: string | null;
|
|
465
|
+
readonly messageSha256: string;
|
|
466
|
+
}[] {
|
|
467
|
+
return Object.freeze([...messages]
|
|
468
|
+
.sort((left, right) => {
|
|
469
|
+
const leftKey = `${left.orderedAt ?? ""}\0${left.providerId}`;
|
|
470
|
+
const rightKey = `${right.orderedAt ?? ""}\0${right.providerId}`;
|
|
471
|
+
return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
|
|
472
|
+
})
|
|
473
|
+
.map((message) => Object.freeze({
|
|
474
|
+
providerMessageId: message.providerId,
|
|
475
|
+
providerRevision: message.providerRevision,
|
|
476
|
+
orderedAt: message.orderedAt,
|
|
477
|
+
messageSha256: sha256(canonicalJson(message)),
|
|
478
|
+
})));
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function pageMessages(
|
|
482
|
+
page: ProviderMaterializedPageV1,
|
|
483
|
+
conversationProviderId: string,
|
|
484
|
+
): readonly ProviderMessageV1[] {
|
|
485
|
+
return Object.freeze(page.entities.map((entity) => {
|
|
486
|
+
if (
|
|
487
|
+
entity.kind !== "message"
|
|
488
|
+
|| entity.conversationProviderId !== conversationProviderId
|
|
489
|
+
) {
|
|
490
|
+
throw new Error("messaging context did not bind the exact route conversation");
|
|
491
|
+
}
|
|
492
|
+
return entity;
|
|
493
|
+
}));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function randomRefMap(
|
|
497
|
+
messages: readonly ProviderMessageV1[],
|
|
498
|
+
): {
|
|
499
|
+
readonly byProvider: ReadonlyMap<string, string>;
|
|
500
|
+
readonly replyTargets: Readonly<Record<string, string>>;
|
|
501
|
+
} {
|
|
502
|
+
const byProvider = new Map<string, string>();
|
|
503
|
+
const replyTargets: Record<string, string> = {};
|
|
504
|
+
for (const message of messages) {
|
|
505
|
+
if (byProvider.has(message.providerId)) {
|
|
506
|
+
throw new Error("messaging context repeated a provider message identity");
|
|
507
|
+
}
|
|
508
|
+
const reference = opaque("wmreply");
|
|
509
|
+
byProvider.set(message.providerId, reference);
|
|
510
|
+
replyTargets[reference] = message.providerId;
|
|
511
|
+
}
|
|
512
|
+
return Object.freeze({
|
|
513
|
+
byProvider,
|
|
514
|
+
replyTargets: Object.freeze(replyTargets),
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function publicMessages(
|
|
519
|
+
messages: readonly ProviderMessageV1[],
|
|
520
|
+
refs: ReadonlyMap<string, string>,
|
|
521
|
+
): readonly MessagingContextMessageV1[] {
|
|
522
|
+
return Object.freeze(messages.map((message) => {
|
|
523
|
+
const messageRef = refs.get(message.providerId);
|
|
524
|
+
if (messageRef === undefined) throw new Error("messaging reply reference disappeared");
|
|
525
|
+
return Object.freeze({
|
|
526
|
+
messageRef,
|
|
527
|
+
direction: message.direction,
|
|
528
|
+
time: message.orderedAt,
|
|
529
|
+
author: message.sender === null
|
|
530
|
+
? null
|
|
531
|
+
: Object.freeze({
|
|
532
|
+
displayName: message.sender.displayName,
|
|
533
|
+
handle: message.sender.handle,
|
|
534
|
+
}),
|
|
535
|
+
body: message.body,
|
|
536
|
+
bodyTruncated: message.bodyTruncated === true,
|
|
537
|
+
edited: "unknown" as const,
|
|
538
|
+
retracted:
|
|
539
|
+
message.state === "revoked"
|
|
540
|
+
|| message.state === "revoked-and-deleted-for-me"
|
|
541
|
+
? "observed" as const
|
|
542
|
+
: "unknown" as const,
|
|
543
|
+
reply: Object.freeze({
|
|
544
|
+
toMessageRef: message.replyToProviderId === null
|
|
545
|
+
? null
|
|
546
|
+
: refs.get(message.replyToProviderId) ?? null,
|
|
547
|
+
}),
|
|
548
|
+
attachments: Object.freeze(message.attachments.map((attachment) =>
|
|
549
|
+
Object.freeze({
|
|
550
|
+
kind: attachment.kind,
|
|
551
|
+
mimeType: attachment.mimeType,
|
|
552
|
+
name: attachment.name,
|
|
553
|
+
sizeBytes: attachment.sizeBytes,
|
|
554
|
+
}))),
|
|
555
|
+
untrustedData: true as const,
|
|
556
|
+
});
|
|
557
|
+
}));
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export function validateMessagingPrivateOutputPath(
|
|
561
|
+
path: string,
|
|
562
|
+
environment: Environment = process.env,
|
|
563
|
+
): string {
|
|
564
|
+
if (
|
|
565
|
+
!isAbsolute(path)
|
|
566
|
+
|| resolve(path) !== path
|
|
567
|
+
|| Buffer.byteLength(path, "utf8") > 4_096
|
|
568
|
+
|| /[\0\r\n]/u.test(path)
|
|
569
|
+
) throw new Error("messaging private output path must be normalized and absolute");
|
|
570
|
+
if (isGhostgetStatePath(path, environment)) {
|
|
571
|
+
throw new Error(
|
|
572
|
+
"messaging private output path must be outside GHOSTGET_STATE_HOME",
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
return path;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export function writeMessagingPrivateOutput(
|
|
579
|
+
path: string,
|
|
580
|
+
artifact: MessagingPrivateOutputArtifact,
|
|
581
|
+
environment: Environment = process.env,
|
|
582
|
+
): MessagingPrivateOutputReceiptV1 {
|
|
583
|
+
const outputPath = validateMessagingPrivateOutputPath(path, environment);
|
|
584
|
+
writePrivateJson(outputPath, artifact, { privateParent: true });
|
|
585
|
+
const artifactSha256 = sha256(canonicalJson(artifact));
|
|
586
|
+
const expiresAt = artifact.format === "wrench.messaging-context"
|
|
587
|
+
? artifact.binding?.expiresAt ?? null
|
|
588
|
+
: artifact.format === "wrench.messaging-preview"
|
|
589
|
+
? artifact.expiresAt
|
|
590
|
+
: artifact.format === "wrench.messaging-route"
|
|
591
|
+
? artifact.expiresAt
|
|
592
|
+
: artifact.format === "wrench.messaging-run"
|
|
593
|
+
? null
|
|
594
|
+
: artifact.format === "wrench.messaging-receipt-binding"
|
|
595
|
+
? null
|
|
596
|
+
: artifact.routes.reduce<string | null>((latest, route) =>
|
|
597
|
+
latest === null || route.expiresAt < latest ? route.expiresAt : latest, null);
|
|
598
|
+
return Object.freeze({
|
|
599
|
+
schemaVersion: 1,
|
|
600
|
+
format: "wrench.messaging-private-output-receipt",
|
|
601
|
+
artifactFormat: artifact.format,
|
|
602
|
+
artifactSha256,
|
|
603
|
+
itemCount: artifact.format === "wrench.messaging-routes"
|
|
604
|
+
? artifact.routes.length
|
|
605
|
+
: artifact.format === "wrench.messaging-context"
|
|
606
|
+
? artifact.messages.length
|
|
607
|
+
: artifact.format === "wrench.messaging-route"
|
|
608
|
+
? 1
|
|
609
|
+
: artifact.format === "wrench.messaging-preview"
|
|
610
|
+
|| artifact.format === "wrench.messaging-run"
|
|
611
|
+
? artifact.partCount
|
|
612
|
+
: 1,
|
|
613
|
+
generatedAt: artifact.format === "wrench.messaging-routes"
|
|
614
|
+
? artifact.generatedAt
|
|
615
|
+
: artifact.format === "wrench.messaging-context"
|
|
616
|
+
? artifact.binding?.validatedAt ?? new Date().toISOString()
|
|
617
|
+
: artifact.format === "wrench.messaging-route"
|
|
618
|
+
? new Date().toISOString()
|
|
619
|
+
: artifact.format === "wrench.messaging-run"
|
|
620
|
+
|| artifact.format === "wrench.messaging-receipt-binding"
|
|
621
|
+
? artifact.recordedAt
|
|
622
|
+
: new Date().toISOString(),
|
|
623
|
+
expiresAt,
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function reserveMessagingPrivateOutput(
|
|
628
|
+
path: string,
|
|
629
|
+
artifactFormat: MessagingPrivateOutputArtifact["format"],
|
|
630
|
+
reservationId: string,
|
|
631
|
+
environment: Environment,
|
|
632
|
+
): MessagingPrivateOutputReservationV1 {
|
|
633
|
+
const outputPath = validateMessagingPrivateOutputPath(path, environment);
|
|
634
|
+
const marker = Object.freeze({
|
|
635
|
+
schemaVersion: 1 as const,
|
|
636
|
+
format: "wrench.messaging-private-output-reservation" as const,
|
|
637
|
+
reservationId,
|
|
638
|
+
artifactFormat,
|
|
639
|
+
});
|
|
640
|
+
const reservedContentSha256 = sha256(`${canonicalJson(marker)}\n`);
|
|
641
|
+
let created: boolean;
|
|
642
|
+
try {
|
|
643
|
+
created = createPrivateJsonIfAbsent(
|
|
644
|
+
outputPath,
|
|
645
|
+
marker,
|
|
646
|
+
{ privateParent: true },
|
|
647
|
+
).created;
|
|
648
|
+
} catch {
|
|
649
|
+
throw new Error("messaging private output sink failed physical reservation");
|
|
650
|
+
}
|
|
651
|
+
if (!created) {
|
|
652
|
+
throw new Error("messaging private output sink already exists");
|
|
653
|
+
}
|
|
654
|
+
return Object.freeze({
|
|
655
|
+
path: outputPath,
|
|
656
|
+
artifactFormat,
|
|
657
|
+
reservationId,
|
|
658
|
+
reservedContentSha256,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export function reserveMessagingPrivateOutputPair(
|
|
663
|
+
runPath: string,
|
|
664
|
+
receiptBindingPath: string,
|
|
665
|
+
environment: Environment = process.env,
|
|
666
|
+
): MessagingPrivateOutputReservationPairV1 {
|
|
667
|
+
const exactRunPath = validateMessagingPrivateOutputPath(runPath, environment);
|
|
668
|
+
const exactReceiptBindingPath = validateMessagingPrivateOutputPath(
|
|
669
|
+
receiptBindingPath,
|
|
670
|
+
environment,
|
|
671
|
+
);
|
|
672
|
+
if (exactRunPath === exactReceiptBindingPath) {
|
|
673
|
+
throw new Error(
|
|
674
|
+
"messaging confirmation requires distinct private output and receipt-binding paths",
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
const pairId = `wmoutput_${randomBytes(16).toString("base64url")}`;
|
|
678
|
+
const run = reserveMessagingPrivateOutput(
|
|
679
|
+
exactRunPath,
|
|
680
|
+
"wrench.messaging-run",
|
|
681
|
+
pairId,
|
|
682
|
+
environment,
|
|
683
|
+
);
|
|
684
|
+
let receiptBinding: MessagingPrivateOutputReservationV1;
|
|
685
|
+
try {
|
|
686
|
+
receiptBinding = reserveMessagingPrivateOutput(
|
|
687
|
+
exactReceiptBindingPath,
|
|
688
|
+
"wrench.messaging-receipt-binding",
|
|
689
|
+
pairId,
|
|
690
|
+
environment,
|
|
691
|
+
);
|
|
692
|
+
} catch (error) {
|
|
693
|
+
throw new Error(
|
|
694
|
+
"messaging receipt-binding sink failed physical reservation after the body-free run sink was reserved",
|
|
695
|
+
{ cause: error },
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
return Object.freeze({ run, receiptBinding });
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
export function writeReservedMessagingPrivateOutput(
|
|
702
|
+
reservation: MessagingPrivateOutputReservationV1,
|
|
703
|
+
artifact: MessagingPrivateOutputArtifact,
|
|
704
|
+
_environment: Environment = process.env,
|
|
705
|
+
): MessagingPrivateOutputReceiptV1 {
|
|
706
|
+
if (artifact.format !== reservation.artifactFormat) {
|
|
707
|
+
throw new Error("messaging private output reservation has another artifact format");
|
|
708
|
+
}
|
|
709
|
+
let written: boolean;
|
|
710
|
+
try {
|
|
711
|
+
written = writePrivateJsonIfUnchanged(reservation.path, artifact, {
|
|
712
|
+
expectedCurrentContentSha256: reservation.reservedContentSha256,
|
|
713
|
+
maximumExpectedCurrentBytes: 4_096,
|
|
714
|
+
privateParent: true,
|
|
715
|
+
});
|
|
716
|
+
} catch {
|
|
717
|
+
throw new Error("messaging private output final export failed");
|
|
718
|
+
}
|
|
719
|
+
if (!written) {
|
|
720
|
+
throw new Error(
|
|
721
|
+
"messaging private output reservation changed before final export; recover by run ID",
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
const artifactSha256 = sha256(canonicalJson(artifact));
|
|
725
|
+
const expiresAt = artifact.format === "wrench.messaging-context"
|
|
726
|
+
? artifact.binding?.expiresAt ?? null
|
|
727
|
+
: artifact.format === "wrench.messaging-preview"
|
|
728
|
+
? artifact.expiresAt
|
|
729
|
+
: artifact.format === "wrench.messaging-route"
|
|
730
|
+
? artifact.expiresAt
|
|
731
|
+
: artifact.format === "wrench.messaging-routes"
|
|
732
|
+
? artifact.routes.reduce<string | null>((latest, route) =>
|
|
733
|
+
latest === null || route.expiresAt < latest
|
|
734
|
+
? route.expiresAt
|
|
735
|
+
: latest, null)
|
|
736
|
+
: null;
|
|
737
|
+
return Object.freeze({
|
|
738
|
+
schemaVersion: 1,
|
|
739
|
+
format: "wrench.messaging-private-output-receipt",
|
|
740
|
+
artifactFormat: artifact.format,
|
|
741
|
+
artifactSha256,
|
|
742
|
+
itemCount: artifact.format === "wrench.messaging-routes"
|
|
743
|
+
? artifact.routes.length
|
|
744
|
+
: artifact.format === "wrench.messaging-context"
|
|
745
|
+
? artifact.messages.length
|
|
746
|
+
: artifact.format === "wrench.messaging-preview"
|
|
747
|
+
|| artifact.format === "wrench.messaging-run"
|
|
748
|
+
? artifact.partCount
|
|
749
|
+
: 1,
|
|
750
|
+
generatedAt: artifact.format === "wrench.messaging-routes"
|
|
751
|
+
? artifact.generatedAt
|
|
752
|
+
: artifact.format === "wrench.messaging-context"
|
|
753
|
+
? artifact.binding?.validatedAt ?? new Date().toISOString()
|
|
754
|
+
: artifact.format === "wrench.messaging-run"
|
|
755
|
+
|| artifact.format === "wrench.messaging-receipt-binding"
|
|
756
|
+
? artifact.recordedAt
|
|
757
|
+
: new Date().toISOString(),
|
|
758
|
+
expiresAt,
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
export async function discoverMessagingRoutesInternal(
|
|
763
|
+
value: unknown,
|
|
764
|
+
options: MessagingRuntimeOptions = {},
|
|
765
|
+
): Promise<MessagingRoutesV2> {
|
|
766
|
+
const request = parseMessagingRoutesRequestV1(value);
|
|
767
|
+
const environment = options.environment ?? process.env;
|
|
768
|
+
const registry = options.registry ?? providerPluginRegistry;
|
|
769
|
+
const observation = now(options);
|
|
770
|
+
const generatedAt = observation.toISOString();
|
|
771
|
+
const expiresAt = new Date(observation.getTime() + ROUTE_TTL_MS).toISOString();
|
|
772
|
+
const routes: MessagingRouteV2[] = [];
|
|
773
|
+
const routeIdentities = new Set<string>();
|
|
774
|
+
const source = request.source;
|
|
775
|
+
const invocation = prepareInvocation(
|
|
776
|
+
source.adapterId,
|
|
777
|
+
"messaging.list",
|
|
778
|
+
source.listInput,
|
|
779
|
+
source.authId,
|
|
780
|
+
environment,
|
|
781
|
+
registry,
|
|
782
|
+
);
|
|
783
|
+
const resolution = messagingResolution(invocation, registry, "messaging.list");
|
|
784
|
+
await requireRuntimeReady(resolution.binding, invocation.auth, environment, registry);
|
|
785
|
+
const live = await executeReadInvocation(invocation, {
|
|
786
|
+
headed: false,
|
|
787
|
+
environment,
|
|
788
|
+
registry,
|
|
789
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
790
|
+
});
|
|
791
|
+
if (live.receipt.status !== "succeeded") {
|
|
792
|
+
throw new Error("messaging route discovery live read did not succeed");
|
|
793
|
+
}
|
|
794
|
+
const exactDataRevision = sha256(canonicalJson(live.output));
|
|
795
|
+
const page = exactLivePage(invocation, resolution, live.output);
|
|
796
|
+
const candidates = resolution.messaging.enumerateRoutes(invocation.input, page);
|
|
797
|
+
if (!Array.isArray(candidates) || candidates.length > 1_000) {
|
|
798
|
+
throw new Error("provider messaging route enumeration exceeded its bound");
|
|
799
|
+
}
|
|
800
|
+
const identity = identityFor(invocation, resolution, registry);
|
|
801
|
+
for (const candidate of candidates) {
|
|
802
|
+
const target = resolution.messaging.parseTarget(candidate.target);
|
|
803
|
+
if (
|
|
804
|
+
typeof candidate.conversationProviderId !== "string"
|
|
805
|
+
|| candidate.conversationProviderId.length < 1
|
|
806
|
+
|| candidate.conversationProviderId.length > 4_096
|
|
807
|
+
|| !Array.isArray(candidate.participants)
|
|
808
|
+
|| candidate.participants.length > 10_000
|
|
809
|
+
) throw new Error("provider messaging route candidate is malformed");
|
|
810
|
+
const dedupe = sha256(canonicalJson({
|
|
811
|
+
adapter: identity.adapter,
|
|
812
|
+
binding: identity.binding,
|
|
813
|
+
auth: identity.auth,
|
|
814
|
+
target,
|
|
815
|
+
}));
|
|
816
|
+
if (routeIdentities.has(dedupe)) {
|
|
817
|
+
throw new Error("messaging route page repeated one exact provider conversation");
|
|
818
|
+
}
|
|
819
|
+
routeIdentities.add(dedupe);
|
|
820
|
+
const routeRef = opaque("wmroute");
|
|
821
|
+
const participantFingerprint = sha256(canonicalJson(candidate.participants));
|
|
822
|
+
const routeRecord: MessagingRouteRecordV1 = Object.freeze({
|
|
823
|
+
schemaVersion: 1,
|
|
824
|
+
format: "wrench.messaging-route-record",
|
|
825
|
+
routeRef,
|
|
826
|
+
createdAt: generatedAt,
|
|
827
|
+
expiresAt,
|
|
828
|
+
...identity,
|
|
829
|
+
list: Object.freeze({
|
|
830
|
+
operation: "messaging.list",
|
|
831
|
+
input: invocation.input,
|
|
832
|
+
inputHash: sha256(canonicalJson(invocation.input)),
|
|
833
|
+
exactDataRevision,
|
|
834
|
+
validatedAt: generatedAt,
|
|
835
|
+
}),
|
|
836
|
+
target,
|
|
837
|
+
resolution: "list-candidate",
|
|
838
|
+
network: resolution.messaging.network,
|
|
839
|
+
sourceConversationCoordinate: null,
|
|
840
|
+
conversationProviderId: candidate.conversationProviderId,
|
|
841
|
+
conversation: Object.freeze({
|
|
842
|
+
kind: candidate.conversationKind,
|
|
843
|
+
title: candidate.title,
|
|
844
|
+
participantCount: candidate.participants.length,
|
|
845
|
+
participantFingerprint,
|
|
846
|
+
providerRevision: candidate.providerRevision,
|
|
847
|
+
}),
|
|
848
|
+
});
|
|
849
|
+
saveMessagingRouteRecord(routeRecord, environment);
|
|
850
|
+
routes.push(Object.freeze({
|
|
851
|
+
schemaVersion: 2,
|
|
852
|
+
format: "wrench.messaging-route",
|
|
853
|
+
routeRef,
|
|
854
|
+
network: resolution.messaging.network,
|
|
855
|
+
conversation: Object.freeze({
|
|
856
|
+
kind: candidate.conversationKind,
|
|
857
|
+
title: candidate.title,
|
|
858
|
+
participantCount: candidate.participants.length,
|
|
859
|
+
}),
|
|
860
|
+
readiness: Object.freeze({
|
|
861
|
+
context: "resolution-required",
|
|
862
|
+
turn: "unavailable",
|
|
863
|
+
reply: "unsupported",
|
|
864
|
+
reason:
|
|
865
|
+
"an exact provider coordinate must be resolved before this list candidate can become a context or action capability",
|
|
866
|
+
}),
|
|
867
|
+
completeness: page.completeness,
|
|
868
|
+
expiresAt,
|
|
869
|
+
}));
|
|
870
|
+
}
|
|
871
|
+
return Object.freeze({
|
|
872
|
+
schemaVersion: 2,
|
|
873
|
+
format: "wrench.messaging-routes",
|
|
874
|
+
generatedAt,
|
|
875
|
+
completeness: page.completeness,
|
|
876
|
+
continuation: Object.freeze({
|
|
877
|
+
direction: page.cursor.direction,
|
|
878
|
+
request: page.cursor.request,
|
|
879
|
+
nextInput: page.cursor.nextInput,
|
|
880
|
+
}),
|
|
881
|
+
routes: Object.freeze(routes),
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
export async function resolveMessagingRouteInternal(
|
|
886
|
+
value: unknown,
|
|
887
|
+
options: MessagingRuntimeOptions = {},
|
|
888
|
+
): Promise<MessagingRouteV2> {
|
|
889
|
+
const request = parseMessagingRouteResolveRequestV2(value);
|
|
890
|
+
const environment = options.environment ?? process.env;
|
|
891
|
+
const registry = options.registry ?? providerPluginRegistry;
|
|
892
|
+
const observation = now(options);
|
|
893
|
+
const createdAt = observation.toISOString();
|
|
894
|
+
const expiresAt = new Date(observation.getTime() + ROUTE_TTL_MS).toISOString();
|
|
895
|
+
const listCandidate = loadMessagingRouteRecord(
|
|
896
|
+
request.routeRef,
|
|
897
|
+
environment,
|
|
898
|
+
observation,
|
|
899
|
+
);
|
|
900
|
+
if (listCandidate.resolution !== "list-candidate") {
|
|
901
|
+
throw new Error("messaging route resolution requires one unresolved list candidate");
|
|
902
|
+
}
|
|
903
|
+
const sourceInvocation = prepareInvocation(
|
|
904
|
+
listCandidate.adapter.id,
|
|
905
|
+
listCandidate.list.operation,
|
|
906
|
+
listCandidate.list.input,
|
|
907
|
+
listCandidate.auth.id,
|
|
908
|
+
environment,
|
|
909
|
+
registry,
|
|
910
|
+
);
|
|
911
|
+
const resolution = messagingResolution(
|
|
912
|
+
sourceInvocation,
|
|
913
|
+
registry,
|
|
914
|
+
"messaging.list",
|
|
915
|
+
);
|
|
916
|
+
assertRouteIdentity(
|
|
917
|
+
listCandidate,
|
|
918
|
+
sourceInvocation,
|
|
919
|
+
resolution,
|
|
920
|
+
registry,
|
|
921
|
+
observation,
|
|
922
|
+
);
|
|
923
|
+
await requireRuntimeReady(
|
|
924
|
+
resolution.binding,
|
|
925
|
+
sourceInvocation.auth,
|
|
926
|
+
environment,
|
|
927
|
+
registry,
|
|
928
|
+
);
|
|
929
|
+
const storedTarget = resolution.messaging.parseTarget(listCandidate.target);
|
|
930
|
+
const exactInput = resolution.messaging.resolveRoute.input(
|
|
931
|
+
storedTarget,
|
|
932
|
+
);
|
|
933
|
+
const exactInvocation = prepareInvocation(
|
|
934
|
+
listCandidate.adapter.id,
|
|
935
|
+
resolution.messaging.resolveRoute.operation,
|
|
936
|
+
exactInput,
|
|
937
|
+
listCandidate.auth.id,
|
|
938
|
+
environment,
|
|
939
|
+
registry,
|
|
940
|
+
);
|
|
941
|
+
const exactResolution = operationResolution(exactInvocation, registry);
|
|
942
|
+
if (
|
|
943
|
+
exactResolution.plugin.id !== resolution.plugin.id
|
|
944
|
+
|| exactResolution.binding !== resolution.binding
|
|
945
|
+
|| exactResolution.binding.messaging?.contractId
|
|
946
|
+
!== resolution.messaging.contractId
|
|
947
|
+
|| exactResolution.operation.name
|
|
948
|
+
!== resolution.messaging.resolveRoute.operation
|
|
949
|
+
) throw new Error("exact messaging route resolution changed provider binding");
|
|
950
|
+
const live = await executeReadInvocation(exactInvocation, {
|
|
951
|
+
headed: false,
|
|
952
|
+
environment,
|
|
953
|
+
registry,
|
|
954
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
955
|
+
});
|
|
956
|
+
if (live.receipt.status !== "succeeded") {
|
|
957
|
+
throw new Error("exact messaging route live read did not succeed");
|
|
958
|
+
}
|
|
959
|
+
const exactDataRevision = sha256(canonicalJson(live.output));
|
|
960
|
+
const candidates = resolution.messaging.resolveRoute.candidates(
|
|
961
|
+
storedTarget,
|
|
962
|
+
live.output,
|
|
963
|
+
);
|
|
964
|
+
if (!Array.isArray(candidates) || candidates.length !== 1) {
|
|
965
|
+
throw new Error(
|
|
966
|
+
Array.isArray(candidates) && candidates.length === 0
|
|
967
|
+
? "exact messaging route candidate was not proven by the live provider read"
|
|
968
|
+
: "exact messaging route candidate was ambiguous in the live provider read",
|
|
969
|
+
);
|
|
970
|
+
}
|
|
971
|
+
const candidate = candidates[0]!;
|
|
972
|
+
const target = resolution.messaging.parseTarget(candidate.target);
|
|
973
|
+
if (
|
|
974
|
+
candidate.conversationProviderId.length < 1
|
|
975
|
+
|| candidate.conversationProviderId.length > 4_096
|
|
976
|
+
|| !Array.isArray(candidate.participants)
|
|
977
|
+
|| candidate.participants.length > 10_000
|
|
978
|
+
) throw new Error("provider messaging route candidate is malformed");
|
|
979
|
+
if (
|
|
980
|
+
canonicalJson(target) !== canonicalJson(storedTarget)
|
|
981
|
+
|| candidate.conversationProviderId !== listCandidate.conversationProviderId
|
|
982
|
+
) {
|
|
983
|
+
throw new Error("exact messaging route read returned another conversation");
|
|
984
|
+
}
|
|
985
|
+
const identity = identityFor(sourceInvocation, resolution, registry);
|
|
986
|
+
const sourceConversationCoordinate =
|
|
987
|
+
resolution.messaging.resolveRoute.sourceConversationCoordinate(
|
|
988
|
+
storedTarget,
|
|
989
|
+
live.output,
|
|
990
|
+
identity.auth.subject,
|
|
991
|
+
);
|
|
992
|
+
const network = resolution.messaging.network;
|
|
993
|
+
const routeRef = opaque("wmroute");
|
|
994
|
+
const routeRecord: MessagingRouteRecordV1 = Object.freeze({
|
|
995
|
+
schemaVersion: 1,
|
|
996
|
+
format: "wrench.messaging-route-record",
|
|
997
|
+
routeRef,
|
|
998
|
+
createdAt,
|
|
999
|
+
expiresAt,
|
|
1000
|
+
...identity,
|
|
1001
|
+
list: Object.freeze({
|
|
1002
|
+
operation: "messaging.list",
|
|
1003
|
+
input: sourceInvocation.input,
|
|
1004
|
+
inputHash: sha256(canonicalJson(sourceInvocation.input)),
|
|
1005
|
+
exactDataRevision,
|
|
1006
|
+
validatedAt: createdAt,
|
|
1007
|
+
}),
|
|
1008
|
+
target,
|
|
1009
|
+
resolution: "exact-coordinate",
|
|
1010
|
+
network,
|
|
1011
|
+
sourceConversationCoordinate,
|
|
1012
|
+
conversationProviderId: candidate.conversationProviderId,
|
|
1013
|
+
conversation: Object.freeze({
|
|
1014
|
+
kind: candidate.conversationKind,
|
|
1015
|
+
title: candidate.title,
|
|
1016
|
+
participantCount: candidate.participants.length,
|
|
1017
|
+
participantFingerprint: sha256(canonicalJson(candidate.participants)),
|
|
1018
|
+
providerRevision: candidate.providerRevision,
|
|
1019
|
+
}),
|
|
1020
|
+
});
|
|
1021
|
+
saveMessagingRouteRecord(routeRecord, environment);
|
|
1022
|
+
const action = resolution.messaging.action;
|
|
1023
|
+
return Object.freeze({
|
|
1024
|
+
schemaVersion: 2,
|
|
1025
|
+
format: "wrench.messaging-route",
|
|
1026
|
+
routeRef,
|
|
1027
|
+
network,
|
|
1028
|
+
conversation: Object.freeze({
|
|
1029
|
+
kind: candidate.conversationKind,
|
|
1030
|
+
title: candidate.title,
|
|
1031
|
+
participantCount: candidate.participants.length,
|
|
1032
|
+
}),
|
|
1033
|
+
readiness: Object.freeze({
|
|
1034
|
+
context: resolution.messaging.contextLiveness === "fresh-as-of-live-preflight"
|
|
1035
|
+
? "ready"
|
|
1036
|
+
: "historical-readable",
|
|
1037
|
+
turn:
|
|
1038
|
+
sourceConversationCoordinate !== null
|
|
1039
|
+
&&
|
|
1040
|
+
action.state === "supported"
|
|
1041
|
+
&& resolution.messaging.contextLiveness === "fresh-as-of-live-preflight"
|
|
1042
|
+
? "ready"
|
|
1043
|
+
: "unavailable",
|
|
1044
|
+
reply:
|
|
1045
|
+
sourceConversationCoordinate !== null
|
|
1046
|
+
&&
|
|
1047
|
+
action.state === "supported"
|
|
1048
|
+
&& resolution.messaging.contextLiveness === "fresh-as-of-live-preflight"
|
|
1049
|
+
? action.reply
|
|
1050
|
+
: "unsupported",
|
|
1051
|
+
reason: resolution.messaging.contextLiveness === "freshness-unproven"
|
|
1052
|
+
? "provider context freshness is unproven"
|
|
1053
|
+
: action.state === "supported"
|
|
1054
|
+
? sourceConversationCoordinate === null
|
|
1055
|
+
? "this exact route is not eligible for checked turn actions"
|
|
1056
|
+
: null
|
|
1057
|
+
: action.reason,
|
|
1058
|
+
}),
|
|
1059
|
+
completeness: Object.freeze({
|
|
1060
|
+
kind: "complete",
|
|
1061
|
+
reason: "provider-native exact read proved the stored route candidate",
|
|
1062
|
+
}),
|
|
1063
|
+
expiresAt,
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
async function resolveCurrentRoute(
|
|
1068
|
+
routeRef: string,
|
|
1069
|
+
observation: Date,
|
|
1070
|
+
environment: Environment,
|
|
1071
|
+
registry: ProviderPluginRegistry,
|
|
1072
|
+
): Promise<{
|
|
1073
|
+
readonly record: MessagingRouteRecordV1;
|
|
1074
|
+
readonly resolution: MessagingResolution;
|
|
1075
|
+
}> {
|
|
1076
|
+
const record = loadMessagingRouteRecord(routeRef, environment, observation);
|
|
1077
|
+
if (record.resolution !== "exact-coordinate") {
|
|
1078
|
+
throw new Error(
|
|
1079
|
+
"messaging route requires exact conversations.read resolution before context or action use",
|
|
1080
|
+
);
|
|
1081
|
+
}
|
|
1082
|
+
const invocation = prepareInvocation(
|
|
1083
|
+
record.adapter.id,
|
|
1084
|
+
record.list.operation,
|
|
1085
|
+
record.list.input,
|
|
1086
|
+
record.auth.id,
|
|
1087
|
+
environment,
|
|
1088
|
+
registry,
|
|
1089
|
+
);
|
|
1090
|
+
const resolution = messagingResolution(invocation, registry, "messaging.list");
|
|
1091
|
+
assertRouteIdentity(record, invocation, resolution, registry, observation);
|
|
1092
|
+
await requireRuntimeReady(resolution.binding, invocation.auth, environment, registry);
|
|
1093
|
+
return Object.freeze({ record, resolution });
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
async function currentContextPage(
|
|
1097
|
+
record: MessagingRouteRecordV1,
|
|
1098
|
+
resolution: MessagingResolution,
|
|
1099
|
+
limit: number,
|
|
1100
|
+
environment: Environment,
|
|
1101
|
+
registry: ProviderPluginRegistry,
|
|
1102
|
+
signal?: AbortSignal,
|
|
1103
|
+
): Promise<{
|
|
1104
|
+
readonly exactDataRevision: string;
|
|
1105
|
+
readonly latestMessageRevision: string;
|
|
1106
|
+
readonly page: ProviderMaterializedPageV1;
|
|
1107
|
+
readonly messages: readonly ProviderMessageV1[];
|
|
1108
|
+
}> {
|
|
1109
|
+
const target = resolution.messaging.parseTarget(record.target);
|
|
1110
|
+
const input = resolution.messaging.contextInput(target, limit);
|
|
1111
|
+
const invocation = prepareInvocation(
|
|
1112
|
+
record.adapter.id,
|
|
1113
|
+
resolution.messaging.contextOperation,
|
|
1114
|
+
input,
|
|
1115
|
+
record.auth.id,
|
|
1116
|
+
environment,
|
|
1117
|
+
registry,
|
|
1118
|
+
);
|
|
1119
|
+
const contextResolution = messagingResolution(invocation, registry, "messaging.read");
|
|
1120
|
+
if (
|
|
1121
|
+
contextResolution.plugin.id !== resolution.plugin.id
|
|
1122
|
+
|| contextResolution.binding !== resolution.binding
|
|
1123
|
+
|| contextResolution.messaging.contractId !== resolution.messaging.contractId
|
|
1124
|
+
) throw new Error("messaging context operation changed provider route");
|
|
1125
|
+
const live = await executeReadInvocation(invocation, {
|
|
1126
|
+
headed: false,
|
|
1127
|
+
environment,
|
|
1128
|
+
registry,
|
|
1129
|
+
...(signal === undefined ? {} : { signal }),
|
|
1130
|
+
});
|
|
1131
|
+
if (live.receipt.status !== "succeeded") {
|
|
1132
|
+
throw new Error("messaging context live preflight did not succeed");
|
|
1133
|
+
}
|
|
1134
|
+
const exactDataRevision = sha256(canonicalJson(live.output));
|
|
1135
|
+
const page = exactLivePage(invocation, contextResolution, live.output);
|
|
1136
|
+
const messages = pageMessages(page, record.conversationProviderId);
|
|
1137
|
+
return Object.freeze({
|
|
1138
|
+
exactDataRevision,
|
|
1139
|
+
latestMessageRevision: latestRevision(
|
|
1140
|
+
resolution.binding.surfaceId,
|
|
1141
|
+
record.conversationProviderId,
|
|
1142
|
+
messages,
|
|
1143
|
+
exactDataRevision,
|
|
1144
|
+
),
|
|
1145
|
+
page,
|
|
1146
|
+
messages,
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
async function currentMessagingRouteState(
|
|
1151
|
+
record: MessagingRouteRecordV1,
|
|
1152
|
+
resolution: MessagingResolution,
|
|
1153
|
+
environment: Environment,
|
|
1154
|
+
registry: ProviderPluginRegistry,
|
|
1155
|
+
signal?: AbortSignal,
|
|
1156
|
+
): Promise<{
|
|
1157
|
+
readonly revision: string;
|
|
1158
|
+
readonly recipient: MessagingPreviewV1["recipient"];
|
|
1159
|
+
}> {
|
|
1160
|
+
const action = resolution.messaging.action;
|
|
1161
|
+
if (action.state !== "supported") {
|
|
1162
|
+
throw new Error("messaging route does not support an actionable live preflight");
|
|
1163
|
+
}
|
|
1164
|
+
const target = resolution.messaging.parseTarget(record.target);
|
|
1165
|
+
const invocation = prepareInvocation(
|
|
1166
|
+
record.adapter.id,
|
|
1167
|
+
action.livePreflight.operation,
|
|
1168
|
+
action.livePreflight.input(target),
|
|
1169
|
+
record.auth.id,
|
|
1170
|
+
environment,
|
|
1171
|
+
registry,
|
|
1172
|
+
);
|
|
1173
|
+
const liveResolution = operationResolution(invocation, registry);
|
|
1174
|
+
if (
|
|
1175
|
+
liveResolution.binding !== resolution.binding
|
|
1176
|
+
|| liveResolution.plugin.id !== resolution.plugin.id
|
|
1177
|
+
) throw new Error("messaging live preflight changed provider binding");
|
|
1178
|
+
const live = await executeReadInvocation(invocation, {
|
|
1179
|
+
headed: false,
|
|
1180
|
+
environment,
|
|
1181
|
+
registry,
|
|
1182
|
+
...(signal === undefined ? {} : { signal }),
|
|
1183
|
+
});
|
|
1184
|
+
if (live.receipt.status !== "succeeded") {
|
|
1185
|
+
throw new Error("messaging live route preflight did not succeed");
|
|
1186
|
+
}
|
|
1187
|
+
const snapshot = action.livePreflight.snapshot(
|
|
1188
|
+
live.output,
|
|
1189
|
+
record.auth.subject,
|
|
1190
|
+
);
|
|
1191
|
+
const sourceConversationCoordinate =
|
|
1192
|
+
parseMessageLikeMeSourceConversationCoordinateBindingV1(
|
|
1193
|
+
snapshot.sourceConversationCoordinate,
|
|
1194
|
+
);
|
|
1195
|
+
if (
|
|
1196
|
+
typeof snapshot.conversationProviderId !== "string"
|
|
1197
|
+
|| snapshot.conversationProviderId !== record.conversationProviderId
|
|
1198
|
+
|| typeof snapshot.network !== "string"
|
|
1199
|
+
|| !/^[a-z][a-z0-9-]{0,63}$/u.test(snapshot.network)
|
|
1200
|
+
|| snapshot.conversation.kind !== "single"
|
|
1201
|
+
&& snapshot.conversation.kind !== "group"
|
|
1202
|
+
&& snapshot.conversation.kind !== "unknown"
|
|
1203
|
+
|| snapshot.conversation.title !== null
|
|
1204
|
+
&& (typeof snapshot.conversation.title !== "string"
|
|
1205
|
+
|| Buffer.byteLength(snapshot.conversation.title, "utf8") > 4_096
|
|
1206
|
+
|| /[\0\r\n]/u.test(snapshot.conversation.title))
|
|
1207
|
+
|| typeof snapshot.conversation.participantCount !== "number"
|
|
1208
|
+
|| !Number.isSafeInteger(snapshot.conversation.participantCount)
|
|
1209
|
+
|| snapshot.conversation.participantCount < 0
|
|
1210
|
+
|| snapshot.conversation.participantCount > 10_000
|
|
1211
|
+
|| typeof snapshot.participantFingerprint !== "string"
|
|
1212
|
+
|| !/^[a-f0-9]{64}$/u.test(snapshot.participantFingerprint)
|
|
1213
|
+
|| snapshot.providerRevision !== null
|
|
1214
|
+
&& (typeof snapshot.providerRevision !== "string"
|
|
1215
|
+
|| snapshot.providerRevision.length < 1
|
|
1216
|
+
|| Buffer.byteLength(snapshot.providerRevision, "utf8") > 4_096
|
|
1217
|
+
|| /[\0\r\n]/u.test(snapshot.providerRevision))
|
|
1218
|
+
) throw new Error("messaging live route preflight returned malformed identity state");
|
|
1219
|
+
if (
|
|
1220
|
+
snapshot.network !== record.network
|
|
1221
|
+
|| snapshot.conversation.kind !== record.conversation.kind
|
|
1222
|
+
|| snapshot.conversation.title !== record.conversation.title
|
|
1223
|
+
|| snapshot.conversation.participantCount
|
|
1224
|
+
!== record.conversation.participantCount
|
|
1225
|
+
|| sourceConversationCoordinate.sha256
|
|
1226
|
+
!== record.sourceConversationCoordinate?.sha256
|
|
1227
|
+
|| snapshot.participantFingerprint !== record.conversation.participantFingerprint
|
|
1228
|
+
|| snapshot.providerRevision !== record.conversation.providerRevision
|
|
1229
|
+
) throw new Error("messaging route recipient or provider state changed; resolve a new route");
|
|
1230
|
+
const recipient = Object.freeze({
|
|
1231
|
+
network: snapshot.network,
|
|
1232
|
+
conversation: Object.freeze({
|
|
1233
|
+
kind: snapshot.conversation.kind,
|
|
1234
|
+
title: snapshot.conversation.title,
|
|
1235
|
+
participantCount: snapshot.conversation.participantCount,
|
|
1236
|
+
}),
|
|
1237
|
+
});
|
|
1238
|
+
return Object.freeze({
|
|
1239
|
+
revision: sha256(canonicalJson({
|
|
1240
|
+
conversationProviderId: snapshot.conversationProviderId,
|
|
1241
|
+
recipient,
|
|
1242
|
+
sourceConversationCoordinate,
|
|
1243
|
+
participantFingerprint: snapshot.participantFingerprint,
|
|
1244
|
+
providerRevision: snapshot.providerRevision,
|
|
1245
|
+
})),
|
|
1246
|
+
recipient,
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
export async function readMessagingContextInternal(
|
|
1251
|
+
value: unknown,
|
|
1252
|
+
options: MessagingRuntimeOptions = {},
|
|
1253
|
+
): Promise<MessagingContextV1> {
|
|
1254
|
+
const request = parseMessagingContextRequestV1(value);
|
|
1255
|
+
const environment = options.environment ?? process.env;
|
|
1256
|
+
const registry = options.registry ?? providerPluginRegistry;
|
|
1257
|
+
const observation = now(options);
|
|
1258
|
+
const { record, resolution } = await resolveCurrentRoute(
|
|
1259
|
+
request.routeRef,
|
|
1260
|
+
observation,
|
|
1261
|
+
environment,
|
|
1262
|
+
registry,
|
|
1263
|
+
);
|
|
1264
|
+
if (
|
|
1265
|
+
record.sourceConversationCoordinate !== null
|
|
1266
|
+
&& resolution.messaging.action.state === "supported"
|
|
1267
|
+
) {
|
|
1268
|
+
await currentMessagingRouteState(
|
|
1269
|
+
record,
|
|
1270
|
+
resolution,
|
|
1271
|
+
environment,
|
|
1272
|
+
registry,
|
|
1273
|
+
options.signal,
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
1276
|
+
const current = await currentContextPage(
|
|
1277
|
+
record,
|
|
1278
|
+
resolution,
|
|
1279
|
+
request.limit,
|
|
1280
|
+
environment,
|
|
1281
|
+
registry,
|
|
1282
|
+
options.signal,
|
|
1283
|
+
);
|
|
1284
|
+
const validationObservation = options.now ?? new Date();
|
|
1285
|
+
if (!Number.isFinite(validationObservation.getTime())) {
|
|
1286
|
+
throw new Error("messaging live-preflight completion time is invalid");
|
|
1287
|
+
}
|
|
1288
|
+
const validatedAt = validationObservation.toISOString();
|
|
1289
|
+
const expiresAt = new Date(
|
|
1290
|
+
validationObservation.getTime() + CONTEXT_TTL_MS,
|
|
1291
|
+
).toISOString();
|
|
1292
|
+
const contextRef = opaque("wmcontext");
|
|
1293
|
+
const references = randomRefMap(current.messages);
|
|
1294
|
+
const binding: MessagingContextBindingV2 | null =
|
|
1295
|
+
record.sourceConversationCoordinate === null
|
|
1296
|
+
? null
|
|
1297
|
+
: parseMessagingContextBindingV2({
|
|
1298
|
+
schemaVersion: 2,
|
|
1299
|
+
format: "wrench.messaging-context-binding",
|
|
1300
|
+
contractId: MESSAGING_CONTEXT_BINDING_CONTRACT_ID,
|
|
1301
|
+
contractHash: MESSAGING_CONTEXT_BINDING_CONTRACT_HASH,
|
|
1302
|
+
sourceConversationCoordinate: record.sourceConversationCoordinate,
|
|
1303
|
+
routeRef: record.routeRef,
|
|
1304
|
+
contextRef,
|
|
1305
|
+
exactDataRevision: current.exactDataRevision,
|
|
1306
|
+
latestMessageRevision: current.latestMessageRevision,
|
|
1307
|
+
validatedAt,
|
|
1308
|
+
expiresAt,
|
|
1309
|
+
});
|
|
1310
|
+
if (binding !== null) {
|
|
1311
|
+
const contextRecord: MessagingContextRecordV1 = Object.freeze({
|
|
1312
|
+
schemaVersion: 1,
|
|
1313
|
+
format: "wrench.messaging-context-record",
|
|
1314
|
+
contextRef,
|
|
1315
|
+
routeRef: record.routeRef,
|
|
1316
|
+
routeRecordHash: messagingRouteRecordHash(record),
|
|
1317
|
+
sourceConversationCoordinate: binding.sourceConversationCoordinate,
|
|
1318
|
+
exactDataRevision: current.exactDataRevision,
|
|
1319
|
+
latestMessageRevision: current.latestMessageRevision,
|
|
1320
|
+
validatedAt,
|
|
1321
|
+
expiresAt,
|
|
1322
|
+
limit: request.limit,
|
|
1323
|
+
liveness: resolution.messaging.contextLiveness,
|
|
1324
|
+
replyTargets: references.replyTargets,
|
|
1325
|
+
});
|
|
1326
|
+
saveMessagingContextRecord(contextRecord, environment);
|
|
1327
|
+
}
|
|
1328
|
+
return Object.freeze({
|
|
1329
|
+
schemaVersion: 1,
|
|
1330
|
+
format: "wrench.messaging-context",
|
|
1331
|
+
binding,
|
|
1332
|
+
network: record.network,
|
|
1333
|
+
liveness: resolution.messaging.contextLiveness,
|
|
1334
|
+
truncated:
|
|
1335
|
+
current.page.completeness.kind !== "complete"
|
|
1336
|
+
|| current.messages.some((message) => message.bodyTruncated === true),
|
|
1337
|
+
completeness: current.page.completeness,
|
|
1338
|
+
messages: publicMessages(current.messages, references.byProvider),
|
|
1339
|
+
warnings: Object.freeze([
|
|
1340
|
+
...(resolution.messaging.contextLiveness === "fresh-as-of-live-preflight"
|
|
1341
|
+
? ["fresh-as-of-live-preflight-only"]
|
|
1342
|
+
: []),
|
|
1343
|
+
...(resolution.messaging.contextLiveness === "freshness-unproven"
|
|
1344
|
+
? ["provider-freshness-unproven-action-blocked"]
|
|
1345
|
+
: []),
|
|
1346
|
+
...(current.page.completeness.kind === "complete"
|
|
1347
|
+
? []
|
|
1348
|
+
: ["provider-history-incomplete"]),
|
|
1349
|
+
]),
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
export async function previewMessagingTurnInternal(
|
|
1354
|
+
value: unknown,
|
|
1355
|
+
options: MessagingRuntimeOptions = {},
|
|
1356
|
+
): Promise<MessagingPreviewV1> {
|
|
1357
|
+
const turn = parseMessagingTurnV1(value);
|
|
1358
|
+
const environment = options.environment ?? process.env;
|
|
1359
|
+
const registry = options.registry ?? providerPluginRegistry;
|
|
1360
|
+
const observation = now(options);
|
|
1361
|
+
const { record, resolution } = await resolveCurrentRoute(
|
|
1362
|
+
turn.routeRef,
|
|
1363
|
+
observation,
|
|
1364
|
+
environment,
|
|
1365
|
+
registry,
|
|
1366
|
+
);
|
|
1367
|
+
if (record.sourceConversationCoordinate === null) {
|
|
1368
|
+
throw new Error("messaging route is not eligible for checked turn actions");
|
|
1369
|
+
}
|
|
1370
|
+
if (resolution.messaging.action.state !== "supported") {
|
|
1371
|
+
throw new Error("messaging route does not support checked turn actions");
|
|
1372
|
+
}
|
|
1373
|
+
const action = resolution.messaging.action;
|
|
1374
|
+
const context = loadMessagingContextRecord(
|
|
1375
|
+
turn.contextRef,
|
|
1376
|
+
environment,
|
|
1377
|
+
observation,
|
|
1378
|
+
);
|
|
1379
|
+
if (
|
|
1380
|
+
context.routeRef !== record.routeRef
|
|
1381
|
+
|| context.routeRecordHash !== messagingRouteRecordHash(record)
|
|
1382
|
+
|| context.sourceConversationCoordinate === null
|
|
1383
|
+
|| context.sourceConversationCoordinate.sha256
|
|
1384
|
+
!== record.sourceConversationCoordinate.sha256
|
|
1385
|
+
|| Date.parse(context.expiresAt) <= observation.getTime()
|
|
1386
|
+
) throw new Error("messaging context is stale or belongs to another route");
|
|
1387
|
+
if (context.liveness !== "fresh-as-of-live-preflight") {
|
|
1388
|
+
throw new Error("messaging context freshness is unproven; no action preview is allowed");
|
|
1389
|
+
}
|
|
1390
|
+
const current = await currentContextPage(
|
|
1391
|
+
record,
|
|
1392
|
+
resolution,
|
|
1393
|
+
context.limit,
|
|
1394
|
+
environment,
|
|
1395
|
+
registry,
|
|
1396
|
+
options.signal,
|
|
1397
|
+
);
|
|
1398
|
+
if (
|
|
1399
|
+
current.exactDataRevision !== context.exactDataRevision
|
|
1400
|
+
|| current.latestMessageRevision !== context.latestMessageRevision
|
|
1401
|
+
) throw new Error("messaging context changed; read a new context before preview");
|
|
1402
|
+
const exactContextBinding = parseMessagingContextBindingV2({
|
|
1403
|
+
schemaVersion: 2,
|
|
1404
|
+
format: "wrench.messaging-context-binding",
|
|
1405
|
+
contractId: MESSAGING_CONTEXT_BINDING_CONTRACT_ID,
|
|
1406
|
+
contractHash: MESSAGING_CONTEXT_BINDING_CONTRACT_HASH,
|
|
1407
|
+
sourceConversationCoordinate: context.sourceConversationCoordinate,
|
|
1408
|
+
routeRef: context.routeRef,
|
|
1409
|
+
contextRef: context.contextRef,
|
|
1410
|
+
exactDataRevision: context.exactDataRevision,
|
|
1411
|
+
latestMessageRevision: context.latestMessageRevision,
|
|
1412
|
+
validatedAt: context.validatedAt,
|
|
1413
|
+
expiresAt: context.expiresAt,
|
|
1414
|
+
});
|
|
1415
|
+
const contextBindingSha256 =
|
|
1416
|
+
ghostgetMessagingContextBindingSha256V2(exactContextBinding);
|
|
1417
|
+
const baseRouteState = await currentMessagingRouteState(
|
|
1418
|
+
record,
|
|
1419
|
+
resolution,
|
|
1420
|
+
environment,
|
|
1421
|
+
registry,
|
|
1422
|
+
options.signal,
|
|
1423
|
+
);
|
|
1424
|
+
const target = resolution.messaging.parseTarget(record.target);
|
|
1425
|
+
const plannedParts = turn.parts.map((part) => {
|
|
1426
|
+
const replyToProviderId = part.replyRef === null
|
|
1427
|
+
? null
|
|
1428
|
+
: context.replyTargets[part.replyRef];
|
|
1429
|
+
if (part.replyRef !== null && replyToProviderId === undefined) {
|
|
1430
|
+
throw new Error("messaging reply reference is not part of the bound context");
|
|
1431
|
+
}
|
|
1432
|
+
if (replyToProviderId !== null && action.reply !== "supported") {
|
|
1433
|
+
throw new Error("messaging route does not support exact replies");
|
|
1434
|
+
}
|
|
1435
|
+
const input: OperationInput = action.compileTurnPart(
|
|
1436
|
+
target,
|
|
1437
|
+
Object.freeze({
|
|
1438
|
+
partId: part.partId,
|
|
1439
|
+
text: part.text,
|
|
1440
|
+
replyToProviderId: replyToProviderId ?? null,
|
|
1441
|
+
}),
|
|
1442
|
+
);
|
|
1443
|
+
const invocation = prepareInvocation(
|
|
1444
|
+
record.adapter.id,
|
|
1445
|
+
action.operation,
|
|
1446
|
+
input,
|
|
1447
|
+
record.auth.id,
|
|
1448
|
+
environment,
|
|
1449
|
+
registry,
|
|
1450
|
+
);
|
|
1451
|
+
const actionResolution = messagingResolution(invocation, registry, "action");
|
|
1452
|
+
if (
|
|
1453
|
+
actionResolution.binding !== resolution.binding
|
|
1454
|
+
|| actionResolution.plugin.id !== resolution.plugin.id
|
|
1455
|
+
|| actionResolution.messaging.contractId !== resolution.messaging.contractId
|
|
1456
|
+
) throw new Error("messaging action changed provider route");
|
|
1457
|
+
return Object.freeze({
|
|
1458
|
+
partId: part.partId,
|
|
1459
|
+
text: part.text,
|
|
1460
|
+
replyRef: part.replyRef,
|
|
1461
|
+
replyToProviderId: replyToProviderId ?? null,
|
|
1462
|
+
invocation,
|
|
1463
|
+
});
|
|
1464
|
+
});
|
|
1465
|
+
const initial = createMessagingCompositeInvocationPlan(
|
|
1466
|
+
plannedParts,
|
|
1467
|
+
Object.freeze({
|
|
1468
|
+
routeRef: record.routeRef,
|
|
1469
|
+
contextRef: context.contextRef,
|
|
1470
|
+
clientIntentSha256: turn.clientIntentSha256,
|
|
1471
|
+
contextBindingSha256,
|
|
1472
|
+
sourceConversationCoordinateSha256:
|
|
1473
|
+
exactContextBinding.sourceConversationCoordinate.sha256,
|
|
1474
|
+
turnDigest: messagingTurnDigest(turn),
|
|
1475
|
+
contextLimit: context.limit,
|
|
1476
|
+
baseExactDataRevision: context.exactDataRevision,
|
|
1477
|
+
baseLatestMessageRevision: context.latestMessageRevision,
|
|
1478
|
+
baseRouteStateRevision: baseRouteState.revision,
|
|
1479
|
+
baseMessages: messagingBaseMessages(current.messages),
|
|
1480
|
+
recipient: baseRouteState.recipient,
|
|
1481
|
+
}),
|
|
1482
|
+
observation,
|
|
1483
|
+
registry,
|
|
1484
|
+
);
|
|
1485
|
+
const initialComposite = initial.plan.messagingComposite;
|
|
1486
|
+
if (initialComposite === undefined) throw new Error("messaging composite plan disappeared");
|
|
1487
|
+
const preview: MessagingPreviewV1 = Object.freeze({
|
|
1488
|
+
schemaVersion: 1,
|
|
1489
|
+
format: "wrench.messaging-preview",
|
|
1490
|
+
status: "confirmation-required",
|
|
1491
|
+
planDigest: initial.digest,
|
|
1492
|
+
expiresAt: initial.plan.expiresAt,
|
|
1493
|
+
routeRef: record.routeRef,
|
|
1494
|
+
contextRef: context.contextRef,
|
|
1495
|
+
clientIntentSha256: turn.clientIntentSha256,
|
|
1496
|
+
turnDigest: messagingTurnDigest(turn),
|
|
1497
|
+
recipient: initialComposite.recipient,
|
|
1498
|
+
partCount: turn.parts.length,
|
|
1499
|
+
bubbles: Object.freeze(turn.parts.map((part) => Object.freeze({
|
|
1500
|
+
partId: part.partId,
|
|
1501
|
+
text: part.text,
|
|
1502
|
+
replyRef: part.replyRef,
|
|
1503
|
+
}))),
|
|
1504
|
+
risk: "R3",
|
|
1505
|
+
sideEffect: initial.plan.sideEffect,
|
|
1506
|
+
});
|
|
1507
|
+
const stored = bindMessagingCompositePreviewDigest(
|
|
1508
|
+
initial,
|
|
1509
|
+
sha256(canonicalJson(preview)),
|
|
1510
|
+
);
|
|
1511
|
+
if (invocationPlanDigest(stored.plan) !== preview.planDigest) {
|
|
1512
|
+
throw new Error("messaging preview changed its confirmation digest");
|
|
1513
|
+
}
|
|
1514
|
+
saveInvocationPlan(stored, environment);
|
|
1515
|
+
return preview;
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
export function previewFromStoredMessagingPlan(stored: StoredPlan): MessagingPreviewV1 {
|
|
1519
|
+
const composite = stored.plan.messagingComposite;
|
|
1520
|
+
if (composite === undefined) throw new Error("confirmation plan is not a messaging composite");
|
|
1521
|
+
return Object.freeze({
|
|
1522
|
+
schemaVersion: 1,
|
|
1523
|
+
format: "wrench.messaging-preview",
|
|
1524
|
+
status: "confirmation-required",
|
|
1525
|
+
planDigest: stored.digest,
|
|
1526
|
+
expiresAt: stored.plan.expiresAt,
|
|
1527
|
+
routeRef: composite.routeRef,
|
|
1528
|
+
contextRef: composite.contextRef,
|
|
1529
|
+
clientIntentSha256: composite.clientIntentSha256,
|
|
1530
|
+
turnDigest: composite.turnDigest,
|
|
1531
|
+
recipient: composite.recipient,
|
|
1532
|
+
partCount: composite.parts.length,
|
|
1533
|
+
bubbles: Object.freeze(composite.parts.map((part) => Object.freeze({
|
|
1534
|
+
partId: part.partId,
|
|
1535
|
+
text: part.text,
|
|
1536
|
+
replyRef: part.replyRef,
|
|
1537
|
+
}))),
|
|
1538
|
+
risk: "R3",
|
|
1539
|
+
sideEffect: stored.plan.sideEffect,
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
export function verifyStoredMessagingPreview(stored: StoredPlan): MessagingPreviewV1 {
|
|
1544
|
+
const composite = stored.plan.messagingComposite;
|
|
1545
|
+
if (composite === undefined) throw new Error("confirmation plan is not a messaging composite");
|
|
1546
|
+
const preview = previewFromStoredMessagingPlan(stored);
|
|
1547
|
+
if (sha256(canonicalJson(preview)) !== composite.previewDigest) {
|
|
1548
|
+
throw new Error("messaging preview digest does not match the exact reconstructed artifact");
|
|
1549
|
+
}
|
|
1550
|
+
return preview;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
export function initializeMessagingCompositeRunInternal(
|
|
1554
|
+
stored: StoredPlan,
|
|
1555
|
+
runId: string,
|
|
1556
|
+
options: Pick<MessagingRuntimeOptions, "environment" | "now"> = {},
|
|
1557
|
+
): MessagingRunSnapshotV1 {
|
|
1558
|
+
const composite = stored.plan.messagingComposite;
|
|
1559
|
+
if (composite === undefined) {
|
|
1560
|
+
throw new Error("confirmation plan is not a messaging composite");
|
|
1561
|
+
}
|
|
1562
|
+
if (
|
|
1563
|
+
composite.contextBindingSha256 === null
|
|
1564
|
+
|| composite.sourceConversationCoordinateSha256 === null
|
|
1565
|
+
) {
|
|
1566
|
+
throw new Error(
|
|
1567
|
+
"predecessor messaging plan lacks current context evidence; preview the action again",
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
verifyStoredMessagingPreview(stored);
|
|
1571
|
+
return initializeMessagingRun(
|
|
1572
|
+
runId,
|
|
1573
|
+
stored.digest,
|
|
1574
|
+
composite,
|
|
1575
|
+
options.environment ?? process.env,
|
|
1576
|
+
now(options).toISOString(),
|
|
1577
|
+
);
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
function messagingTransitionTime(
|
|
1581
|
+
snapshot: MessagingRunSnapshotV1,
|
|
1582
|
+
options: MessagingRuntimeOptions,
|
|
1583
|
+
): string {
|
|
1584
|
+
const observed = now(options).toISOString();
|
|
1585
|
+
return Date.parse(observed) < Date.parse(snapshot.run.recordedAt)
|
|
1586
|
+
? snapshot.run.recordedAt
|
|
1587
|
+
: observed;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
function stopMessagingBeforeDispatch(
|
|
1591
|
+
snapshot: MessagingRunSnapshotV1,
|
|
1592
|
+
reason:
|
|
1593
|
+
| "context-drift"
|
|
1594
|
+
| "prefix-freshness-unproven"
|
|
1595
|
+
| "provider-failed-before-dispatch"
|
|
1596
|
+
| "journal-recovery-required",
|
|
1597
|
+
options: MessagingRuntimeOptions,
|
|
1598
|
+
): MessagingRunSnapshotV1 {
|
|
1599
|
+
return updateMessagingRun(snapshot, {
|
|
1600
|
+
type: "categorical-stop",
|
|
1601
|
+
index: snapshot.run.provenPartCount,
|
|
1602
|
+
partState: snapshot.run.provenPartCount === 0
|
|
1603
|
+
? "failed-before-dispatch"
|
|
1604
|
+
: "failed-permanent",
|
|
1605
|
+
reason,
|
|
1606
|
+
at: messagingTransitionTime(snapshot, options),
|
|
1607
|
+
}, options.environment ?? process.env);
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
function stopMessagingIndeterminate(
|
|
1611
|
+
snapshot: MessagingRunSnapshotV1,
|
|
1612
|
+
reason: "provider-result-indeterminate" | "journal-recovery-required",
|
|
1613
|
+
options: MessagingRuntimeOptions,
|
|
1614
|
+
privateProviderOutcome: MessagingPrivateProviderOutcomeV1 | null = null,
|
|
1615
|
+
): MessagingRunSnapshotV1 {
|
|
1616
|
+
return updateMessagingRun(
|
|
1617
|
+
snapshot,
|
|
1618
|
+
reason === "provider-result-indeterminate"
|
|
1619
|
+
? {
|
|
1620
|
+
type: "indeterminate",
|
|
1621
|
+
index: snapshot.run.provenPartCount,
|
|
1622
|
+
reason,
|
|
1623
|
+
privateProviderOutcome,
|
|
1624
|
+
at: messagingTransitionTime(snapshot, options),
|
|
1625
|
+
}
|
|
1626
|
+
: {
|
|
1627
|
+
type: "indeterminate",
|
|
1628
|
+
index: snapshot.run.provenPartCount,
|
|
1629
|
+
reason,
|
|
1630
|
+
at: messagingTransitionTime(snapshot, options),
|
|
1631
|
+
},
|
|
1632
|
+
options.environment ?? process.env,
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
function stopMessagingAfterError(
|
|
1637
|
+
snapshot: MessagingRunSnapshotV1,
|
|
1638
|
+
options: MessagingRuntimeOptions,
|
|
1639
|
+
): MessagingRunSnapshotV1 {
|
|
1640
|
+
const active = snapshot.run.parts[snapshot.run.provenPartCount];
|
|
1641
|
+
if (active?.state === "dispatching") {
|
|
1642
|
+
return stopMessagingIndeterminate(
|
|
1643
|
+
snapshot,
|
|
1644
|
+
"provider-result-indeterminate",
|
|
1645
|
+
options,
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1648
|
+
if (active?.state === "claimed" || active?.state === "unattempted") {
|
|
1649
|
+
return stopMessagingBeforeDispatch(
|
|
1650
|
+
snapshot,
|
|
1651
|
+
"provider-failed-before-dispatch",
|
|
1652
|
+
options,
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
if (snapshot.run.state !== "pending") return snapshot;
|
|
1656
|
+
throw new Error("messaging run error state could not be classified");
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
function stopMessagingAfterCaughtError(
|
|
1660
|
+
snapshot: MessagingRunSnapshotV1,
|
|
1661
|
+
options: MessagingRuntimeOptions,
|
|
1662
|
+
): MessagingRunSnapshotV1 {
|
|
1663
|
+
try {
|
|
1664
|
+
return stopMessagingAfterError(snapshot, options);
|
|
1665
|
+
} catch {
|
|
1666
|
+
const environment = options.environment ?? process.env;
|
|
1667
|
+
const latest = readMessagingRun(snapshot.run.runId, environment);
|
|
1668
|
+
if (latest.run.state !== "pending") return latest;
|
|
1669
|
+
const active = latest.run.parts[latest.run.provenPartCount];
|
|
1670
|
+
if (active?.state === "dispatching") {
|
|
1671
|
+
return stopMessagingIndeterminate(
|
|
1672
|
+
latest,
|
|
1673
|
+
"journal-recovery-required",
|
|
1674
|
+
options,
|
|
1675
|
+
);
|
|
1676
|
+
}
|
|
1677
|
+
return stopMessagingBeforeDispatch(
|
|
1678
|
+
latest,
|
|
1679
|
+
"journal-recovery-required",
|
|
1680
|
+
options,
|
|
1681
|
+
);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
/**
|
|
1686
|
+
* Execute one already-owned and consumed composite confirmation. The caller
|
|
1687
|
+
* owns the outer confirmation claim and ordinary receipt projection. This
|
|
1688
|
+
* function owns the one encrypted per-bubble journal and never retries.
|
|
1689
|
+
*/
|
|
1690
|
+
export async function executeMessagingCompositeInternal(
|
|
1691
|
+
stored: StoredPlan,
|
|
1692
|
+
invocation: PreparedInvocation,
|
|
1693
|
+
initialSnapshot: MessagingRunSnapshotV1,
|
|
1694
|
+
options: MessagingRuntimeOptions = {},
|
|
1695
|
+
): Promise<MessagingRunSnapshotV1> {
|
|
1696
|
+
const composite = stored.plan.messagingComposite;
|
|
1697
|
+
if (composite === undefined) {
|
|
1698
|
+
throw new Error("confirmation plan is not a messaging composite");
|
|
1699
|
+
}
|
|
1700
|
+
if (
|
|
1701
|
+
composite.contextBindingSha256 === null
|
|
1702
|
+
|| composite.sourceConversationCoordinateSha256 === null
|
|
1703
|
+
) {
|
|
1704
|
+
throw new Error(
|
|
1705
|
+
"predecessor messaging plan lacks current context evidence; preview the action again",
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1708
|
+
if (
|
|
1709
|
+
initialSnapshot.run.planDigest !== stored.digest
|
|
1710
|
+
|| initialSnapshot.run.turnDigest !== composite.turnDigest
|
|
1711
|
+
|| initialSnapshot.run.contextBindingSha256
|
|
1712
|
+
!== composite.contextBindingSha256
|
|
1713
|
+
|| initialSnapshot.run.sourceConversationCoordinateSha256
|
|
1714
|
+
!== composite.sourceConversationCoordinateSha256
|
|
1715
|
+
|| initialSnapshot.run.state !== "pending"
|
|
1716
|
+
) throw new Error("messaging run does not own the exact composite plan");
|
|
1717
|
+
const environment = options.environment ?? process.env;
|
|
1718
|
+
const registry = options.registry ?? providerPluginRegistry;
|
|
1719
|
+
const actionResolution = messagingResolution(invocation, registry, "action");
|
|
1720
|
+
const action = actionResolution.messaging.action;
|
|
1721
|
+
if (action.state !== "supported") {
|
|
1722
|
+
throw new Error("messaging action support disappeared after confirmation");
|
|
1723
|
+
}
|
|
1724
|
+
if (actionResolution.binding.executeMessagingPart === undefined) {
|
|
1725
|
+
throw new Error("messaging provider has no private action executor");
|
|
1726
|
+
}
|
|
1727
|
+
if (actionResolution.binding.transport === "provider-api") {
|
|
1728
|
+
throw new Error(
|
|
1729
|
+
"messaging actions require a cleanup-qualified session or local CLI transport",
|
|
1730
|
+
);
|
|
1731
|
+
}
|
|
1732
|
+
const operation = invocation.manifest.operations[invocation.operationId];
|
|
1733
|
+
if (operation === undefined) {
|
|
1734
|
+
throw new Error("messaging action lost its exact operation recipe");
|
|
1735
|
+
}
|
|
1736
|
+
const actionTimeoutMs = isLocalCliOperation(operation)
|
|
1737
|
+
? operation.localCli.timeoutMs
|
|
1738
|
+
: isWebSessionOperation(operation)
|
|
1739
|
+
? operation.webSession.timeoutMs
|
|
1740
|
+
: null;
|
|
1741
|
+
if (actionTimeoutMs === null) {
|
|
1742
|
+
throw new Error("messaging action has no cleanup-qualified bounded recipe");
|
|
1743
|
+
}
|
|
1744
|
+
const boundAuth = invocation.auth;
|
|
1745
|
+
requireBoundAuth(actionResolution.binding, boundAuth);
|
|
1746
|
+
let snapshot = initialSnapshot;
|
|
1747
|
+
for (let index = snapshot.run.provenPartCount; index < composite.parts.length; index += 1) {
|
|
1748
|
+
if (snapshot.run.state !== "pending" || snapshot.run.provenPartCount !== index) {
|
|
1749
|
+
return snapshot;
|
|
1750
|
+
}
|
|
1751
|
+
if (options.signal?.aborted === true) {
|
|
1752
|
+
return stopMessagingBeforeDispatch(
|
|
1753
|
+
snapshot,
|
|
1754
|
+
"provider-failed-before-dispatch",
|
|
1755
|
+
options,
|
|
1756
|
+
);
|
|
1757
|
+
}
|
|
1758
|
+
const operationDeadline = new OperationDeadline(actionTimeoutMs, {
|
|
1759
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
1760
|
+
...(options.deadlineClock === undefined
|
|
1761
|
+
? {}
|
|
1762
|
+
: { clock: options.deadlineClock }),
|
|
1763
|
+
});
|
|
1764
|
+
try {
|
|
1765
|
+
snapshot = await operationDeadline.run(async () => {
|
|
1766
|
+
try {
|
|
1767
|
+
await requireRuntimeReady(
|
|
1768
|
+
actionResolution.binding,
|
|
1769
|
+
boundAuth,
|
|
1770
|
+
environment,
|
|
1771
|
+
registry,
|
|
1772
|
+
);
|
|
1773
|
+
} catch {
|
|
1774
|
+
operationDeadline.throwIfUnavailable("messaging provider action");
|
|
1775
|
+
return stopMessagingBeforeDispatch(
|
|
1776
|
+
snapshot,
|
|
1777
|
+
"prefix-freshness-unproven",
|
|
1778
|
+
options,
|
|
1779
|
+
);
|
|
1780
|
+
}
|
|
1781
|
+
let record: MessagingRouteRecordV1;
|
|
1782
|
+
let routeResolution: MessagingResolution;
|
|
1783
|
+
let current: Awaited<ReturnType<typeof currentContextPage>>;
|
|
1784
|
+
let routeState: Awaited<ReturnType<typeof currentMessagingRouteState>>;
|
|
1785
|
+
try {
|
|
1786
|
+
const route = await resolveCurrentRoute(
|
|
1787
|
+
composite.routeRef,
|
|
1788
|
+
now(options),
|
|
1789
|
+
environment,
|
|
1790
|
+
registry,
|
|
1791
|
+
);
|
|
1792
|
+
record = route.record;
|
|
1793
|
+
routeResolution = route.resolution;
|
|
1794
|
+
if (
|
|
1795
|
+
routeResolution.binding !== actionResolution.binding
|
|
1796
|
+
|| routeResolution.plugin.id !== actionResolution.plugin.id
|
|
1797
|
+
|| routeResolution.messaging.contractId !== actionResolution.messaging.contractId
|
|
1798
|
+
) {
|
|
1799
|
+
return stopMessagingBeforeDispatch(snapshot, "context-drift", options);
|
|
1800
|
+
}
|
|
1801
|
+
current = await currentContextPage(
|
|
1802
|
+
record,
|
|
1803
|
+
routeResolution,
|
|
1804
|
+
composite.contextLimit,
|
|
1805
|
+
environment,
|
|
1806
|
+
registry,
|
|
1807
|
+
operationDeadline.signal,
|
|
1808
|
+
);
|
|
1809
|
+
routeState = await currentMessagingRouteState(
|
|
1810
|
+
record,
|
|
1811
|
+
routeResolution,
|
|
1812
|
+
environment,
|
|
1813
|
+
registry,
|
|
1814
|
+
operationDeadline.signal,
|
|
1815
|
+
);
|
|
1816
|
+
} catch {
|
|
1817
|
+
operationDeadline.throwIfUnavailable("messaging provider action");
|
|
1818
|
+
return stopMessagingBeforeDispatch(
|
|
1819
|
+
snapshot,
|
|
1820
|
+
"prefix-freshness-unproven",
|
|
1821
|
+
options,
|
|
1822
|
+
);
|
|
1823
|
+
}
|
|
1824
|
+
if (
|
|
1825
|
+
routeState.revision !== composite.baseRouteStateRevision
|
|
1826
|
+
|| record.sourceConversationCoordinate?.sha256
|
|
1827
|
+
!== composite.sourceConversationCoordinateSha256
|
|
1828
|
+
) {
|
|
1829
|
+
return stopMessagingBeforeDispatch(snapshot, "context-drift", options);
|
|
1830
|
+
}
|
|
1831
|
+
let proof: ProviderPluginMessagingExpectedOwnPrefixProofV1;
|
|
1832
|
+
try {
|
|
1833
|
+
const accepted = messagingExpectedOwnPrefix(snapshot.run);
|
|
1834
|
+
proof = parseExpectedOwnPrefixProof(action.proveExpectedOwnPrefix(Object.freeze({
|
|
1835
|
+
base: Object.freeze({
|
|
1836
|
+
exactDataRevision: composite.baseExactDataRevision,
|
|
1837
|
+
latestMessageRevision: composite.baseLatestMessageRevision,
|
|
1838
|
+
contextLimit: composite.contextLimit,
|
|
1839
|
+
messages: composite.baseMessages,
|
|
1840
|
+
}),
|
|
1841
|
+
current: Object.freeze({
|
|
1842
|
+
exactDataRevision: current.exactDataRevision,
|
|
1843
|
+
latestMessageRevision: current.latestMessageRevision,
|
|
1844
|
+
messages: current.messages,
|
|
1845
|
+
}),
|
|
1846
|
+
accepted,
|
|
1847
|
+
})), accepted.length);
|
|
1848
|
+
} catch {
|
|
1849
|
+
operationDeadline.throwIfUnavailable("messaging provider action");
|
|
1850
|
+
return stopMessagingBeforeDispatch(
|
|
1851
|
+
snapshot,
|
|
1852
|
+
"prefix-freshness-unproven",
|
|
1853
|
+
options,
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
operationDeadline.throwIfUnavailable("messaging provider action");
|
|
1857
|
+
if (
|
|
1858
|
+
proof.state !== "proven"
|
|
1859
|
+
|| proof.matchedAcceptedPrefixCount < snapshot.run.observedAcceptedPrefixCount
|
|
1860
|
+
) {
|
|
1861
|
+
return stopMessagingBeforeDispatch(snapshot, "context-drift", options);
|
|
1862
|
+
}
|
|
1863
|
+
snapshot = updateMessagingRun(snapshot, {
|
|
1864
|
+
type: "claimed",
|
|
1865
|
+
index,
|
|
1866
|
+
observedAcceptedPrefixCount: proof.matchedAcceptedPrefixCount,
|
|
1867
|
+
at: messagingTransitionTime(snapshot, options),
|
|
1868
|
+
}, environment);
|
|
1869
|
+
operationDeadline.throwIfUnavailable("messaging provider action");
|
|
1870
|
+
let crossedExternalBoundary = false;
|
|
1871
|
+
try {
|
|
1872
|
+
const recordPrivateIndeterminateOutcome = async (
|
|
1873
|
+
code: string,
|
|
1874
|
+
): Promise<void> => {
|
|
1875
|
+
if (!crossedExternalBoundary) {
|
|
1876
|
+
throw new Error(
|
|
1877
|
+
"messaging provider outcome preceded its durable dispatch boundary",
|
|
1878
|
+
);
|
|
1879
|
+
}
|
|
1880
|
+
snapshot = stopMessagingIndeterminate(
|
|
1881
|
+
snapshot,
|
|
1882
|
+
"provider-result-indeterminate",
|
|
1883
|
+
options,
|
|
1884
|
+
Object.freeze({
|
|
1885
|
+
schemaVersion: 1,
|
|
1886
|
+
messagingContractId: actionResolution.messaging.contractId,
|
|
1887
|
+
code,
|
|
1888
|
+
}),
|
|
1889
|
+
);
|
|
1890
|
+
};
|
|
1891
|
+
const beforeExternalBegin = async (): Promise<void> => {
|
|
1892
|
+
operationDeadline.throwIfUnavailable("messaging provider action");
|
|
1893
|
+
if (crossedExternalBoundary) {
|
|
1894
|
+
throw new Error(
|
|
1895
|
+
"messaging provider attempted more than one dispatch boundary",
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
snapshot = updateMessagingRun(snapshot, {
|
|
1899
|
+
type: "dispatching",
|
|
1900
|
+
index,
|
|
1901
|
+
at: messagingTransitionTime(snapshot, options),
|
|
1902
|
+
}, environment);
|
|
1903
|
+
crossedExternalBoundary = true;
|
|
1904
|
+
operationDeadline.throwIfUnavailable("messaging provider action");
|
|
1905
|
+
};
|
|
1906
|
+
const executePart = (
|
|
1907
|
+
deadline: Parameters<
|
|
1908
|
+
NonNullable<typeof actionResolution.binding.executeMessagingPart>
|
|
1909
|
+
>[3]["operationDeadline"],
|
|
1910
|
+
registerCleanupBarrier?: Parameters<
|
|
1911
|
+
NonNullable<typeof actionResolution.binding.executeMessagingPart>
|
|
1912
|
+
>[3]["registerCleanupBarrier"],
|
|
1913
|
+
) => actionResolution.binding.executeMessagingPart!(
|
|
1914
|
+
action.operation,
|
|
1915
|
+
composite.parts[index]!.input,
|
|
1916
|
+
boundAuth,
|
|
1917
|
+
Object.freeze({
|
|
1918
|
+
beforeExternalBegin,
|
|
1919
|
+
recordPrivateIndeterminateOutcome,
|
|
1920
|
+
operationDeadline: deadline,
|
|
1921
|
+
signal: deadline.signal,
|
|
1922
|
+
...(registerCleanupBarrier === undefined
|
|
1923
|
+
? {}
|
|
1924
|
+
: { registerCleanupBarrier }),
|
|
1925
|
+
environment,
|
|
1926
|
+
}),
|
|
1927
|
+
);
|
|
1928
|
+
const output = actionResolution.binding.transport === "local-cli"
|
|
1929
|
+
? await (() => {
|
|
1930
|
+
if (!isLocalCliOperation(operation)) {
|
|
1931
|
+
throw new Error(
|
|
1932
|
+
"local CLI messaging action lost its exact operation recipe",
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
return withLocalCliProviderCleanupAdmission(
|
|
1936
|
+
{
|
|
1937
|
+
registry,
|
|
1938
|
+
binding: actionResolution.binding,
|
|
1939
|
+
auth: boundAuth,
|
|
1940
|
+
purpose: {
|
|
1941
|
+
kind: "messaging",
|
|
1942
|
+
action: operation.localCli.action,
|
|
1943
|
+
contractVersion: operation.localCli.contractVersion,
|
|
1944
|
+
messagingRunId: snapshot.run.runId,
|
|
1945
|
+
partIndex: index,
|
|
1946
|
+
},
|
|
1947
|
+
environment,
|
|
1948
|
+
...(options.now === undefined ? {} : { now: options.now }),
|
|
1949
|
+
},
|
|
1950
|
+
(registerCleanupBarrier) => runLocalCliOperationWithDeadline(
|
|
1951
|
+
operation.localCli,
|
|
1952
|
+
{
|
|
1953
|
+
environment,
|
|
1954
|
+
signal: operationDeadline.signal,
|
|
1955
|
+
operationDeadline,
|
|
1956
|
+
registerCleanupBarrier,
|
|
1957
|
+
},
|
|
1958
|
+
(boundedOptions) => {
|
|
1959
|
+
const boundedDeadline = boundedOptions.operationDeadline;
|
|
1960
|
+
if (boundedDeadline === undefined) {
|
|
1961
|
+
throw new Error(
|
|
1962
|
+
"local CLI messaging deadline is unavailable",
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
return executePart(
|
|
1966
|
+
boundedDeadline,
|
|
1967
|
+
boundedOptions.registerCleanupBarrier,
|
|
1968
|
+
);
|
|
1969
|
+
},
|
|
1970
|
+
),
|
|
1971
|
+
);
|
|
1972
|
+
})()
|
|
1973
|
+
: await (() => {
|
|
1974
|
+
if (!isWebSessionOperation(operation)) {
|
|
1975
|
+
throw new Error(
|
|
1976
|
+
"session messaging action lost its exact operation recipe",
|
|
1977
|
+
);
|
|
1978
|
+
}
|
|
1979
|
+
return withWebSessionCleanupAdmission(
|
|
1980
|
+
messagingWebCleanupAdmissionIdentity(
|
|
1981
|
+
invocation,
|
|
1982
|
+
actionResolution,
|
|
1983
|
+
registry,
|
|
1984
|
+
snapshot.run.runId,
|
|
1985
|
+
index,
|
|
1986
|
+
),
|
|
1987
|
+
environment,
|
|
1988
|
+
(registerCleanupBarrier) =>
|
|
1989
|
+
runWebSessionOperationWithDeadline(
|
|
1990
|
+
operation.webSession,
|
|
1991
|
+
{
|
|
1992
|
+
environment,
|
|
1993
|
+
signal: operationDeadline.signal,
|
|
1994
|
+
operationDeadline,
|
|
1995
|
+
registerCleanupBarrier,
|
|
1996
|
+
},
|
|
1997
|
+
(boundedOptions) => {
|
|
1998
|
+
const boundedDeadline = boundedOptions.operationDeadline;
|
|
1999
|
+
if (boundedDeadline === undefined) {
|
|
2000
|
+
throw new Error(
|
|
2001
|
+
"session messaging deadline is unavailable",
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
2004
|
+
return executePart(
|
|
2005
|
+
boundedDeadline,
|
|
2006
|
+
boundedOptions.registerCleanupBarrier,
|
|
2007
|
+
);
|
|
2008
|
+
},
|
|
2009
|
+
),
|
|
2010
|
+
options.now,
|
|
2011
|
+
);
|
|
2012
|
+
})();
|
|
2013
|
+
if (snapshot.run.state !== "pending") return snapshot;
|
|
2014
|
+
operationDeadline.throwIfUnavailable("messaging provider action");
|
|
2015
|
+
if (!crossedExternalBoundary) {
|
|
2016
|
+
throw new Error(
|
|
2017
|
+
"messaging provider returned without crossing its durable dispatch boundary",
|
|
2018
|
+
);
|
|
2019
|
+
}
|
|
2020
|
+
const accepted = action.mapAcceptedResult(output);
|
|
2021
|
+
if (
|
|
2022
|
+
accepted.state !== "submitted"
|
|
2023
|
+
|| typeof accepted.providerMessageId !== "string"
|
|
2024
|
+
|| accepted.providerMessageId.length < 1
|
|
2025
|
+
|| Buffer.byteLength(accepted.providerMessageId, "utf8") > 4_096
|
|
2026
|
+
|| /[\0\r\n]/u.test(accepted.providerMessageId)
|
|
2027
|
+
|| accepted.providerRevision !== null
|
|
2028
|
+
&& (typeof accepted.providerRevision !== "string"
|
|
2029
|
+
|| accepted.providerRevision.length < 1
|
|
2030
|
+
|| Buffer.byteLength(accepted.providerRevision, "utf8") > 4_096
|
|
2031
|
+
|| /[\0\r\n]/u.test(accepted.providerRevision))
|
|
2032
|
+
) {
|
|
2033
|
+
throw new Error(
|
|
2034
|
+
"messaging provider returned malformed acceptance evidence",
|
|
2035
|
+
);
|
|
2036
|
+
}
|
|
2037
|
+
const existingProviderMessageIds = new Set([
|
|
2038
|
+
...composite.baseMessages.map((message) =>
|
|
2039
|
+
message.providerMessageId),
|
|
2040
|
+
...snapshot.run.parts
|
|
2041
|
+
.slice(0, snapshot.run.provenPartCount)
|
|
2042
|
+
.flatMap((part) => part.providerMessageId === null
|
|
2043
|
+
? []
|
|
2044
|
+
: [part.providerMessageId]),
|
|
2045
|
+
]);
|
|
2046
|
+
if (existingProviderMessageIds.has(accepted.providerMessageId)) {
|
|
2047
|
+
throw new Error(
|
|
2048
|
+
"messaging provider reused an existing message identity for acceptance",
|
|
2049
|
+
);
|
|
2050
|
+
}
|
|
2051
|
+
operationDeadline.throwIfUnavailable("messaging provider action");
|
|
2052
|
+
snapshot = updateMessagingRun(snapshot, {
|
|
2053
|
+
type: "accepted",
|
|
2054
|
+
index,
|
|
2055
|
+
providerMessageId: accepted.providerMessageId,
|
|
2056
|
+
providerRevision: accepted.providerRevision,
|
|
2057
|
+
at: messagingTransitionTime(snapshot, options),
|
|
2058
|
+
}, environment);
|
|
2059
|
+
operationDeadline.throwIfUnavailable("messaging provider action");
|
|
2060
|
+
return snapshot;
|
|
2061
|
+
} catch {
|
|
2062
|
+
return stopMessagingAfterCaughtError(snapshot, options);
|
|
2063
|
+
}
|
|
2064
|
+
}, "messaging provider action");
|
|
2065
|
+
} catch {
|
|
2066
|
+
snapshot = stopMessagingAfterCaughtError(snapshot, options);
|
|
2067
|
+
} finally {
|
|
2068
|
+
operationDeadline.dispose();
|
|
2069
|
+
}
|
|
2070
|
+
if (snapshot.run.state !== "pending") return snapshot;
|
|
2071
|
+
}
|
|
2072
|
+
return snapshot;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
export function loadMessagingPreviewForConfirmationInternal(
|
|
2076
|
+
digest: string,
|
|
2077
|
+
options: Pick<MessagingRuntimeOptions, "environment"> = {},
|
|
2078
|
+
): MessagingPreviewV1 {
|
|
2079
|
+
const stored = loadInvocationPlan(digest, options.environment ?? process.env);
|
|
2080
|
+
return verifyStoredMessagingPreview(stored);
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2083
|
+
export function showMessagingRunInternal(
|
|
2084
|
+
runId: string,
|
|
2085
|
+
options: Pick<MessagingRuntimeOptions, "environment"> = {},
|
|
2086
|
+
): {
|
|
2087
|
+
readonly run: MessagingRunV1;
|
|
2088
|
+
readonly receipt: MessagingRunReceipt;
|
|
2089
|
+
readonly receiptBinding: MessagingReceiptBinding;
|
|
2090
|
+
} {
|
|
2091
|
+
const run = readMessagingRun(runId, options.environment ?? process.env).run;
|
|
2092
|
+
if (run.state === "pending") {
|
|
2093
|
+
throw new Error("messaging run is pending or requires checked recovery");
|
|
2094
|
+
}
|
|
2095
|
+
return Object.freeze({
|
|
2096
|
+
run,
|
|
2097
|
+
receipt: messagingRunReceipt(run),
|
|
2098
|
+
receiptBinding: messagingReceiptBinding(run),
|
|
2099
|
+
});
|
|
2100
|
+
}
|
|
2101
|
+
|
|
2102
|
+
export function reconcileMessagingRunInternal(
|
|
2103
|
+
runId: string,
|
|
2104
|
+
options: Pick<MessagingRuntimeOptions, "environment"> = {},
|
|
2105
|
+
): {
|
|
2106
|
+
readonly schemaVersion: 1;
|
|
2107
|
+
readonly format: "wrench.messaging-reconciliation";
|
|
2108
|
+
readonly runId: string;
|
|
2109
|
+
readonly state: MessagingRunV1["state"];
|
|
2110
|
+
readonly action: "not-required" | "retained-unretriable";
|
|
2111
|
+
readonly receiptBindingSha256: string;
|
|
2112
|
+
readonly reason: string;
|
|
2113
|
+
} {
|
|
2114
|
+
const run = readMessagingRun(
|
|
2115
|
+
runId,
|
|
2116
|
+
options.environment ?? process.env,
|
|
2117
|
+
).run;
|
|
2118
|
+
if (run.state === "pending") {
|
|
2119
|
+
throw new Error(
|
|
2120
|
+
"messaging run is pending; run ghostget doctor before reconciliation",
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
2123
|
+
const binding = messagingReceiptBinding(run);
|
|
2124
|
+
const indeterminate = run.state === "indeterminate";
|
|
2125
|
+
return Object.freeze({
|
|
2126
|
+
schemaVersion: 1,
|
|
2127
|
+
format: "wrench.messaging-reconciliation",
|
|
2128
|
+
runId,
|
|
2129
|
+
state: run.state,
|
|
2130
|
+
action: indeterminate ? "retained-unretriable" : "not-required",
|
|
2131
|
+
receiptBindingSha256: binding.receiptSha256,
|
|
2132
|
+
reason: indeterminate
|
|
2133
|
+
? "the provider result has no exact accepted message identity; the run remains indeterminate and must not be retried"
|
|
2134
|
+
: "the messaging run already has a categorical terminal state",
|
|
2135
|
+
});
|
|
2136
|
+
}
|