@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,1740 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code-owned policy primitives for future Meta Relay requests.
|
|
3
|
+
*
|
|
4
|
+
* This module performs no network I/O and contains no live registered-operation
|
|
5
|
+
* revisions. A caller can only build a request template from a reviewed
|
|
6
|
+
* descriptor; capture-required descriptors remain inert. Bootstrap secrets are
|
|
7
|
+
* deliberately absent: descriptors declare their exact source-to-sink flow so
|
|
8
|
+
* a separate opaque bootstrap boundary can write them directly to the request.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { canonicalJson, sha256 } from "../canonical-json";
|
|
12
|
+
|
|
13
|
+
export const META_RELAY_ORIGINS = Object.freeze({
|
|
14
|
+
facebook: "https://www.facebook.com",
|
|
15
|
+
instagram: "https://www.instagram.com",
|
|
16
|
+
threads: "https://www.threads.com",
|
|
17
|
+
} as const);
|
|
18
|
+
|
|
19
|
+
export type MetaRelayPlatform = keyof typeof META_RELAY_ORIGINS;
|
|
20
|
+
export type MetaRelayOrigin = (typeof META_RELAY_ORIGINS)[MetaRelayPlatform];
|
|
21
|
+
export type MetaOperationType = "query" | "mutation";
|
|
22
|
+
export type MetaAccessKind = "personal" | "page" | "group" | "marketplace";
|
|
23
|
+
|
|
24
|
+
export type MetaDescriptorContract =
|
|
25
|
+
| {
|
|
26
|
+
readonly state: "capture-required";
|
|
27
|
+
readonly contractVersion: number;
|
|
28
|
+
readonly reason: string;
|
|
29
|
+
}
|
|
30
|
+
| {
|
|
31
|
+
readonly state: "observed";
|
|
32
|
+
readonly contractVersion: number;
|
|
33
|
+
readonly evidenceId: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type MetaAccessPolicy = {
|
|
37
|
+
readonly kind: MetaAccessKind;
|
|
38
|
+
readonly actorBinding: "viewer" | "target";
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type MetaAccessContext =
|
|
42
|
+
| {
|
|
43
|
+
readonly kind: "personal";
|
|
44
|
+
readonly platform: MetaRelayPlatform;
|
|
45
|
+
readonly viewerId: string;
|
|
46
|
+
readonly actorId: string;
|
|
47
|
+
readonly targetId: string;
|
|
48
|
+
}
|
|
49
|
+
| {
|
|
50
|
+
readonly kind: "page" | "group" | "marketplace";
|
|
51
|
+
readonly platform: "facebook";
|
|
52
|
+
readonly viewerId: string;
|
|
53
|
+
readonly actorId: string;
|
|
54
|
+
readonly targetId: string;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type MetaViewerProofSink = "access.viewer-id" | "form.__user";
|
|
58
|
+
export type MetaActorProofSink = "access.actor-id" | "form.av";
|
|
59
|
+
|
|
60
|
+
export type MetaBootstrapProofDeclaration =
|
|
61
|
+
| {
|
|
62
|
+
readonly kind: "viewer";
|
|
63
|
+
readonly source: "bootstrap.viewer";
|
|
64
|
+
readonly sinks: readonly MetaViewerProofSink[];
|
|
65
|
+
}
|
|
66
|
+
| {
|
|
67
|
+
readonly kind: "actor";
|
|
68
|
+
readonly source: "bootstrap.actor";
|
|
69
|
+
readonly sinks: readonly MetaActorProofSink[];
|
|
70
|
+
}
|
|
71
|
+
| {
|
|
72
|
+
readonly kind: "fb_dtsg";
|
|
73
|
+
readonly source: "bootstrap.fb_dtsg";
|
|
74
|
+
readonly sinks: readonly ["form.fb_dtsg"];
|
|
75
|
+
}
|
|
76
|
+
| {
|
|
77
|
+
readonly kind: "jazoest";
|
|
78
|
+
readonly source: "derived.fb_dtsg-jazoest";
|
|
79
|
+
readonly sinks: readonly ["form.jazoest"];
|
|
80
|
+
}
|
|
81
|
+
| {
|
|
82
|
+
readonly kind: "lsd";
|
|
83
|
+
readonly source: "bootstrap.lsd";
|
|
84
|
+
readonly sinks: readonly ["form.lsd"];
|
|
85
|
+
}
|
|
86
|
+
| {
|
|
87
|
+
readonly kind: "client-revision";
|
|
88
|
+
readonly source: "bootstrap.client-revision";
|
|
89
|
+
readonly sinks: readonly ["form.__rev"];
|
|
90
|
+
}
|
|
91
|
+
| {
|
|
92
|
+
readonly kind: "hsi";
|
|
93
|
+
readonly source: "bootstrap.hsi";
|
|
94
|
+
readonly sinks: readonly ["form.__hsi"];
|
|
95
|
+
}
|
|
96
|
+
| {
|
|
97
|
+
readonly kind: "comet-environment";
|
|
98
|
+
readonly source: "bootstrap.comet-environment";
|
|
99
|
+
readonly sinks: readonly ["form.__comet_req"];
|
|
100
|
+
}
|
|
101
|
+
| {
|
|
102
|
+
readonly kind: "request-counter";
|
|
103
|
+
readonly source: "session.request-counter";
|
|
104
|
+
readonly sinks: readonly ["form.__req"];
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export type MetaJsonScalar = string | number | boolean | null;
|
|
108
|
+
|
|
109
|
+
export type MetaVariableSchema =
|
|
110
|
+
| { readonly kind: "id" }
|
|
111
|
+
| { readonly kind: "cursor" }
|
|
112
|
+
| {
|
|
113
|
+
readonly kind: "string";
|
|
114
|
+
readonly minimumLength: number;
|
|
115
|
+
readonly maximumLength: number;
|
|
116
|
+
}
|
|
117
|
+
| { readonly kind: "boolean" }
|
|
118
|
+
| {
|
|
119
|
+
readonly kind: "integer";
|
|
120
|
+
readonly minimum: number;
|
|
121
|
+
readonly maximum: number;
|
|
122
|
+
}
|
|
123
|
+
| {
|
|
124
|
+
readonly kind: "number";
|
|
125
|
+
readonly minimum: number;
|
|
126
|
+
readonly maximum: number;
|
|
127
|
+
}
|
|
128
|
+
| {
|
|
129
|
+
readonly kind: "enum";
|
|
130
|
+
readonly values: readonly string[];
|
|
131
|
+
}
|
|
132
|
+
| {
|
|
133
|
+
readonly kind: "literal";
|
|
134
|
+
readonly value: MetaJsonScalar;
|
|
135
|
+
}
|
|
136
|
+
| {
|
|
137
|
+
readonly kind: "nullable";
|
|
138
|
+
readonly value: MetaVariableSchema;
|
|
139
|
+
}
|
|
140
|
+
| {
|
|
141
|
+
readonly kind: "list";
|
|
142
|
+
readonly items: MetaVariableSchema;
|
|
143
|
+
readonly minimumItems: number;
|
|
144
|
+
readonly maximumItems: number;
|
|
145
|
+
}
|
|
146
|
+
| {
|
|
147
|
+
readonly kind: "object";
|
|
148
|
+
readonly fields: readonly MetaNestedVariableField[];
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export type MetaNestedVariableField = {
|
|
152
|
+
readonly name: string;
|
|
153
|
+
readonly optional: boolean;
|
|
154
|
+
readonly schema: MetaVariableSchema;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
export type MetaVariableSource =
|
|
158
|
+
| { readonly kind: "input"; readonly key: string }
|
|
159
|
+
| { readonly kind: "viewer" }
|
|
160
|
+
| { readonly kind: "actor" }
|
|
161
|
+
| { readonly kind: "target" }
|
|
162
|
+
| { readonly kind: "pagination" }
|
|
163
|
+
| { readonly kind: "literal"; readonly value: MetaJsonScalar };
|
|
164
|
+
|
|
165
|
+
export type MetaVariableField = {
|
|
166
|
+
readonly name: string;
|
|
167
|
+
readonly optional: boolean;
|
|
168
|
+
readonly source: MetaVariableSource;
|
|
169
|
+
readonly schema: MetaVariableSchema;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export type MetaVariableDefinition = {
|
|
173
|
+
readonly fields: readonly MetaVariableField[];
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export type MetaPaginationPolicy =
|
|
177
|
+
| { readonly kind: "none" }
|
|
178
|
+
| {
|
|
179
|
+
readonly kind: "cursor";
|
|
180
|
+
readonly variableName: string;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export type MetaQueryResponseRootVariant =
|
|
184
|
+
| {
|
|
185
|
+
readonly kind: "query-data" | "prefetch-data";
|
|
186
|
+
readonly path: readonly string[];
|
|
187
|
+
}
|
|
188
|
+
| {
|
|
189
|
+
readonly kind: "incremental-data";
|
|
190
|
+
readonly label: string;
|
|
191
|
+
readonly path: readonly string[];
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
export type MetaMutationResponseRootVariant =
|
|
195
|
+
| {
|
|
196
|
+
readonly kind: "mutation-data";
|
|
197
|
+
readonly path: readonly string[];
|
|
198
|
+
}
|
|
199
|
+
| {
|
|
200
|
+
readonly kind: "incremental-data";
|
|
201
|
+
readonly label: string;
|
|
202
|
+
readonly path: readonly string[];
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export type MetaResponseRootVariant =
|
|
206
|
+
| MetaQueryResponseRootVariant
|
|
207
|
+
| MetaMutationResponseRootVariant;
|
|
208
|
+
|
|
209
|
+
export type MetaReadbackSchedule =
|
|
210
|
+
| {
|
|
211
|
+
readonly kind: "none";
|
|
212
|
+
readonly reason: string;
|
|
213
|
+
}
|
|
214
|
+
| {
|
|
215
|
+
readonly kind: "independent-query";
|
|
216
|
+
readonly descriptorId: string;
|
|
217
|
+
readonly after: "dispatch-response";
|
|
218
|
+
readonly actorBinding: "same";
|
|
219
|
+
readonly targetBinding: "same";
|
|
220
|
+
readonly attempts: 1;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export type MetaDispatchSchedule =
|
|
224
|
+
| {
|
|
225
|
+
readonly kind: "inert";
|
|
226
|
+
readonly dispatches: readonly [];
|
|
227
|
+
readonly readback: Extract<MetaReadbackSchedule, { readonly kind: "none" }>;
|
|
228
|
+
}
|
|
229
|
+
| {
|
|
230
|
+
readonly kind: "single-dispatch";
|
|
231
|
+
readonly dispatchId: string;
|
|
232
|
+
readonly attempts: 1;
|
|
233
|
+
readonly retry: "never";
|
|
234
|
+
readonly readback: Extract<
|
|
235
|
+
MetaReadbackSchedule,
|
|
236
|
+
{ readonly kind: "independent-query" }
|
|
237
|
+
>;
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
type MetaBaseDescriptor = {
|
|
241
|
+
readonly schemaVersion: 1;
|
|
242
|
+
readonly id: string;
|
|
243
|
+
readonly platform: MetaRelayPlatform;
|
|
244
|
+
readonly friendlyName: string;
|
|
245
|
+
readonly docId: string;
|
|
246
|
+
readonly origin: MetaRelayOrigin;
|
|
247
|
+
readonly path: string;
|
|
248
|
+
readonly contract: MetaDescriptorContract;
|
|
249
|
+
readonly access: MetaAccessPolicy;
|
|
250
|
+
readonly proofs: readonly MetaBootstrapProofDeclaration[];
|
|
251
|
+
readonly variables: MetaVariableDefinition;
|
|
252
|
+
readonly pagination: MetaPaginationPolicy;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
export type MetaQueryOperationDescriptor = MetaBaseDescriptor & {
|
|
256
|
+
readonly kind: "query";
|
|
257
|
+
readonly operationType: "query";
|
|
258
|
+
readonly method: "GET" | "POST";
|
|
259
|
+
readonly responseRoots: readonly MetaQueryResponseRootVariant[];
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export type MetaMutationOperationDescriptor = MetaBaseDescriptor & {
|
|
263
|
+
readonly kind: "mutation";
|
|
264
|
+
readonly operationType: "mutation";
|
|
265
|
+
readonly method: "POST";
|
|
266
|
+
readonly responseRoots: readonly MetaMutationResponseRootVariant[];
|
|
267
|
+
readonly schedule: MetaDispatchSchedule;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
export type MetaOperationDescriptor =
|
|
271
|
+
| MetaQueryOperationDescriptor
|
|
272
|
+
| MetaMutationOperationDescriptor;
|
|
273
|
+
|
|
274
|
+
export type MetaObservedOperationDescriptor = {
|
|
275
|
+
readonly friendlyName: string;
|
|
276
|
+
readonly docId: string;
|
|
277
|
+
readonly operationType: MetaOperationType;
|
|
278
|
+
readonly origin: MetaRelayOrigin;
|
|
279
|
+
readonly method: "GET" | "POST";
|
|
280
|
+
readonly path: string;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
export type MetaPaginationCursor = {
|
|
284
|
+
readonly schemaVersion: 1;
|
|
285
|
+
readonly descriptorKey: string;
|
|
286
|
+
readonly actorId: string;
|
|
287
|
+
readonly targetId: string;
|
|
288
|
+
readonly cursor: string;
|
|
289
|
+
readonly previousCursor: string | null;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
export type MetaRelayRequestBuildInput = {
|
|
293
|
+
readonly input: unknown;
|
|
294
|
+
readonly access: MetaAccessContext;
|
|
295
|
+
readonly pagination?: MetaPaginationCursor | null;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
export type MetaRelayRequestParameter = {
|
|
299
|
+
readonly name: "fb_api_req_friendly_name" | "doc_id" | "variables";
|
|
300
|
+
readonly value: string;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
export type MetaRelayProofFormFieldName =
|
|
304
|
+
| "__user"
|
|
305
|
+
| "av"
|
|
306
|
+
| "fb_dtsg"
|
|
307
|
+
| "jazoest"
|
|
308
|
+
| "lsd"
|
|
309
|
+
| "__rev"
|
|
310
|
+
| "__hsi"
|
|
311
|
+
| "__comet_req"
|
|
312
|
+
| "__req";
|
|
313
|
+
|
|
314
|
+
export type MetaRelayRequest = {
|
|
315
|
+
readonly descriptorId: string;
|
|
316
|
+
readonly operationType: MetaOperationType;
|
|
317
|
+
readonly origin: MetaRelayOrigin;
|
|
318
|
+
readonly method: "GET" | "POST";
|
|
319
|
+
readonly path: string;
|
|
320
|
+
readonly url: string;
|
|
321
|
+
readonly parameterLocation: "query" | "form";
|
|
322
|
+
readonly parameters: readonly MetaRelayRequestParameter[];
|
|
323
|
+
readonly proofBindings: readonly MetaBootstrapProofDeclaration[];
|
|
324
|
+
readonly proofFormFields: readonly MetaRelayProofFormFieldName[];
|
|
325
|
+
readonly access: MetaAccessContext;
|
|
326
|
+
readonly pagination: MetaPaginationCursor | null;
|
|
327
|
+
readonly schedule: MetaDispatchSchedule | null;
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
export type MetaRelayRequestProofCoordinates = {
|
|
331
|
+
readonly descriptorKey: string;
|
|
332
|
+
readonly viewerId: string;
|
|
333
|
+
readonly actorId: string;
|
|
334
|
+
readonly targetId: string;
|
|
335
|
+
readonly proofFormFields: readonly MetaRelayProofFormFieldName[];
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
export type MetaRelayResponseBinding = {
|
|
339
|
+
readonly descriptorId: string;
|
|
340
|
+
readonly operationType: MetaOperationType;
|
|
341
|
+
readonly variant: MetaResponseRootVariant;
|
|
342
|
+
readonly value: unknown;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const issuedRelayRequestCoordinates = new WeakMap<
|
|
346
|
+
MetaRelayRequest,
|
|
347
|
+
MetaRelayRequestProofCoordinates
|
|
348
|
+
>();
|
|
349
|
+
|
|
350
|
+
type JsonRecord = Record<string, unknown>;
|
|
351
|
+
|
|
352
|
+
const issuedPaginationCursors = new WeakSet<object>();
|
|
353
|
+
const EMPTY_DISPATCHES: readonly [] = Object.freeze([]);
|
|
354
|
+
|
|
355
|
+
const EXPECTED_ORIGIN = {
|
|
356
|
+
facebook: META_RELAY_ORIGINS.facebook,
|
|
357
|
+
instagram: META_RELAY_ORIGINS.instagram,
|
|
358
|
+
threads: META_RELAY_ORIGINS.threads,
|
|
359
|
+
} as const satisfies Readonly<Record<MetaRelayPlatform, MetaRelayOrigin>>;
|
|
360
|
+
|
|
361
|
+
const EXPECTED_RELAY_PATHS = Object.freeze({
|
|
362
|
+
facebook: Object.freeze(["/api/graphql/"]),
|
|
363
|
+
instagram: Object.freeze(["/api/graphql", "/api/graphql/"]),
|
|
364
|
+
threads: Object.freeze(["/api/graphql", "/api/graphql/"]),
|
|
365
|
+
} as const satisfies Readonly<Record<MetaRelayPlatform, readonly string[]>>);
|
|
366
|
+
|
|
367
|
+
const PROOF_SOURCES = Object.freeze({
|
|
368
|
+
viewer: "bootstrap.viewer",
|
|
369
|
+
actor: "bootstrap.actor",
|
|
370
|
+
fb_dtsg: "bootstrap.fb_dtsg",
|
|
371
|
+
jazoest: "derived.fb_dtsg-jazoest",
|
|
372
|
+
lsd: "bootstrap.lsd",
|
|
373
|
+
"client-revision": "bootstrap.client-revision",
|
|
374
|
+
hsi: "bootstrap.hsi",
|
|
375
|
+
"comet-environment": "bootstrap.comet-environment",
|
|
376
|
+
"request-counter": "session.request-counter",
|
|
377
|
+
} as const);
|
|
378
|
+
|
|
379
|
+
const PROOF_SINKS = Object.freeze({
|
|
380
|
+
viewer: Object.freeze(["access.viewer-id", "form.__user"]),
|
|
381
|
+
actor: Object.freeze(["access.actor-id", "form.av"]),
|
|
382
|
+
fb_dtsg: Object.freeze(["form.fb_dtsg"]),
|
|
383
|
+
jazoest: Object.freeze(["form.jazoest"]),
|
|
384
|
+
lsd: Object.freeze(["form.lsd"]),
|
|
385
|
+
"client-revision": Object.freeze(["form.__rev"]),
|
|
386
|
+
hsi: Object.freeze(["form.__hsi"]),
|
|
387
|
+
"comet-environment": Object.freeze(["form.__comet_req"]),
|
|
388
|
+
"request-counter": Object.freeze(["form.__req"]),
|
|
389
|
+
} as const);
|
|
390
|
+
|
|
391
|
+
function isRecord(value: unknown): value is JsonRecord {
|
|
392
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function record(value: unknown, label: string): JsonRecord {
|
|
396
|
+
if (!isRecord(value)) throw new Error(`${label} must be an object`);
|
|
397
|
+
return value;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function exactKeys(
|
|
401
|
+
value: JsonRecord,
|
|
402
|
+
required: readonly string[],
|
|
403
|
+
optional: readonly string[],
|
|
404
|
+
label: string,
|
|
405
|
+
): void {
|
|
406
|
+
const allowed = new Set([...required, ...optional]);
|
|
407
|
+
const missing = required.filter((key) => !Object.hasOwn(value, key));
|
|
408
|
+
const extra = Object.keys(value).filter((key) => !allowed.has(key));
|
|
409
|
+
if (missing.length > 0) throw new Error(`${label} omitted ${missing.join(", ")}`);
|
|
410
|
+
if (extra.length > 0) {
|
|
411
|
+
throw new Error(`${label} contained unsupported field(s): ${extra.join(", ")}`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function boundedString(
|
|
416
|
+
value: unknown,
|
|
417
|
+
label: string,
|
|
418
|
+
minimum: number,
|
|
419
|
+
maximum: number,
|
|
420
|
+
): string {
|
|
421
|
+
if (
|
|
422
|
+
typeof value !== "string"
|
|
423
|
+
|| value.length < minimum
|
|
424
|
+
|| value.length > maximum
|
|
425
|
+
|| /[\0\r]/u.test(value)
|
|
426
|
+
) {
|
|
427
|
+
throw new Error(`${label} must be bounded text`);
|
|
428
|
+
}
|
|
429
|
+
return value;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function boundedInteger(
|
|
433
|
+
value: unknown,
|
|
434
|
+
label: string,
|
|
435
|
+
minimum: number,
|
|
436
|
+
maximum: number,
|
|
437
|
+
): number {
|
|
438
|
+
if (
|
|
439
|
+
!Number.isSafeInteger(value)
|
|
440
|
+
|| (value as number) < minimum
|
|
441
|
+
|| (value as number) > maximum
|
|
442
|
+
) {
|
|
443
|
+
throw new Error(`${label} must be an integer between ${minimum} and ${maximum}`);
|
|
444
|
+
}
|
|
445
|
+
return value as number;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function boundedFiniteNumber(value: unknown, label: string): number {
|
|
449
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
450
|
+
throw new Error(`${label} must be a finite number`);
|
|
451
|
+
}
|
|
452
|
+
return value;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function exactIdentifier(value: unknown, label: string): string {
|
|
456
|
+
if (
|
|
457
|
+
typeof value !== "string"
|
|
458
|
+
|| value.length < 1
|
|
459
|
+
|| value.length > 256
|
|
460
|
+
|| !/^[A-Za-z0-9][A-Za-z0-9._:-]*$/u.test(value)
|
|
461
|
+
) {
|
|
462
|
+
throw new Error(`${label} must be an exact bounded Meta identifier`);
|
|
463
|
+
}
|
|
464
|
+
return value;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function exactCursor(value: unknown, label: string): string {
|
|
468
|
+
if (
|
|
469
|
+
typeof value !== "string"
|
|
470
|
+
|| value.length < 1
|
|
471
|
+
|| value.length > 4_096
|
|
472
|
+
|| [...value].some((character) => {
|
|
473
|
+
const codePoint = character.codePointAt(0);
|
|
474
|
+
return codePoint !== undefined && (codePoint <= 31 || codePoint === 127);
|
|
475
|
+
})
|
|
476
|
+
) {
|
|
477
|
+
throw new Error(`${label} must be an exact bounded opaque cursor`);
|
|
478
|
+
}
|
|
479
|
+
return value;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function exactFieldName(value: unknown, label: string): string {
|
|
483
|
+
if (
|
|
484
|
+
typeof value !== "string"
|
|
485
|
+
|| !/^[A-Za-z_][A-Za-z0-9_]{0,127}$/u.test(value)
|
|
486
|
+
|| value === "__proto__"
|
|
487
|
+
|| value === "constructor"
|
|
488
|
+
|| value === "prototype"
|
|
489
|
+
) {
|
|
490
|
+
throw new Error(`${label} must be an exact variable field name`);
|
|
491
|
+
}
|
|
492
|
+
return value;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function exactDescriptorId(value: unknown, label: string): string {
|
|
496
|
+
if (
|
|
497
|
+
typeof value !== "string"
|
|
498
|
+
|| !/^[a-z][a-z0-9.-]{2,127}$/u.test(value)
|
|
499
|
+
) {
|
|
500
|
+
throw new Error(`${label} must be a stable code-owned descriptor ID`);
|
|
501
|
+
}
|
|
502
|
+
return value;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export function assertMetaFriendlyName(value: unknown): string {
|
|
506
|
+
if (
|
|
507
|
+
typeof value !== "string"
|
|
508
|
+
|| !/^[A-Za-z][A-Za-z0-9_]{2,160}$/u.test(value)
|
|
509
|
+
) {
|
|
510
|
+
throw new Error("Meta friendlyName must match the reviewed Relay operation-name grammar");
|
|
511
|
+
}
|
|
512
|
+
return value;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export function assertMetaDocId(value: unknown): string {
|
|
516
|
+
if (typeof value !== "string" || !/^[0-9]{5,32}$/u.test(value)) {
|
|
517
|
+
throw new Error("Meta docId must be an exact 5-32 digit registered-operation revision");
|
|
518
|
+
}
|
|
519
|
+
return value;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function exactPlatform(value: unknown, label: string): MetaRelayPlatform {
|
|
523
|
+
if (value !== "facebook" && value !== "instagram" && value !== "threads") {
|
|
524
|
+
throw new Error(`${label} must be facebook, instagram, or threads`);
|
|
525
|
+
}
|
|
526
|
+
return value;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function exactOperationType(value: unknown, label: string): MetaOperationType {
|
|
530
|
+
if (value !== "query" && value !== "mutation") {
|
|
531
|
+
throw new Error(`${label} must be query or mutation`);
|
|
532
|
+
}
|
|
533
|
+
return value;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function exactMethod(value: unknown, label: string): "GET" | "POST" {
|
|
537
|
+
if (value !== "GET" && value !== "POST") {
|
|
538
|
+
throw new Error(`${label} must be exactly GET or POST`);
|
|
539
|
+
}
|
|
540
|
+
return value;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function exactOrigin(
|
|
544
|
+
value: unknown,
|
|
545
|
+
platform: MetaRelayPlatform,
|
|
546
|
+
label: string,
|
|
547
|
+
): MetaRelayOrigin {
|
|
548
|
+
const expected = EXPECTED_ORIGIN[platform];
|
|
549
|
+
if (value !== expected) {
|
|
550
|
+
throw new Error(`${label} must be the exact ${platform} first-party origin`);
|
|
551
|
+
}
|
|
552
|
+
return expected;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function exactPath(value: unknown, platform: MetaRelayPlatform, label: string): string {
|
|
556
|
+
if (
|
|
557
|
+
typeof value !== "string"
|
|
558
|
+
|| !EXPECTED_RELAY_PATHS[platform].includes(value)
|
|
559
|
+
) {
|
|
560
|
+
throw new Error(`${label} must be an exact reviewed ${platform} Relay path`);
|
|
561
|
+
}
|
|
562
|
+
return value;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function parseContract(value: unknown, label: string): MetaDescriptorContract {
|
|
566
|
+
const candidate = record(value, label);
|
|
567
|
+
if (candidate.state === "capture-required") {
|
|
568
|
+
exactKeys(candidate, ["state", "contractVersion", "reason"], [], label);
|
|
569
|
+
return Object.freeze({
|
|
570
|
+
state: "capture-required",
|
|
571
|
+
contractVersion: boundedInteger(
|
|
572
|
+
candidate.contractVersion,
|
|
573
|
+
`${label}.contractVersion`,
|
|
574
|
+
1,
|
|
575
|
+
1_000_000,
|
|
576
|
+
),
|
|
577
|
+
reason: boundedString(candidate.reason, `${label}.reason`, 1, 1_024),
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
if (candidate.state === "observed") {
|
|
581
|
+
exactKeys(candidate, ["state", "contractVersion", "evidenceId"], [], label);
|
|
582
|
+
return Object.freeze({
|
|
583
|
+
state: "observed",
|
|
584
|
+
contractVersion: boundedInteger(
|
|
585
|
+
candidate.contractVersion,
|
|
586
|
+
`${label}.contractVersion`,
|
|
587
|
+
1,
|
|
588
|
+
1_000_000,
|
|
589
|
+
),
|
|
590
|
+
evidenceId: boundedString(candidate.evidenceId, `${label}.evidenceId`, 1, 256),
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
throw new Error(`${label}.state must be observed or capture-required`);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function parseAccessPolicy(
|
|
597
|
+
value: unknown,
|
|
598
|
+
platform: MetaRelayPlatform,
|
|
599
|
+
label: string,
|
|
600
|
+
): MetaAccessPolicy {
|
|
601
|
+
const candidate = record(value, label);
|
|
602
|
+
exactKeys(candidate, ["kind", "actorBinding"], [], label);
|
|
603
|
+
const kind = candidate.kind;
|
|
604
|
+
if (
|
|
605
|
+
kind !== "personal"
|
|
606
|
+
&& kind !== "page"
|
|
607
|
+
&& kind !== "group"
|
|
608
|
+
&& kind !== "marketplace"
|
|
609
|
+
) {
|
|
610
|
+
throw new Error(`${label}.kind must be personal, page, group, or marketplace`);
|
|
611
|
+
}
|
|
612
|
+
if (kind !== "personal" && platform !== "facebook") {
|
|
613
|
+
throw new Error(`${label}.${kind} access is available only on facebook`);
|
|
614
|
+
}
|
|
615
|
+
if (candidate.actorBinding !== "viewer" && candidate.actorBinding !== "target") {
|
|
616
|
+
throw new Error(`${label}.actorBinding must be viewer or target`);
|
|
617
|
+
}
|
|
618
|
+
if (kind !== "page" && candidate.actorBinding !== "viewer") {
|
|
619
|
+
throw new Error(`${label}.${kind} access must bind its actor to the viewer`);
|
|
620
|
+
}
|
|
621
|
+
return Object.freeze({ kind, actorBinding: candidate.actorBinding });
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function parseProofDeclarations(
|
|
625
|
+
value: unknown,
|
|
626
|
+
method: "GET" | "POST",
|
|
627
|
+
label: string,
|
|
628
|
+
): readonly MetaBootstrapProofDeclaration[] {
|
|
629
|
+
if (!Array.isArray(value)) throw new Error(`${label} must be an array`);
|
|
630
|
+
const results: MetaBootstrapProofDeclaration[] = [];
|
|
631
|
+
const kinds = new Set<string>();
|
|
632
|
+
const allSinks = new Set<string>();
|
|
633
|
+
for (const [index, item] of value.entries()) {
|
|
634
|
+
const itemLabel = `${label}[${index}]`;
|
|
635
|
+
const candidate = record(item, itemLabel);
|
|
636
|
+
exactKeys(candidate, ["kind", "source", "sinks"], [], itemLabel);
|
|
637
|
+
const kind = candidate.kind;
|
|
638
|
+
if (
|
|
639
|
+
kind !== "viewer"
|
|
640
|
+
&& kind !== "actor"
|
|
641
|
+
&& kind !== "fb_dtsg"
|
|
642
|
+
&& kind !== "jazoest"
|
|
643
|
+
&& kind !== "lsd"
|
|
644
|
+
&& kind !== "client-revision"
|
|
645
|
+
&& kind !== "hsi"
|
|
646
|
+
&& kind !== "comet-environment"
|
|
647
|
+
&& kind !== "request-counter"
|
|
648
|
+
) {
|
|
649
|
+
throw new Error(`${itemLabel}.kind is not a reviewed Meta bootstrap proof`);
|
|
650
|
+
}
|
|
651
|
+
if (kinds.has(kind)) throw new Error(`${label} contained duplicate ${kind} proof`);
|
|
652
|
+
kinds.add(kind);
|
|
653
|
+
if (candidate.source !== PROOF_SOURCES[kind]) {
|
|
654
|
+
throw new Error(`${itemLabel}.source did not match the ${kind} proof source`);
|
|
655
|
+
}
|
|
656
|
+
if (!Array.isArray(candidate.sinks) || candidate.sinks.length < 1) {
|
|
657
|
+
throw new Error(`${itemLabel}.sinks must be a non-empty array`);
|
|
658
|
+
}
|
|
659
|
+
const allowedSinks: readonly string[] = PROOF_SINKS[kind];
|
|
660
|
+
const sinks: string[] = [];
|
|
661
|
+
for (const sink of candidate.sinks) {
|
|
662
|
+
if (typeof sink !== "string" || !allowedSinks.includes(sink)) {
|
|
663
|
+
throw new Error(`${itemLabel} declared an invalid ${kind} proof sink`);
|
|
664
|
+
}
|
|
665
|
+
if (sinks.includes(sink)) throw new Error(`${itemLabel} contained a duplicate proof sink`);
|
|
666
|
+
if (allSinks.has(sink)) throw new Error(`${label} bound one proof sink more than once`);
|
|
667
|
+
if (method === "GET" && sink.startsWith("form.")) {
|
|
668
|
+
throw new Error(`${itemLabel} may not bind a form proof sink to GET`);
|
|
669
|
+
}
|
|
670
|
+
sinks.push(sink);
|
|
671
|
+
allSinks.add(sink);
|
|
672
|
+
}
|
|
673
|
+
results.push(Object.freeze({
|
|
674
|
+
kind,
|
|
675
|
+
source: PROOF_SOURCES[kind],
|
|
676
|
+
sinks: Object.freeze(sinks),
|
|
677
|
+
}) as MetaBootstrapProofDeclaration);
|
|
678
|
+
}
|
|
679
|
+
if (!allSinks.has("access.viewer-id")) {
|
|
680
|
+
throw new Error(`${label} must bind bootstrap.viewer to access.viewer-id`);
|
|
681
|
+
}
|
|
682
|
+
if (!allSinks.has("access.actor-id")) {
|
|
683
|
+
throw new Error(`${label} must bind bootstrap.actor to access.actor-id`);
|
|
684
|
+
}
|
|
685
|
+
return Object.freeze(results);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function parseJsonScalar(value: unknown, label: string): MetaJsonScalar {
|
|
689
|
+
if (
|
|
690
|
+
value === null
|
|
691
|
+
|| typeof value === "string"
|
|
692
|
+
|| typeof value === "boolean"
|
|
693
|
+
|| (typeof value === "number" && Number.isFinite(value))
|
|
694
|
+
) {
|
|
695
|
+
return value;
|
|
696
|
+
}
|
|
697
|
+
throw new Error(`${label} must be a JSON scalar`);
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function parseNestedFields(
|
|
701
|
+
value: unknown,
|
|
702
|
+
label: string,
|
|
703
|
+
depth: number,
|
|
704
|
+
): readonly MetaNestedVariableField[] {
|
|
705
|
+
if (!Array.isArray(value)) throw new Error(`${label} must be an array`);
|
|
706
|
+
if (value.length > 128) throw new Error(`${label} exceeded its reviewed field bound`);
|
|
707
|
+
const names = new Set<string>();
|
|
708
|
+
const fields: MetaNestedVariableField[] = [];
|
|
709
|
+
for (const [index, item] of value.entries()) {
|
|
710
|
+
const itemLabel = `${label}[${index}]`;
|
|
711
|
+
const candidate = record(item, itemLabel);
|
|
712
|
+
exactKeys(candidate, ["name", "optional", "schema"], [], itemLabel);
|
|
713
|
+
const name = exactFieldName(candidate.name, `${itemLabel}.name`);
|
|
714
|
+
if (names.has(name)) throw new Error(`${label} contained duplicate field ${name}`);
|
|
715
|
+
names.add(name);
|
|
716
|
+
if (typeof candidate.optional !== "boolean") {
|
|
717
|
+
throw new Error(`${itemLabel}.optional must be boolean`);
|
|
718
|
+
}
|
|
719
|
+
fields.push(Object.freeze({
|
|
720
|
+
name,
|
|
721
|
+
optional: candidate.optional,
|
|
722
|
+
schema: parseVariableSchema(candidate.schema, `${itemLabel}.schema`, depth + 1),
|
|
723
|
+
}));
|
|
724
|
+
}
|
|
725
|
+
return Object.freeze(fields);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function parseVariableSchema(
|
|
729
|
+
value: unknown,
|
|
730
|
+
label: string,
|
|
731
|
+
depth = 0,
|
|
732
|
+
): MetaVariableSchema {
|
|
733
|
+
if (depth > 12) throw new Error(`${label} exceeded the reviewed schema depth`);
|
|
734
|
+
const candidate = record(value, label);
|
|
735
|
+
switch (candidate.kind) {
|
|
736
|
+
case "id":
|
|
737
|
+
case "cursor":
|
|
738
|
+
case "boolean":
|
|
739
|
+
exactKeys(candidate, ["kind"], [], label);
|
|
740
|
+
return Object.freeze({ kind: candidate.kind });
|
|
741
|
+
case "string": {
|
|
742
|
+
exactKeys(candidate, ["kind", "minimumLength", "maximumLength"], [], label);
|
|
743
|
+
const minimumLength = boundedInteger(
|
|
744
|
+
candidate.minimumLength,
|
|
745
|
+
`${label}.minimumLength`,
|
|
746
|
+
0,
|
|
747
|
+
1_000_000,
|
|
748
|
+
);
|
|
749
|
+
const maximumLength = boundedInteger(
|
|
750
|
+
candidate.maximumLength,
|
|
751
|
+
`${label}.maximumLength`,
|
|
752
|
+
0,
|
|
753
|
+
1_000_000,
|
|
754
|
+
);
|
|
755
|
+
if (maximumLength < minimumLength) {
|
|
756
|
+
throw new Error(`${label}.maximumLength must not be below minimumLength`);
|
|
757
|
+
}
|
|
758
|
+
return Object.freeze({ kind: "string", minimumLength, maximumLength });
|
|
759
|
+
}
|
|
760
|
+
case "integer":
|
|
761
|
+
case "number": {
|
|
762
|
+
exactKeys(candidate, ["kind", "minimum", "maximum"], [], label);
|
|
763
|
+
const minimum = candidate.kind === "integer"
|
|
764
|
+
? boundedInteger(
|
|
765
|
+
candidate.minimum,
|
|
766
|
+
`${label}.minimum`,
|
|
767
|
+
Number.MIN_SAFE_INTEGER,
|
|
768
|
+
Number.MAX_SAFE_INTEGER,
|
|
769
|
+
)
|
|
770
|
+
: boundedFiniteNumber(candidate.minimum, `${label}.minimum`);
|
|
771
|
+
const maximum = candidate.kind === "integer"
|
|
772
|
+
? boundedInteger(
|
|
773
|
+
candidate.maximum,
|
|
774
|
+
`${label}.maximum`,
|
|
775
|
+
Number.MIN_SAFE_INTEGER,
|
|
776
|
+
Number.MAX_SAFE_INTEGER,
|
|
777
|
+
)
|
|
778
|
+
: boundedFiniteNumber(candidate.maximum, `${label}.maximum`);
|
|
779
|
+
if (maximum < minimum) throw new Error(`${label}.maximum must not be below minimum`);
|
|
780
|
+
return Object.freeze({ kind: candidate.kind, minimum, maximum });
|
|
781
|
+
}
|
|
782
|
+
case "enum": {
|
|
783
|
+
exactKeys(candidate, ["kind", "values"], [], label);
|
|
784
|
+
if (!Array.isArray(candidate.values) || candidate.values.length < 1) {
|
|
785
|
+
throw new Error(`${label}.values must be a non-empty array`);
|
|
786
|
+
}
|
|
787
|
+
const values = candidate.values.map((item, index) => (
|
|
788
|
+
boundedString(item, `${label}.values[${index}]`, 1, 256)
|
|
789
|
+
));
|
|
790
|
+
if (new Set(values).size !== values.length) {
|
|
791
|
+
throw new Error(`${label}.values contained duplicates`);
|
|
792
|
+
}
|
|
793
|
+
return Object.freeze({ kind: "enum", values: Object.freeze(values) });
|
|
794
|
+
}
|
|
795
|
+
case "literal":
|
|
796
|
+
exactKeys(candidate, ["kind", "value"], [], label);
|
|
797
|
+
return Object.freeze({
|
|
798
|
+
kind: "literal",
|
|
799
|
+
value: parseJsonScalar(candidate.value, `${label}.value`),
|
|
800
|
+
});
|
|
801
|
+
case "nullable":
|
|
802
|
+
exactKeys(candidate, ["kind", "value"], [], label);
|
|
803
|
+
return Object.freeze({
|
|
804
|
+
kind: "nullable",
|
|
805
|
+
value: parseVariableSchema(candidate.value, `${label}.value`, depth + 1),
|
|
806
|
+
});
|
|
807
|
+
case "list": {
|
|
808
|
+
exactKeys(candidate, ["kind", "items", "minimumItems", "maximumItems"], [], label);
|
|
809
|
+
const minimumItems = boundedInteger(
|
|
810
|
+
candidate.minimumItems,
|
|
811
|
+
`${label}.minimumItems`,
|
|
812
|
+
0,
|
|
813
|
+
10_000,
|
|
814
|
+
);
|
|
815
|
+
const maximumItems = boundedInteger(
|
|
816
|
+
candidate.maximumItems,
|
|
817
|
+
`${label}.maximumItems`,
|
|
818
|
+
0,
|
|
819
|
+
10_000,
|
|
820
|
+
);
|
|
821
|
+
if (maximumItems < minimumItems) {
|
|
822
|
+
throw new Error(`${label}.maximumItems must not be below minimumItems`);
|
|
823
|
+
}
|
|
824
|
+
return Object.freeze({
|
|
825
|
+
kind: "list",
|
|
826
|
+
items: parseVariableSchema(candidate.items, `${label}.items`, depth + 1),
|
|
827
|
+
minimumItems,
|
|
828
|
+
maximumItems,
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
case "object":
|
|
832
|
+
exactKeys(candidate, ["kind", "fields"], [], label);
|
|
833
|
+
return Object.freeze({
|
|
834
|
+
kind: "object",
|
|
835
|
+
fields: parseNestedFields(candidate.fields, `${label}.fields`, depth),
|
|
836
|
+
});
|
|
837
|
+
default:
|
|
838
|
+
throw new Error(`${label}.kind is not a reviewed Meta variable schema`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function parseVariableSource(value: unknown, label: string): MetaVariableSource {
|
|
843
|
+
const candidate = record(value, label);
|
|
844
|
+
switch (candidate.kind) {
|
|
845
|
+
case "input":
|
|
846
|
+
exactKeys(candidate, ["kind", "key"], [], label);
|
|
847
|
+
return Object.freeze({
|
|
848
|
+
kind: "input",
|
|
849
|
+
key: exactFieldName(candidate.key, `${label}.key`),
|
|
850
|
+
});
|
|
851
|
+
case "viewer":
|
|
852
|
+
case "actor":
|
|
853
|
+
case "target":
|
|
854
|
+
case "pagination":
|
|
855
|
+
exactKeys(candidate, ["kind"], [], label);
|
|
856
|
+
return Object.freeze({ kind: candidate.kind });
|
|
857
|
+
case "literal":
|
|
858
|
+
exactKeys(candidate, ["kind", "value"], [], label);
|
|
859
|
+
return Object.freeze({
|
|
860
|
+
kind: "literal",
|
|
861
|
+
value: parseJsonScalar(candidate.value, `${label}.value`),
|
|
862
|
+
});
|
|
863
|
+
default:
|
|
864
|
+
throw new Error(`${label}.kind is not a reviewed Meta variable source`);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function schemaAcceptsKind(
|
|
869
|
+
schema: MetaVariableSchema,
|
|
870
|
+
kind: "id" | "cursor",
|
|
871
|
+
): boolean {
|
|
872
|
+
return schema.kind === kind
|
|
873
|
+
|| (schema.kind === "nullable" && schemaAcceptsKind(schema.value, kind));
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function parseVariableDefinition(value: unknown, label: string): MetaVariableDefinition {
|
|
877
|
+
const candidate = record(value, label);
|
|
878
|
+
exactKeys(candidate, ["fields"], [], label);
|
|
879
|
+
if (!Array.isArray(candidate.fields) || candidate.fields.length > 128) {
|
|
880
|
+
throw new Error(`${label}.fields must be a bounded array`);
|
|
881
|
+
}
|
|
882
|
+
const names = new Set<string>();
|
|
883
|
+
const fields: MetaVariableField[] = [];
|
|
884
|
+
for (const [index, item] of candidate.fields.entries()) {
|
|
885
|
+
const itemLabel = `${label}.fields[${index}]`;
|
|
886
|
+
const field = record(item, itemLabel);
|
|
887
|
+
exactKeys(field, ["name", "optional", "source", "schema"], [], itemLabel);
|
|
888
|
+
const name = exactFieldName(field.name, `${itemLabel}.name`);
|
|
889
|
+
if (names.has(name)) throw new Error(`${label}.fields contained duplicate field ${name}`);
|
|
890
|
+
names.add(name);
|
|
891
|
+
if (typeof field.optional !== "boolean") {
|
|
892
|
+
throw new Error(`${itemLabel}.optional must be boolean`);
|
|
893
|
+
}
|
|
894
|
+
const source = parseVariableSource(field.source, `${itemLabel}.source`);
|
|
895
|
+
const schema = parseVariableSchema(field.schema, `${itemLabel}.schema`);
|
|
896
|
+
if (
|
|
897
|
+
(source.kind === "viewer" || source.kind === "actor" || source.kind === "target")
|
|
898
|
+
&& !schemaAcceptsKind(schema, "id")
|
|
899
|
+
) {
|
|
900
|
+
throw new Error(`${itemLabel} identity source requires an id schema`);
|
|
901
|
+
}
|
|
902
|
+
if (source.kind === "pagination") {
|
|
903
|
+
if (!field.optional || !schemaAcceptsKind(schema, "cursor")) {
|
|
904
|
+
throw new Error(`${itemLabel} pagination source requires an optional cursor schema`);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
if (source.kind === "literal") {
|
|
908
|
+
validateVariableValue(schema, source.value, `${itemLabel}.source.value`);
|
|
909
|
+
}
|
|
910
|
+
fields.push(Object.freeze({ name, optional: field.optional, source, schema }));
|
|
911
|
+
}
|
|
912
|
+
return Object.freeze({ fields: Object.freeze(fields) });
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function parsePaginationPolicy(
|
|
916
|
+
value: unknown,
|
|
917
|
+
variables: MetaVariableDefinition,
|
|
918
|
+
label: string,
|
|
919
|
+
): MetaPaginationPolicy {
|
|
920
|
+
const candidate = record(value, label);
|
|
921
|
+
if (candidate.kind === "none") {
|
|
922
|
+
exactKeys(candidate, ["kind"], [], label);
|
|
923
|
+
if (variables.fields.some((field) => field.source.kind === "pagination")) {
|
|
924
|
+
throw new Error(`${label} none cannot declare a pagination variable`);
|
|
925
|
+
}
|
|
926
|
+
return Object.freeze({ kind: "none" });
|
|
927
|
+
}
|
|
928
|
+
if (candidate.kind === "cursor") {
|
|
929
|
+
exactKeys(candidate, ["kind", "variableName"], [], label);
|
|
930
|
+
const variableName = exactFieldName(candidate.variableName, `${label}.variableName`);
|
|
931
|
+
const cursorFields = variables.fields.filter((field) => field.source.kind === "pagination");
|
|
932
|
+
if (cursorFields.length !== 1 || cursorFields[0]?.name !== variableName) {
|
|
933
|
+
throw new Error(`${label} must bind exactly one matching pagination variable`);
|
|
934
|
+
}
|
|
935
|
+
return Object.freeze({ kind: "cursor", variableName });
|
|
936
|
+
}
|
|
937
|
+
throw new Error(`${label}.kind must be none or cursor`);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function parsePath(value: unknown, label: string): readonly string[] {
|
|
941
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 32) {
|
|
942
|
+
throw new Error(`${label} must be a non-empty bounded path`);
|
|
943
|
+
}
|
|
944
|
+
return Object.freeze(value.map((segment, index) => (
|
|
945
|
+
exactFieldName(segment, `${label}[${index}]`)
|
|
946
|
+
)));
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function parseResponseRoots(
|
|
950
|
+
value: unknown,
|
|
951
|
+
operationType: MetaOperationType,
|
|
952
|
+
label: string,
|
|
953
|
+
): readonly MetaResponseRootVariant[] {
|
|
954
|
+
if (!Array.isArray(value) || value.length < 1 || value.length > 16) {
|
|
955
|
+
throw new Error(`${label} must be a non-empty bounded array`);
|
|
956
|
+
}
|
|
957
|
+
const roots: MetaResponseRootVariant[] = [];
|
|
958
|
+
const keys = new Set<string>();
|
|
959
|
+
for (const [index, item] of value.entries()) {
|
|
960
|
+
const itemLabel = `${label}[${index}]`;
|
|
961
|
+
const candidate = record(item, itemLabel);
|
|
962
|
+
const kind = candidate.kind;
|
|
963
|
+
if (kind === "incremental-data") {
|
|
964
|
+
exactKeys(candidate, ["kind", "label", "path"], [], itemLabel);
|
|
965
|
+
const root = Object.freeze({
|
|
966
|
+
kind,
|
|
967
|
+
label: exactFieldName(candidate.label, `${itemLabel}.label`),
|
|
968
|
+
path: parsePath(candidate.path, `${itemLabel}.path`),
|
|
969
|
+
});
|
|
970
|
+
const key = `${root.kind}:${root.label}:${root.path.join(".")}`;
|
|
971
|
+
if (keys.has(key)) throw new Error(`${label} contained duplicate response root`);
|
|
972
|
+
keys.add(key);
|
|
973
|
+
roots.push(root);
|
|
974
|
+
continue;
|
|
975
|
+
}
|
|
976
|
+
exactKeys(candidate, ["kind", "path"], [], itemLabel);
|
|
977
|
+
const allowed = operationType === "query"
|
|
978
|
+
? kind === "query-data" || kind === "prefetch-data"
|
|
979
|
+
: kind === "mutation-data";
|
|
980
|
+
if (!allowed) {
|
|
981
|
+
throw new Error(`${itemLabel}.kind did not agree with ${operationType}`);
|
|
982
|
+
}
|
|
983
|
+
const root = Object.freeze({
|
|
984
|
+
kind,
|
|
985
|
+
path: parsePath(candidate.path, `${itemLabel}.path`),
|
|
986
|
+
}) as MetaResponseRootVariant;
|
|
987
|
+
const key = `${root.kind}:${root.path.join(".")}`;
|
|
988
|
+
if (keys.has(key)) throw new Error(`${label} contained duplicate response root`);
|
|
989
|
+
keys.add(key);
|
|
990
|
+
roots.push(root);
|
|
991
|
+
}
|
|
992
|
+
return Object.freeze(roots);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function parseReadbackSchedule(value: unknown, label: string): MetaReadbackSchedule {
|
|
996
|
+
const candidate = record(value, label);
|
|
997
|
+
if (candidate.kind === "none") {
|
|
998
|
+
exactKeys(candidate, ["kind", "reason"], [], label);
|
|
999
|
+
return Object.freeze({
|
|
1000
|
+
kind: "none",
|
|
1001
|
+
reason: boundedString(candidate.reason, `${label}.reason`, 1, 1_024),
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
if (candidate.kind === "independent-query") {
|
|
1005
|
+
exactKeys(
|
|
1006
|
+
candidate,
|
|
1007
|
+
[
|
|
1008
|
+
"kind",
|
|
1009
|
+
"descriptorId",
|
|
1010
|
+
"after",
|
|
1011
|
+
"actorBinding",
|
|
1012
|
+
"targetBinding",
|
|
1013
|
+
"attempts",
|
|
1014
|
+
],
|
|
1015
|
+
[],
|
|
1016
|
+
label,
|
|
1017
|
+
);
|
|
1018
|
+
if (
|
|
1019
|
+
candidate.after !== "dispatch-response"
|
|
1020
|
+
|| candidate.actorBinding !== "same"
|
|
1021
|
+
|| candidate.targetBinding !== "same"
|
|
1022
|
+
|| candidate.attempts !== 1
|
|
1023
|
+
) {
|
|
1024
|
+
throw new Error(`${label} must be one same-actor, same-target independent readback`);
|
|
1025
|
+
}
|
|
1026
|
+
return Object.freeze({
|
|
1027
|
+
kind: "independent-query",
|
|
1028
|
+
descriptorId: exactDescriptorId(candidate.descriptorId, `${label}.descriptorId`),
|
|
1029
|
+
after: "dispatch-response",
|
|
1030
|
+
actorBinding: "same",
|
|
1031
|
+
targetBinding: "same",
|
|
1032
|
+
attempts: 1,
|
|
1033
|
+
});
|
|
1034
|
+
}
|
|
1035
|
+
throw new Error(`${label}.kind must be none or independent-query`);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
function parseDispatchSchedule(value: unknown, label: string): MetaDispatchSchedule {
|
|
1039
|
+
const candidate = record(value, label);
|
|
1040
|
+
if (candidate.kind === "inert") {
|
|
1041
|
+
exactKeys(candidate, ["kind", "dispatches", "readback"], [], label);
|
|
1042
|
+
if (!Array.isArray(candidate.dispatches) || candidate.dispatches.length !== 0) {
|
|
1043
|
+
throw new Error(`${label}.dispatches must be exactly empty while inert`);
|
|
1044
|
+
}
|
|
1045
|
+
const readback = parseReadbackSchedule(candidate.readback, `${label}.readback`);
|
|
1046
|
+
if (readback.kind !== "none") throw new Error(`${label}.inert schedule cannot read back`);
|
|
1047
|
+
return Object.freeze({
|
|
1048
|
+
kind: "inert",
|
|
1049
|
+
dispatches: EMPTY_DISPATCHES,
|
|
1050
|
+
readback,
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
if (candidate.kind === "single-dispatch") {
|
|
1054
|
+
exactKeys(
|
|
1055
|
+
candidate,
|
|
1056
|
+
["kind", "dispatchId", "attempts", "retry", "readback"],
|
|
1057
|
+
[],
|
|
1058
|
+
label,
|
|
1059
|
+
);
|
|
1060
|
+
if (candidate.attempts !== 1 || candidate.retry !== "never") {
|
|
1061
|
+
throw new Error(`${label} must be one non-retried dispatch`);
|
|
1062
|
+
}
|
|
1063
|
+
const readback = parseReadbackSchedule(candidate.readback, `${label}.readback`);
|
|
1064
|
+
if (readback.kind !== "independent-query") {
|
|
1065
|
+
throw new Error(`${label} requires an independent query readback`);
|
|
1066
|
+
}
|
|
1067
|
+
return Object.freeze({
|
|
1068
|
+
kind: "single-dispatch",
|
|
1069
|
+
dispatchId: exactDescriptorId(candidate.dispatchId, `${label}.dispatchId`),
|
|
1070
|
+
attempts: 1,
|
|
1071
|
+
retry: "never",
|
|
1072
|
+
readback,
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
throw new Error(`${label}.kind must be inert or single-dispatch`);
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
export function defineMetaOperationDescriptor(value: unknown): MetaOperationDescriptor {
|
|
1079
|
+
const candidate = record(value, "Meta operation descriptor");
|
|
1080
|
+
const shared = [
|
|
1081
|
+
"schemaVersion",
|
|
1082
|
+
"id",
|
|
1083
|
+
"platform",
|
|
1084
|
+
"kind",
|
|
1085
|
+
"operationType",
|
|
1086
|
+
"friendlyName",
|
|
1087
|
+
"docId",
|
|
1088
|
+
"origin",
|
|
1089
|
+
"method",
|
|
1090
|
+
"path",
|
|
1091
|
+
"contract",
|
|
1092
|
+
"access",
|
|
1093
|
+
"proofs",
|
|
1094
|
+
"variables",
|
|
1095
|
+
"pagination",
|
|
1096
|
+
"responseRoots",
|
|
1097
|
+
] as const;
|
|
1098
|
+
if (candidate.kind === "query") {
|
|
1099
|
+
exactKeys(candidate, shared, [], "Meta query descriptor");
|
|
1100
|
+
} else if (candidate.kind === "mutation") {
|
|
1101
|
+
exactKeys(candidate, [...shared, "schedule"], [], "Meta mutation descriptor");
|
|
1102
|
+
} else {
|
|
1103
|
+
throw new Error("Meta operation descriptor.kind must be query or mutation");
|
|
1104
|
+
}
|
|
1105
|
+
if (candidate.schemaVersion !== 1) {
|
|
1106
|
+
throw new Error("Meta operation descriptor.schemaVersion must be 1");
|
|
1107
|
+
}
|
|
1108
|
+
const platform = exactPlatform(candidate.platform, "Meta operation descriptor.platform");
|
|
1109
|
+
const operationType = exactOperationType(
|
|
1110
|
+
candidate.operationType,
|
|
1111
|
+
"Meta operation descriptor.operationType",
|
|
1112
|
+
);
|
|
1113
|
+
if (candidate.kind !== operationType) {
|
|
1114
|
+
throw new Error("Meta descriptor kind and operationType did not agree");
|
|
1115
|
+
}
|
|
1116
|
+
const method = exactMethod(candidate.method, "Meta operation descriptor.method");
|
|
1117
|
+
if (operationType === "mutation" && method !== "POST") {
|
|
1118
|
+
throw new Error("Meta Relay mutations require POST");
|
|
1119
|
+
}
|
|
1120
|
+
const origin = exactOrigin(candidate.origin, platform, "Meta operation descriptor.origin");
|
|
1121
|
+
const path = exactPath(candidate.path, platform, "Meta operation descriptor.path");
|
|
1122
|
+
const contract = parseContract(candidate.contract, "Meta operation descriptor.contract");
|
|
1123
|
+
const access = parseAccessPolicy(
|
|
1124
|
+
candidate.access,
|
|
1125
|
+
platform,
|
|
1126
|
+
"Meta operation descriptor.access",
|
|
1127
|
+
);
|
|
1128
|
+
const proofs = parseProofDeclarations(
|
|
1129
|
+
candidate.proofs,
|
|
1130
|
+
method,
|
|
1131
|
+
"Meta operation descriptor.proofs",
|
|
1132
|
+
);
|
|
1133
|
+
const variables = parseVariableDefinition(
|
|
1134
|
+
candidate.variables,
|
|
1135
|
+
"Meta operation descriptor.variables",
|
|
1136
|
+
);
|
|
1137
|
+
const pagination = parsePaginationPolicy(
|
|
1138
|
+
candidate.pagination,
|
|
1139
|
+
variables,
|
|
1140
|
+
"Meta operation descriptor.pagination",
|
|
1141
|
+
);
|
|
1142
|
+
const id = exactDescriptorId(candidate.id, "Meta operation descriptor.id");
|
|
1143
|
+
const common = {
|
|
1144
|
+
schemaVersion: 1 as const,
|
|
1145
|
+
id,
|
|
1146
|
+
platform,
|
|
1147
|
+
friendlyName: assertMetaFriendlyName(candidate.friendlyName),
|
|
1148
|
+
docId: assertMetaDocId(candidate.docId),
|
|
1149
|
+
origin,
|
|
1150
|
+
method,
|
|
1151
|
+
path,
|
|
1152
|
+
contract,
|
|
1153
|
+
access,
|
|
1154
|
+
proofs,
|
|
1155
|
+
variables,
|
|
1156
|
+
pagination,
|
|
1157
|
+
};
|
|
1158
|
+
if (operationType === "query") {
|
|
1159
|
+
return Object.freeze({
|
|
1160
|
+
...common,
|
|
1161
|
+
kind: "query",
|
|
1162
|
+
operationType: "query",
|
|
1163
|
+
method,
|
|
1164
|
+
responseRoots: parseResponseRoots(
|
|
1165
|
+
candidate.responseRoots,
|
|
1166
|
+
"query",
|
|
1167
|
+
"Meta operation descriptor.responseRoots",
|
|
1168
|
+
) as readonly MetaQueryResponseRootVariant[],
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
const schedule = parseDispatchSchedule(
|
|
1172
|
+
candidate.schedule,
|
|
1173
|
+
"Meta operation descriptor.schedule",
|
|
1174
|
+
);
|
|
1175
|
+
if (contract.state === "capture-required" && schedule.kind !== "inert") {
|
|
1176
|
+
throw new Error("capture-required Meta mutations must have an inert schedule");
|
|
1177
|
+
}
|
|
1178
|
+
if (contract.state === "observed" && schedule.kind !== "single-dispatch") {
|
|
1179
|
+
throw new Error("observed Meta mutations require one exact dispatch and readback schedule");
|
|
1180
|
+
}
|
|
1181
|
+
if (
|
|
1182
|
+
schedule.kind === "single-dispatch"
|
|
1183
|
+
&& schedule.readback.descriptorId === id
|
|
1184
|
+
) {
|
|
1185
|
+
throw new Error("Meta mutation readback must use a separate query descriptor");
|
|
1186
|
+
}
|
|
1187
|
+
return Object.freeze({
|
|
1188
|
+
...common,
|
|
1189
|
+
kind: "mutation",
|
|
1190
|
+
operationType: "mutation",
|
|
1191
|
+
method: "POST",
|
|
1192
|
+
responseRoots: parseResponseRoots(
|
|
1193
|
+
candidate.responseRoots,
|
|
1194
|
+
"mutation",
|
|
1195
|
+
"Meta operation descriptor.responseRoots",
|
|
1196
|
+
) as readonly MetaMutationResponseRootVariant[],
|
|
1197
|
+
schedule,
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function parseObservedDescriptor(
|
|
1202
|
+
value: unknown,
|
|
1203
|
+
label: string,
|
|
1204
|
+
): MetaObservedOperationDescriptor {
|
|
1205
|
+
const candidate = record(value, label);
|
|
1206
|
+
exactKeys(
|
|
1207
|
+
candidate,
|
|
1208
|
+
["friendlyName", "docId", "operationType", "origin", "method", "path"],
|
|
1209
|
+
[],
|
|
1210
|
+
label,
|
|
1211
|
+
);
|
|
1212
|
+
const origin = candidate.origin;
|
|
1213
|
+
let platform: MetaRelayPlatform;
|
|
1214
|
+
if (origin === META_RELAY_ORIGINS.facebook) platform = "facebook";
|
|
1215
|
+
else if (origin === META_RELAY_ORIGINS.instagram) platform = "instagram";
|
|
1216
|
+
else if (origin === META_RELAY_ORIGINS.threads) platform = "threads";
|
|
1217
|
+
else throw new Error(`${label}.origin is not an exact Meta first-party origin`);
|
|
1218
|
+
return Object.freeze({
|
|
1219
|
+
friendlyName: assertMetaFriendlyName(candidate.friendlyName),
|
|
1220
|
+
docId: assertMetaDocId(candidate.docId),
|
|
1221
|
+
operationType: exactOperationType(candidate.operationType, `${label}.operationType`),
|
|
1222
|
+
origin: exactOrigin(candidate.origin, platform, `${label}.origin`),
|
|
1223
|
+
method: exactMethod(candidate.method, `${label}.method`),
|
|
1224
|
+
path: exactPath(candidate.path, platform, `${label}.path`),
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* Resolve one current observed operation against a code-owned descriptor.
|
|
1230
|
+
* Duplicate candidates, type/transport drift, and revision drift fail closed.
|
|
1231
|
+
*/
|
|
1232
|
+
export function resolveMetaOperationDescriptor(
|
|
1233
|
+
candidatesValue: unknown,
|
|
1234
|
+
expectedValue: MetaOperationDescriptor,
|
|
1235
|
+
): MetaObservedOperationDescriptor {
|
|
1236
|
+
if (!Array.isArray(candidatesValue)) {
|
|
1237
|
+
throw new Error("observed Meta descriptors must be an array");
|
|
1238
|
+
}
|
|
1239
|
+
const expected = defineMetaOperationDescriptor(expectedValue);
|
|
1240
|
+
const candidates = candidatesValue.map((candidate, index) => (
|
|
1241
|
+
parseObservedDescriptor(candidate, `observed Meta descriptor ${index + 1}`)
|
|
1242
|
+
));
|
|
1243
|
+
const named = candidates.filter(
|
|
1244
|
+
(candidate) => candidate.friendlyName === expected.friendlyName,
|
|
1245
|
+
);
|
|
1246
|
+
if (named.length === 0) {
|
|
1247
|
+
throw new Error(`observed Meta descriptors omitted ${expected.friendlyName}`);
|
|
1248
|
+
}
|
|
1249
|
+
if (named.some((candidate) => candidate.operationType !== expected.operationType)) {
|
|
1250
|
+
throw new Error(`Meta operation-type drift for ${expected.friendlyName}`);
|
|
1251
|
+
}
|
|
1252
|
+
const typed = named.filter(
|
|
1253
|
+
(candidate) => candidate.operationType === expected.operationType,
|
|
1254
|
+
);
|
|
1255
|
+
if (
|
|
1256
|
+
typed.some((candidate) => (
|
|
1257
|
+
candidate.origin !== expected.origin
|
|
1258
|
+
|| candidate.method !== expected.method
|
|
1259
|
+
|| candidate.path !== expected.path
|
|
1260
|
+
))
|
|
1261
|
+
) {
|
|
1262
|
+
throw new Error(`Meta transport drift for ${expected.friendlyName}`);
|
|
1263
|
+
}
|
|
1264
|
+
if (typed.length > 1) {
|
|
1265
|
+
const revisions = new Set(typed.map((candidate) => candidate.docId));
|
|
1266
|
+
if (revisions.size === 1) {
|
|
1267
|
+
throw new Error(`observed Meta descriptors contained duplicate ${expected.friendlyName}`);
|
|
1268
|
+
}
|
|
1269
|
+
throw new Error(`observed Meta descriptors contained ambiguous revision drift for ${expected.friendlyName}`);
|
|
1270
|
+
}
|
|
1271
|
+
const resolved = typed[0];
|
|
1272
|
+
if (resolved === undefined) {
|
|
1273
|
+
throw new Error(`observed Meta descriptors omitted ${expected.friendlyName}`);
|
|
1274
|
+
}
|
|
1275
|
+
if (resolved.docId !== expected.docId) {
|
|
1276
|
+
throw new Error(`Meta docId drift for ${expected.friendlyName}; reviewed evidence is stale`);
|
|
1277
|
+
}
|
|
1278
|
+
return resolved;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
export function metaOperationDescriptorKey(value: MetaOperationDescriptor): string {
|
|
1282
|
+
const descriptor = defineMetaOperationDescriptor(value);
|
|
1283
|
+
return `meta1:${sha256(canonicalJson(descriptor))}`;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
export function bindMetaAccessContext(
|
|
1287
|
+
descriptorValue: MetaOperationDescriptor,
|
|
1288
|
+
value: unknown,
|
|
1289
|
+
): MetaAccessContext {
|
|
1290
|
+
const descriptor = defineMetaOperationDescriptor(descriptorValue);
|
|
1291
|
+
const candidate = record(value, "Meta access context");
|
|
1292
|
+
exactKeys(
|
|
1293
|
+
candidate,
|
|
1294
|
+
["kind", "platform", "viewerId", "actorId", "targetId"],
|
|
1295
|
+
[],
|
|
1296
|
+
"Meta access context",
|
|
1297
|
+
);
|
|
1298
|
+
if (candidate.kind !== descriptor.access.kind) {
|
|
1299
|
+
throw new Error("Meta access context kind did not match its descriptor");
|
|
1300
|
+
}
|
|
1301
|
+
if (candidate.platform !== descriptor.platform) {
|
|
1302
|
+
throw new Error("Meta access context platform did not match its descriptor");
|
|
1303
|
+
}
|
|
1304
|
+
const viewerId = exactIdentifier(candidate.viewerId, "Meta access context.viewerId");
|
|
1305
|
+
const actorId = exactIdentifier(candidate.actorId, "Meta access context.actorId");
|
|
1306
|
+
const targetId = exactIdentifier(candidate.targetId, "Meta access context.targetId");
|
|
1307
|
+
const expectedActor = descriptor.access.actorBinding === "viewer" ? viewerId : targetId;
|
|
1308
|
+
if (actorId !== expectedActor) {
|
|
1309
|
+
throw new Error(
|
|
1310
|
+
`Meta ${descriptor.access.kind} actor did not match its ${descriptor.access.actorBinding}`,
|
|
1311
|
+
);
|
|
1312
|
+
}
|
|
1313
|
+
if (descriptor.access.kind === "personal") {
|
|
1314
|
+
return Object.freeze({
|
|
1315
|
+
kind: "personal",
|
|
1316
|
+
platform: descriptor.platform,
|
|
1317
|
+
viewerId,
|
|
1318
|
+
actorId,
|
|
1319
|
+
targetId,
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
if (descriptor.platform !== "facebook") {
|
|
1323
|
+
throw new Error(`Meta ${descriptor.access.kind} access is available only on facebook`);
|
|
1324
|
+
}
|
|
1325
|
+
return Object.freeze({
|
|
1326
|
+
kind: descriptor.access.kind,
|
|
1327
|
+
platform: "facebook",
|
|
1328
|
+
viewerId,
|
|
1329
|
+
actorId,
|
|
1330
|
+
targetId,
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
function validateVariableValue(
|
|
1335
|
+
schema: MetaVariableSchema,
|
|
1336
|
+
value: unknown,
|
|
1337
|
+
label: string,
|
|
1338
|
+
): unknown {
|
|
1339
|
+
switch (schema.kind) {
|
|
1340
|
+
case "id":
|
|
1341
|
+
return exactIdentifier(value, label);
|
|
1342
|
+
case "cursor":
|
|
1343
|
+
return exactCursor(value, label);
|
|
1344
|
+
case "string":
|
|
1345
|
+
return boundedString(value, label, schema.minimumLength, schema.maximumLength);
|
|
1346
|
+
case "boolean":
|
|
1347
|
+
if (typeof value !== "boolean") throw new Error(`${label} must be boolean`);
|
|
1348
|
+
return value;
|
|
1349
|
+
case "integer":
|
|
1350
|
+
return boundedInteger(value, label, schema.minimum, schema.maximum);
|
|
1351
|
+
case "number": {
|
|
1352
|
+
const number = boundedFiniteNumber(value, label);
|
|
1353
|
+
if (number < schema.minimum || number > schema.maximum) {
|
|
1354
|
+
throw new Error(`${label} must be a number between ${schema.minimum} and ${schema.maximum}`);
|
|
1355
|
+
}
|
|
1356
|
+
return number;
|
|
1357
|
+
}
|
|
1358
|
+
case "enum":
|
|
1359
|
+
if (typeof value !== "string" || !schema.values.includes(value)) {
|
|
1360
|
+
throw new Error(`${label} must be one exact reviewed enum value`);
|
|
1361
|
+
}
|
|
1362
|
+
return value;
|
|
1363
|
+
case "literal":
|
|
1364
|
+
if (!Object.is(value, schema.value)) throw new Error(`${label} did not match its exact literal`);
|
|
1365
|
+
return schema.value;
|
|
1366
|
+
case "nullable":
|
|
1367
|
+
return value === null ? null : validateVariableValue(schema.value, value, label);
|
|
1368
|
+
case "list": {
|
|
1369
|
+
if (
|
|
1370
|
+
!Array.isArray(value)
|
|
1371
|
+
|| value.length < schema.minimumItems
|
|
1372
|
+
|| value.length > schema.maximumItems
|
|
1373
|
+
) {
|
|
1374
|
+
throw new Error(`${label} must be a bounded array`);
|
|
1375
|
+
}
|
|
1376
|
+
return Object.freeze(value.map((item, index) => (
|
|
1377
|
+
validateVariableValue(schema.items, item, `${label}[${index}]`)
|
|
1378
|
+
)));
|
|
1379
|
+
}
|
|
1380
|
+
case "object": {
|
|
1381
|
+
const source = record(value, label);
|
|
1382
|
+
const required = schema.fields
|
|
1383
|
+
.filter((field) => !field.optional)
|
|
1384
|
+
.map((field) => field.name);
|
|
1385
|
+
const optional = schema.fields
|
|
1386
|
+
.filter((field) => field.optional)
|
|
1387
|
+
.map((field) => field.name);
|
|
1388
|
+
exactKeys(source, required, optional, label);
|
|
1389
|
+
const result: Record<string, unknown> = {};
|
|
1390
|
+
for (const field of schema.fields) {
|
|
1391
|
+
if (!Object.hasOwn(source, field.name)) continue;
|
|
1392
|
+
result[field.name] = validateVariableValue(
|
|
1393
|
+
field.schema,
|
|
1394
|
+
source[field.name],
|
|
1395
|
+
`${label}.${field.name}`,
|
|
1396
|
+
);
|
|
1397
|
+
}
|
|
1398
|
+
return Object.freeze(result);
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function assertPaginationCursorBinding(
|
|
1404
|
+
descriptor: MetaOperationDescriptor,
|
|
1405
|
+
access: MetaAccessContext,
|
|
1406
|
+
value: unknown,
|
|
1407
|
+
): MetaPaginationCursor {
|
|
1408
|
+
if (descriptor.pagination.kind !== "cursor") {
|
|
1409
|
+
throw new Error("Meta operation does not permit pagination");
|
|
1410
|
+
}
|
|
1411
|
+
if (!isRecord(value) || !issuedPaginationCursors.has(value)) {
|
|
1412
|
+
throw new Error("Meta pagination cursor was not issued by the binding policy");
|
|
1413
|
+
}
|
|
1414
|
+
exactKeys(
|
|
1415
|
+
value,
|
|
1416
|
+
[
|
|
1417
|
+
"schemaVersion",
|
|
1418
|
+
"descriptorKey",
|
|
1419
|
+
"actorId",
|
|
1420
|
+
"targetId",
|
|
1421
|
+
"cursor",
|
|
1422
|
+
"previousCursor",
|
|
1423
|
+
],
|
|
1424
|
+
[],
|
|
1425
|
+
"Meta pagination cursor",
|
|
1426
|
+
);
|
|
1427
|
+
if (value.schemaVersion !== 1) {
|
|
1428
|
+
throw new Error("Meta pagination cursor schemaVersion must be 1");
|
|
1429
|
+
}
|
|
1430
|
+
if (value.descriptorKey !== metaOperationDescriptorKey(descriptor)) {
|
|
1431
|
+
throw new Error("Meta pagination cursor did not match its descriptor");
|
|
1432
|
+
}
|
|
1433
|
+
if (value.actorId !== access.actorId) {
|
|
1434
|
+
throw new Error("Meta pagination cursor did not match its actor");
|
|
1435
|
+
}
|
|
1436
|
+
if (value.targetId !== access.targetId) {
|
|
1437
|
+
throw new Error("Meta pagination cursor did not match its target");
|
|
1438
|
+
}
|
|
1439
|
+
const cursor = exactCursor(value.cursor, "Meta pagination cursor.cursor");
|
|
1440
|
+
const previousCursor = value.previousCursor === null
|
|
1441
|
+
? null
|
|
1442
|
+
: exactCursor(value.previousCursor, "Meta pagination cursor.previousCursor");
|
|
1443
|
+
if (cursor === previousCursor) {
|
|
1444
|
+
throw new Error("Meta pagination cursor did not advance");
|
|
1445
|
+
}
|
|
1446
|
+
return value as MetaPaginationCursor;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
export function assertMetaPaginationCursorBinding(
|
|
1450
|
+
descriptorValue: MetaOperationDescriptor,
|
|
1451
|
+
accessValue: unknown,
|
|
1452
|
+
cursorValue: unknown,
|
|
1453
|
+
): MetaPaginationCursor {
|
|
1454
|
+
const descriptor = defineMetaOperationDescriptor(descriptorValue);
|
|
1455
|
+
const access = bindMetaAccessContext(descriptor, accessValue);
|
|
1456
|
+
return assertPaginationCursorBinding(descriptor, access, cursorValue);
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
/**
|
|
1460
|
+
* Bind a provider continuation to the exact descriptor revision and access
|
|
1461
|
+
* context that produced it. Passing a previous binding records and validates
|
|
1462
|
+
* the one-step cursor chain.
|
|
1463
|
+
*/
|
|
1464
|
+
export function bindMetaPaginationCursor(
|
|
1465
|
+
descriptorValue: MetaOperationDescriptor,
|
|
1466
|
+
accessValue: unknown,
|
|
1467
|
+
cursorValue: unknown,
|
|
1468
|
+
previousValue: MetaPaginationCursor | null = null,
|
|
1469
|
+
): MetaPaginationCursor {
|
|
1470
|
+
const descriptor = defineMetaOperationDescriptor(descriptorValue);
|
|
1471
|
+
const access = bindMetaAccessContext(descriptor, accessValue);
|
|
1472
|
+
if (descriptor.pagination.kind !== "cursor") {
|
|
1473
|
+
throw new Error("Meta operation does not permit pagination");
|
|
1474
|
+
}
|
|
1475
|
+
const cursor = exactCursor(cursorValue, "Meta next pagination cursor");
|
|
1476
|
+
const previous = previousValue === null
|
|
1477
|
+
? null
|
|
1478
|
+
: assertPaginationCursorBinding(descriptor, access, previousValue);
|
|
1479
|
+
if (previous?.cursor === cursor) {
|
|
1480
|
+
throw new Error("Meta pagination cursor did not advance");
|
|
1481
|
+
}
|
|
1482
|
+
const result: MetaPaginationCursor = Object.freeze({
|
|
1483
|
+
schemaVersion: 1,
|
|
1484
|
+
descriptorKey: metaOperationDescriptorKey(descriptor),
|
|
1485
|
+
actorId: access.actorId,
|
|
1486
|
+
targetId: access.targetId,
|
|
1487
|
+
cursor,
|
|
1488
|
+
previousCursor: previous?.cursor ?? null,
|
|
1489
|
+
});
|
|
1490
|
+
issuedPaginationCursors.add(result);
|
|
1491
|
+
return result;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
export function buildMetaRelayVariables(
|
|
1495
|
+
descriptorValue: MetaOperationDescriptor,
|
|
1496
|
+
inputValue: unknown,
|
|
1497
|
+
accessValue: unknown,
|
|
1498
|
+
paginationValue: MetaPaginationCursor | null = null,
|
|
1499
|
+
): Readonly<Record<string, unknown>> {
|
|
1500
|
+
const descriptor = defineMetaOperationDescriptor(descriptorValue);
|
|
1501
|
+
const access = bindMetaAccessContext(descriptor, accessValue);
|
|
1502
|
+
const input = record(inputValue, "Meta semantic input");
|
|
1503
|
+
const inputKeys = new Set(
|
|
1504
|
+
descriptor.variables.fields.flatMap((field) => (
|
|
1505
|
+
field.source.kind === "input" ? [field.source.key] : []
|
|
1506
|
+
)),
|
|
1507
|
+
);
|
|
1508
|
+
const extra = Object.keys(input).filter((key) => !inputKeys.has(key));
|
|
1509
|
+
if (extra.length > 0) {
|
|
1510
|
+
throw new Error(`Meta semantic input contained unsupported field(s): ${extra.join(", ")}`);
|
|
1511
|
+
}
|
|
1512
|
+
const pagination = paginationValue === null
|
|
1513
|
+
? null
|
|
1514
|
+
: assertPaginationCursorBinding(descriptor, access, paginationValue);
|
|
1515
|
+
const result: Record<string, unknown> = {};
|
|
1516
|
+
for (const field of descriptor.variables.fields) {
|
|
1517
|
+
let sourceValue: unknown;
|
|
1518
|
+
let present = true;
|
|
1519
|
+
switch (field.source.kind) {
|
|
1520
|
+
case "input":
|
|
1521
|
+
present = Object.hasOwn(input, field.source.key);
|
|
1522
|
+
sourceValue = input[field.source.key];
|
|
1523
|
+
break;
|
|
1524
|
+
case "viewer":
|
|
1525
|
+
sourceValue = access.viewerId;
|
|
1526
|
+
break;
|
|
1527
|
+
case "actor":
|
|
1528
|
+
sourceValue = access.actorId;
|
|
1529
|
+
break;
|
|
1530
|
+
case "target":
|
|
1531
|
+
sourceValue = access.targetId;
|
|
1532
|
+
break;
|
|
1533
|
+
case "pagination":
|
|
1534
|
+
present = pagination !== null;
|
|
1535
|
+
sourceValue = pagination?.cursor;
|
|
1536
|
+
break;
|
|
1537
|
+
case "literal":
|
|
1538
|
+
sourceValue = field.source.value;
|
|
1539
|
+
break;
|
|
1540
|
+
}
|
|
1541
|
+
if (!present) {
|
|
1542
|
+
if (!field.optional) {
|
|
1543
|
+
const sourceName = field.source.kind === "input"
|
|
1544
|
+
? `input.${field.source.key}`
|
|
1545
|
+
: field.name;
|
|
1546
|
+
throw new Error(`Meta variables omitted required ${sourceName}`);
|
|
1547
|
+
}
|
|
1548
|
+
continue;
|
|
1549
|
+
}
|
|
1550
|
+
result[field.name] = validateVariableValue(
|
|
1551
|
+
field.schema,
|
|
1552
|
+
sourceValue,
|
|
1553
|
+
`Meta variables.${field.name}`,
|
|
1554
|
+
);
|
|
1555
|
+
}
|
|
1556
|
+
return Object.freeze(result);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
function proofFormFields(
|
|
1560
|
+
proofs: readonly MetaBootstrapProofDeclaration[],
|
|
1561
|
+
): readonly MetaRelayProofFormFieldName[] {
|
|
1562
|
+
const fields: MetaRelayProofFormFieldName[] = [];
|
|
1563
|
+
for (const proof of proofs) {
|
|
1564
|
+
for (const sink of proof.sinks) {
|
|
1565
|
+
switch (sink) {
|
|
1566
|
+
case "form.__user": fields.push("__user"); break;
|
|
1567
|
+
case "form.av": fields.push("av"); break;
|
|
1568
|
+
case "form.fb_dtsg": fields.push("fb_dtsg"); break;
|
|
1569
|
+
case "form.jazoest": fields.push("jazoest"); break;
|
|
1570
|
+
case "form.lsd": fields.push("lsd"); break;
|
|
1571
|
+
case "form.__rev": fields.push("__rev"); break;
|
|
1572
|
+
case "form.__hsi": fields.push("__hsi"); break;
|
|
1573
|
+
case "form.__comet_req": fields.push("__comet_req"); break;
|
|
1574
|
+
case "form.__req": fields.push("__req"); break;
|
|
1575
|
+
case "access.viewer-id":
|
|
1576
|
+
case "access.actor-id":
|
|
1577
|
+
break;
|
|
1578
|
+
}
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return Object.freeze(fields);
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
/**
|
|
1585
|
+
* Build a network-inert exact request template. Raw bootstrap proof values are
|
|
1586
|
+
* not accepted or returned; an opaque bootstrap boundary must consume each
|
|
1587
|
+
* declared proof directly into the named form sink.
|
|
1588
|
+
*/
|
|
1589
|
+
export function buildMetaRelayRequest(
|
|
1590
|
+
descriptorValue: MetaOperationDescriptor,
|
|
1591
|
+
value: unknown,
|
|
1592
|
+
): MetaRelayRequest {
|
|
1593
|
+
const descriptor = defineMetaOperationDescriptor(descriptorValue);
|
|
1594
|
+
if (descriptor.contract.state !== "observed") {
|
|
1595
|
+
throw new Error(
|
|
1596
|
+
`Meta operation ${descriptor.id} is capture-required and cannot build a request`,
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
const candidate = record(value, "Meta Relay request build input");
|
|
1600
|
+
exactKeys(
|
|
1601
|
+
candidate,
|
|
1602
|
+
["input", "access"],
|
|
1603
|
+
["pagination"],
|
|
1604
|
+
"Meta Relay request build input",
|
|
1605
|
+
);
|
|
1606
|
+
const access = bindMetaAccessContext(descriptor, candidate.access);
|
|
1607
|
+
const paginationValue = candidate.pagination === undefined || candidate.pagination === null
|
|
1608
|
+
? null
|
|
1609
|
+
: assertPaginationCursorBinding(descriptor, access, candidate.pagination);
|
|
1610
|
+
const variables = buildMetaRelayVariables(
|
|
1611
|
+
descriptor,
|
|
1612
|
+
candidate.input,
|
|
1613
|
+
access,
|
|
1614
|
+
paginationValue,
|
|
1615
|
+
);
|
|
1616
|
+
const parameters: readonly MetaRelayRequestParameter[] = Object.freeze([
|
|
1617
|
+
Object.freeze({
|
|
1618
|
+
name: "fb_api_req_friendly_name",
|
|
1619
|
+
value: descriptor.friendlyName,
|
|
1620
|
+
}),
|
|
1621
|
+
Object.freeze({ name: "doc_id", value: descriptor.docId }),
|
|
1622
|
+
Object.freeze({ name: "variables", value: JSON.stringify(variables) }),
|
|
1623
|
+
]);
|
|
1624
|
+
const request: MetaRelayRequest = Object.freeze({
|
|
1625
|
+
descriptorId: descriptor.id,
|
|
1626
|
+
operationType: descriptor.operationType,
|
|
1627
|
+
origin: descriptor.origin,
|
|
1628
|
+
method: descriptor.method,
|
|
1629
|
+
path: descriptor.path,
|
|
1630
|
+
url: `${descriptor.origin}${descriptor.path}`,
|
|
1631
|
+
parameterLocation: descriptor.method === "GET" ? "query" : "form",
|
|
1632
|
+
parameters,
|
|
1633
|
+
proofBindings: descriptor.proofs,
|
|
1634
|
+
proofFormFields: proofFormFields(descriptor.proofs),
|
|
1635
|
+
access,
|
|
1636
|
+
pagination: paginationValue,
|
|
1637
|
+
schedule: descriptor.kind === "mutation" ? descriptor.schedule : null,
|
|
1638
|
+
});
|
|
1639
|
+
issuedRelayRequestCoordinates.set(request, Object.freeze({
|
|
1640
|
+
descriptorKey: metaOperationDescriptorKey(descriptor),
|
|
1641
|
+
viewerId: access.viewerId,
|
|
1642
|
+
actorId: access.actorId,
|
|
1643
|
+
targetId: access.targetId,
|
|
1644
|
+
proofFormFields: request.proofFormFields,
|
|
1645
|
+
}));
|
|
1646
|
+
return request;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
export function metaRelayRequestProofCoordinates(
|
|
1650
|
+
value: unknown,
|
|
1651
|
+
): MetaRelayRequestProofCoordinates {
|
|
1652
|
+
if (!isRecord(value)) {
|
|
1653
|
+
throw new Error("Meta Relay request handle is invalid");
|
|
1654
|
+
}
|
|
1655
|
+
const coordinates = issuedRelayRequestCoordinates.get(value as MetaRelayRequest);
|
|
1656
|
+
if (coordinates === undefined) {
|
|
1657
|
+
throw new Error("Meta Relay request handle is invalid");
|
|
1658
|
+
}
|
|
1659
|
+
return coordinates;
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
function valueAtPath(value: unknown, path: readonly string[]): unknown {
|
|
1663
|
+
let current = value;
|
|
1664
|
+
for (const segment of path) {
|
|
1665
|
+
if (!isRecord(current) || !Object.hasOwn(current, segment)) return undefined;
|
|
1666
|
+
current = current[segment];
|
|
1667
|
+
}
|
|
1668
|
+
return current;
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
/**
|
|
1672
|
+
* Select exactly one reviewed response root. Alternate roots are explicit
|
|
1673
|
+
* descriptor variants; a response matching more than one is ambiguous drift.
|
|
1674
|
+
*/
|
|
1675
|
+
export function assertMetaRelayResponseBinding(
|
|
1676
|
+
descriptorValue: MetaOperationDescriptor,
|
|
1677
|
+
responseValue: unknown,
|
|
1678
|
+
): MetaRelayResponseBinding {
|
|
1679
|
+
const descriptor = defineMetaOperationDescriptor(descriptorValue);
|
|
1680
|
+
const response = record(responseValue, "Meta Relay response");
|
|
1681
|
+
if (Object.hasOwn(response, "errors")) {
|
|
1682
|
+
if (!Array.isArray(response.errors)) {
|
|
1683
|
+
throw new Error("Meta Relay response.errors must be an array");
|
|
1684
|
+
}
|
|
1685
|
+
if (response.errors.length > 0) {
|
|
1686
|
+
throw new Error("Meta Relay response contained provider errors");
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
const matches = descriptor.responseRoots.flatMap((variant) => {
|
|
1690
|
+
const value = valueAtPath(response, variant.path);
|
|
1691
|
+
return value === undefined || value === null ? [] : [{ variant, value }];
|
|
1692
|
+
});
|
|
1693
|
+
if (matches.length === 0) {
|
|
1694
|
+
throw new Error("Meta Relay response omitted every reviewed root variant");
|
|
1695
|
+
}
|
|
1696
|
+
if (matches.length > 1) {
|
|
1697
|
+
throw new Error("Meta Relay response matched multiple reviewed root variants");
|
|
1698
|
+
}
|
|
1699
|
+
const match = matches[0];
|
|
1700
|
+
if (match === undefined) {
|
|
1701
|
+
throw new Error("Meta Relay response omitted every reviewed root variant");
|
|
1702
|
+
}
|
|
1703
|
+
return Object.freeze({
|
|
1704
|
+
descriptorId: descriptor.id,
|
|
1705
|
+
operationType: descriptor.operationType,
|
|
1706
|
+
variant: match.variant,
|
|
1707
|
+
value: match.value,
|
|
1708
|
+
});
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
export function assertMetaDispatchScheduleBinding(
|
|
1712
|
+
mutationValue: MetaOperationDescriptor,
|
|
1713
|
+
readbackValue: MetaOperationDescriptor,
|
|
1714
|
+
): MetaDispatchSchedule {
|
|
1715
|
+
const mutation = defineMetaOperationDescriptor(mutationValue);
|
|
1716
|
+
const readback = defineMetaOperationDescriptor(readbackValue);
|
|
1717
|
+
if (mutation.kind !== "mutation") {
|
|
1718
|
+
throw new Error("Meta dispatch schedule requires a mutation descriptor");
|
|
1719
|
+
}
|
|
1720
|
+
if (mutation.schedule.kind !== "single-dispatch") {
|
|
1721
|
+
throw new Error("Meta mutation dispatch schedule is inert");
|
|
1722
|
+
}
|
|
1723
|
+
if (readback.kind !== "query") {
|
|
1724
|
+
throw new Error("Meta mutation readback descriptor must be a query");
|
|
1725
|
+
}
|
|
1726
|
+
if (mutation.schedule.readback.descriptorId !== readback.id) {
|
|
1727
|
+
throw new Error("Meta mutation readback descriptor ID did not match its schedule");
|
|
1728
|
+
}
|
|
1729
|
+
if (
|
|
1730
|
+
mutation.platform !== readback.platform
|
|
1731
|
+
|| mutation.access.kind !== readback.access.kind
|
|
1732
|
+
|| mutation.access.actorBinding !== readback.access.actorBinding
|
|
1733
|
+
) {
|
|
1734
|
+
throw new Error("Meta mutation readback did not preserve its actor and target access policy");
|
|
1735
|
+
}
|
|
1736
|
+
if (readback.contract.state !== "observed") {
|
|
1737
|
+
throw new Error("Meta mutation readback descriptor is capture-required");
|
|
1738
|
+
}
|
|
1739
|
+
return mutation.schedule;
|
|
1740
|
+
}
|