@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,4907 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
constants,
|
|
5
|
+
existsSync,
|
|
6
|
+
fstatSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
openSync,
|
|
9
|
+
opendirSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
readlinkSync,
|
|
12
|
+
readSync,
|
|
13
|
+
realpathSync,
|
|
14
|
+
type BigIntStats,
|
|
15
|
+
} from "node:fs";
|
|
16
|
+
import { builtinModules, createRequire } from "node:module";
|
|
17
|
+
import {
|
|
18
|
+
basename,
|
|
19
|
+
dirname,
|
|
20
|
+
extname,
|
|
21
|
+
isAbsolute,
|
|
22
|
+
parse,
|
|
23
|
+
relative,
|
|
24
|
+
resolve,
|
|
25
|
+
sep,
|
|
26
|
+
} from "node:path";
|
|
27
|
+
import { fileURLToPath } from "node:url";
|
|
28
|
+
|
|
29
|
+
import type { GhostgetAuth } from "./auth";
|
|
30
|
+
import type { MessageLikeMeSourceConversationCoordinateBindingV1 } from "./message-like-me-agentic-messaging";
|
|
31
|
+
import type {
|
|
32
|
+
BrowserDispatchPlan,
|
|
33
|
+
ArrayInputField,
|
|
34
|
+
FileInputField,
|
|
35
|
+
IdempotencyKind,
|
|
36
|
+
InputField,
|
|
37
|
+
InputSchema,
|
|
38
|
+
OperationInput,
|
|
39
|
+
OperationRisk,
|
|
40
|
+
GhostgetManifest,
|
|
41
|
+
ScalarInputField,
|
|
42
|
+
} from "./model";
|
|
43
|
+
import type { ProviderActionContext } from "./provider-context";
|
|
44
|
+
import type {
|
|
45
|
+
ProviderConversationV1,
|
|
46
|
+
ProviderMaterializedPageV1,
|
|
47
|
+
ProviderMessageV1,
|
|
48
|
+
} from "./omni-model";
|
|
49
|
+
import type { LocalCliOperationExecutor } from "./local-cli-execution";
|
|
50
|
+
import type { OperationDeadline } from "./operation-deadline";
|
|
51
|
+
import {
|
|
52
|
+
localCliToolArtifactForCurrentRuntime,
|
|
53
|
+
parseLocalCliToolIdentityV1,
|
|
54
|
+
type LocalCliToolArtifactIdentityV1,
|
|
55
|
+
type LocalCliToolIdentityV1,
|
|
56
|
+
} from "./local-cli-tool-identity";
|
|
57
|
+
import {
|
|
58
|
+
isProviderPluginId,
|
|
59
|
+
isProviderPluginOperationName,
|
|
60
|
+
isProviderPluginSurfaceId,
|
|
61
|
+
} from "./provider-plugin-identifiers";
|
|
62
|
+
import {
|
|
63
|
+
createPortableOperationIdentityV1,
|
|
64
|
+
type PortableOperationIdentityV1,
|
|
65
|
+
} from "./provider-plugin-portable-identity";
|
|
66
|
+
import {
|
|
67
|
+
isVerifiedPortableProviderPluginPackage,
|
|
68
|
+
type PortableProviderPluginBindingV1 as PortablePackageBindingV1,
|
|
69
|
+
type VerifiedPortableProviderPluginPackage,
|
|
70
|
+
} from "./provider-plugin-package";
|
|
71
|
+
import {
|
|
72
|
+
assertKernelPortableProviderPluginBindingProjection,
|
|
73
|
+
} from "./provider-plugin-portable-authority";
|
|
74
|
+
import {
|
|
75
|
+
inspectLocalCliCleanupFilesystemReadiness,
|
|
76
|
+
} from "./provider-plugin-cleanup-resource";
|
|
77
|
+
import type {
|
|
78
|
+
ProviderPluginCleanupBarrierRegistrar,
|
|
79
|
+
PublicWebSessionOperationExecutor,
|
|
80
|
+
WebSessionOperationExecutor,
|
|
81
|
+
} from "./web-session-execution";
|
|
82
|
+
|
|
83
|
+
export const PROVIDER_PLUGIN_API_VERSION = 1 as const;
|
|
84
|
+
|
|
85
|
+
export const providerPluginTransports = [
|
|
86
|
+
"provider-api",
|
|
87
|
+
"web-session-api",
|
|
88
|
+
"linked-device",
|
|
89
|
+
"local-cli",
|
|
90
|
+
] as const;
|
|
91
|
+
|
|
92
|
+
export type ProviderPluginTransport = (typeof providerPluginTransports)[number];
|
|
93
|
+
export type ProviderPluginAuthKind = GhostgetAuth["kind"];
|
|
94
|
+
export type ProviderPluginContractStateV1 = "observed" | "capture-required";
|
|
95
|
+
|
|
96
|
+
type ProviderPluginOperationDefinitionBaseV1 = {
|
|
97
|
+
readonly name: string;
|
|
98
|
+
/** Exact durable contract identity owned by this descriptor. */
|
|
99
|
+
readonly contractVersion: number;
|
|
100
|
+
/**
|
|
101
|
+
* Built-in-only durable routing aliases for contracts whose reviewed
|
|
102
|
+
* semantics remain compatible with this active descriptor.
|
|
103
|
+
*/
|
|
104
|
+
readonly historicalContractVersions?: readonly number[];
|
|
105
|
+
readonly risk: OperationRisk;
|
|
106
|
+
readonly input: InputSchema;
|
|
107
|
+
readonly sideEffect: string;
|
|
108
|
+
readonly idempotency: IdempotencyKind;
|
|
109
|
+
readonly dedupeWindowMs: number;
|
|
110
|
+
readonly state: ProviderPluginContractStateV1;
|
|
111
|
+
/** Built-in web-session-only marker for one reviewed public read. */
|
|
112
|
+
readonly access?: "public";
|
|
113
|
+
readonly dispatch: "none" | "single" | "thread-items" | "bounded-items";
|
|
114
|
+
readonly implementation: string;
|
|
115
|
+
readonly planDispatches: (input: OperationInput) => readonly BrowserDispatchPlan[];
|
|
116
|
+
readonly validateInput: (input: OperationInput) => readonly string[];
|
|
117
|
+
/**
|
|
118
|
+
* Optional provider-owned relationship between the bound auth subject and
|
|
119
|
+
* operation input (for example, an explicit actor field). The kernel still
|
|
120
|
+
* requires the binding-level subject matcher for every authenticated write.
|
|
121
|
+
*/
|
|
122
|
+
readonly validateSubjectInput?: (
|
|
123
|
+
input: OperationInput,
|
|
124
|
+
subject: string,
|
|
125
|
+
) => readonly string[];
|
|
126
|
+
/**
|
|
127
|
+
* Declares that this exact operation supports a read-only boolean
|
|
128
|
+
* desired-state reconciliation. Runtime code supplies the observation; the
|
|
129
|
+
* kernel compares it to this pure desired-state projection.
|
|
130
|
+
*/
|
|
131
|
+
readonly reconciliation?: ProviderPluginReconciliationDefinitionV1;
|
|
132
|
+
/**
|
|
133
|
+
* Provider-owned interpretation of one exact inbox read. Every installed
|
|
134
|
+
* messaging list/read contract must either supply a pure, versioned
|
|
135
|
+
* materializer or state why normalization is unsafe. The kernel reparses
|
|
136
|
+
* supported output from unknown before it reaches durable state.
|
|
137
|
+
*/
|
|
138
|
+
readonly omni?: ProviderPluginOmniDefinitionV1;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export type ProviderPluginReconciliationDefinitionV1 =
|
|
142
|
+
| {
|
|
143
|
+
readonly kind: "boolean-desired-state";
|
|
144
|
+
readonly desiredState: (input: OperationInput) => boolean;
|
|
145
|
+
}
|
|
146
|
+
| {
|
|
147
|
+
/** Reconcile a create from its encrypted response-derived exact target. */
|
|
148
|
+
readonly kind: "provider-accepted-target-presence";
|
|
149
|
+
}
|
|
150
|
+
| {
|
|
151
|
+
/**
|
|
152
|
+
* Reconcile a deletion from an encrypted exact target bound by a strict
|
|
153
|
+
* provider read before the mutation request leaves the process.
|
|
154
|
+
*/
|
|
155
|
+
readonly kind: "provider-bound-target-desired-state";
|
|
156
|
+
readonly desiredState: false;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export type ProviderPluginOmniDefinitionV1 =
|
|
160
|
+
| {
|
|
161
|
+
readonly state: "supported";
|
|
162
|
+
readonly schemaVersion: 1;
|
|
163
|
+
readonly materializerId: string;
|
|
164
|
+
readonly materializerVersion: number;
|
|
165
|
+
readonly materialize: (
|
|
166
|
+
input: OperationInput,
|
|
167
|
+
output: unknown,
|
|
168
|
+
) => unknown;
|
|
169
|
+
}
|
|
170
|
+
| {
|
|
171
|
+
readonly state: "unsupported";
|
|
172
|
+
readonly reason: string;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export type ProviderPluginMessagingTargetV1 = Readonly<
|
|
176
|
+
Record<string, string>
|
|
177
|
+
>;
|
|
178
|
+
|
|
179
|
+
export type ProviderPluginMessagingRouteCandidateV1 = {
|
|
180
|
+
/** Exact provider coordinates retained only in Ghostget private state. */
|
|
181
|
+
readonly target: ProviderPluginMessagingTargetV1;
|
|
182
|
+
/** Exact provider conversation identity used to bind normalized reads. */
|
|
183
|
+
readonly conversationProviderId: string;
|
|
184
|
+
readonly conversationKind: "single" | "group" | "unknown";
|
|
185
|
+
readonly title: string | null;
|
|
186
|
+
readonly participants: ProviderConversationV1["participants"];
|
|
187
|
+
readonly providerRevision: string | null;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
export type ProviderPluginMessagingTurnPartV1 = {
|
|
191
|
+
readonly partId: string;
|
|
192
|
+
readonly text: string;
|
|
193
|
+
/** Exact provider message identity recovered from one current context ref. */
|
|
194
|
+
readonly replyToProviderId: string | null;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export type ProviderPluginMessagingAcceptedResultV1 = {
|
|
198
|
+
/** Provider accepted/submitted work; delivery and read remain unproven. */
|
|
199
|
+
readonly state: "submitted";
|
|
200
|
+
readonly providerMessageId: string;
|
|
201
|
+
readonly providerRevision: string | null;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
export type ProviderPluginMessagingExpectedOwnPrefixV1 = {
|
|
205
|
+
readonly base: {
|
|
206
|
+
readonly exactDataRevision: string;
|
|
207
|
+
readonly latestMessageRevision: string;
|
|
208
|
+
readonly contextLimit: number;
|
|
209
|
+
/** Ordered, bounded hashes of the exact normalized context at preview. */
|
|
210
|
+
readonly messages: readonly {
|
|
211
|
+
readonly providerMessageId: string;
|
|
212
|
+
readonly providerRevision: string | null;
|
|
213
|
+
readonly orderedAt: string | null;
|
|
214
|
+
readonly messageSha256: string;
|
|
215
|
+
}[];
|
|
216
|
+
};
|
|
217
|
+
readonly current: {
|
|
218
|
+
readonly exactDataRevision: string;
|
|
219
|
+
readonly latestMessageRevision: string;
|
|
220
|
+
readonly messages: readonly ProviderMessageV1[];
|
|
221
|
+
};
|
|
222
|
+
readonly accepted: readonly {
|
|
223
|
+
readonly providerMessageId: string;
|
|
224
|
+
readonly providerRevision: string | null;
|
|
225
|
+
readonly direction: "outgoing";
|
|
226
|
+
readonly bodySha256: string;
|
|
227
|
+
readonly replyToProviderId: string | null;
|
|
228
|
+
}[];
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
export type ProviderPluginMessagingExpectedOwnPrefixProofV1 =
|
|
232
|
+
| {
|
|
233
|
+
readonly state: "proven";
|
|
234
|
+
/** Total accepted prefix represented by this exact live window. */
|
|
235
|
+
readonly matchedAcceptedPrefixCount: number;
|
|
236
|
+
}
|
|
237
|
+
| { readonly state: "drift" };
|
|
238
|
+
|
|
239
|
+
export type ProviderPluginMessagingLiveRouteStateV1 = {
|
|
240
|
+
readonly conversationProviderId: string;
|
|
241
|
+
/** Fresh provider-native network shown in the confirmation recipient. */
|
|
242
|
+
readonly network: string;
|
|
243
|
+
readonly conversation: {
|
|
244
|
+
readonly kind: "single" | "group" | "unknown";
|
|
245
|
+
readonly title: string | null;
|
|
246
|
+
readonly participantCount: number;
|
|
247
|
+
};
|
|
248
|
+
readonly sourceConversationCoordinate:
|
|
249
|
+
MessageLikeMeSourceConversationCoordinateBindingV1;
|
|
250
|
+
readonly participantFingerprint: string;
|
|
251
|
+
readonly providerRevision: string | null;
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
export type ProviderPluginMessagingReconciliationRequestV1 = {
|
|
255
|
+
readonly operation: string;
|
|
256
|
+
readonly input: OperationInput;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
export type ProviderPluginMessagingActionDeadlineV1 = Pick<
|
|
260
|
+
OperationDeadline,
|
|
261
|
+
"signal" | "remainingTimeMs" | "run" | "throwIfUnavailable"
|
|
262
|
+
>;
|
|
263
|
+
|
|
264
|
+
export type ProviderPluginMessagingActionAttemptV1 = {
|
|
265
|
+
/**
|
|
266
|
+
* The provider runtime must await this durable fence immediately before its
|
|
267
|
+
* first effect-capable call or child process. It may perform bounded reads
|
|
268
|
+
* before the fence, but no mutation and no retry may happen before or after it.
|
|
269
|
+
*/
|
|
270
|
+
readonly beforeExternalBegin: () => Promise<void>;
|
|
271
|
+
/** One kernel-owned total budget for lazy loading and every provider step. */
|
|
272
|
+
readonly operationDeadline: ProviderPluginMessagingActionDeadlineV1;
|
|
273
|
+
readonly signal: AbortSignal;
|
|
274
|
+
/**
|
|
275
|
+
* Durably terminalize this fenced attempt with one provider-owned symbolic
|
|
276
|
+
* diagnostic. This never authorizes retry and accepts no message data or IDs.
|
|
277
|
+
*/
|
|
278
|
+
readonly recordPrivateIndeterminateOutcome: (code: string) => Promise<void>;
|
|
279
|
+
readonly environment: Readonly<Record<string, string | undefined>>;
|
|
280
|
+
/** Kernel-owned durable cleanup publication for provider-private resources. */
|
|
281
|
+
readonly registerCleanupBarrier?: ProviderPluginCleanupBarrierRegistrar;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
export type ProviderPluginMessagingActionExecutorV1 = (
|
|
285
|
+
operation: string,
|
|
286
|
+
input: OperationInput,
|
|
287
|
+
auth: GhostgetAuth,
|
|
288
|
+
attempt: ProviderPluginMessagingActionAttemptV1,
|
|
289
|
+
) => Promise<unknown>;
|
|
290
|
+
|
|
291
|
+
export type ProviderPluginMessagingActionDefinitionV1 =
|
|
292
|
+
| {
|
|
293
|
+
readonly state: "unavailable";
|
|
294
|
+
readonly reason: string;
|
|
295
|
+
readonly reply: "unsupported";
|
|
296
|
+
}
|
|
297
|
+
| {
|
|
298
|
+
readonly state: "supported";
|
|
299
|
+
readonly operation: string;
|
|
300
|
+
readonly reply: "supported" | "unsupported";
|
|
301
|
+
readonly livePreflight: {
|
|
302
|
+
readonly operation: string;
|
|
303
|
+
readonly input: (target: ProviderPluginMessagingTargetV1) => OperationInput;
|
|
304
|
+
readonly snapshot: (
|
|
305
|
+
output: unknown,
|
|
306
|
+
expectedAccountSubject: string,
|
|
307
|
+
) => ProviderPluginMessagingLiveRouteStateV1;
|
|
308
|
+
};
|
|
309
|
+
readonly compileTurnPart: (
|
|
310
|
+
target: ProviderPluginMessagingTargetV1,
|
|
311
|
+
part: ProviderPluginMessagingTurnPartV1,
|
|
312
|
+
) => OperationInput;
|
|
313
|
+
readonly mapAcceptedResult: (
|
|
314
|
+
output: unknown,
|
|
315
|
+
) => ProviderPluginMessagingAcceptedResultV1;
|
|
316
|
+
/** Prove that live state contains exactly the accepted own-message prefix and no drift. */
|
|
317
|
+
readonly proveExpectedOwnPrefix: (
|
|
318
|
+
value: ProviderPluginMessagingExpectedOwnPrefixV1,
|
|
319
|
+
) => ProviderPluginMessagingExpectedOwnPrefixProofV1;
|
|
320
|
+
/**
|
|
321
|
+
* Build one explicit checked provider read. The generic facade never
|
|
322
|
+
* invokes this implicitly and never changes existing runs reconciliation.
|
|
323
|
+
*/
|
|
324
|
+
readonly reconciliation: (
|
|
325
|
+
target: ProviderPluginMessagingTargetV1,
|
|
326
|
+
accepted: ProviderPluginMessagingAcceptedResultV1,
|
|
327
|
+
) => ProviderPluginMessagingReconciliationRequestV1;
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Provider-owned exact messaging codec. The kernel owns lifecycle, storage,
|
|
332
|
+
* confirmation, and execution; this SPI owns provider coordinate semantics.
|
|
333
|
+
*/
|
|
334
|
+
export type ProviderPluginMessagingDefinitionV1 = {
|
|
335
|
+
readonly schemaVersion: 1;
|
|
336
|
+
readonly contractId: string;
|
|
337
|
+
readonly network: string;
|
|
338
|
+
/** Whether an exact provider read proves current remote state. */
|
|
339
|
+
readonly contextLiveness:
|
|
340
|
+
| "fresh-as-of-live-preflight"
|
|
341
|
+
| "freshness-unproven";
|
|
342
|
+
readonly listOperation: "messaging.list";
|
|
343
|
+
readonly contextOperation: "messaging.read";
|
|
344
|
+
readonly enumerateRoutes: (
|
|
345
|
+
input: OperationInput,
|
|
346
|
+
page: ProviderMaterializedPageV1,
|
|
347
|
+
) => readonly ProviderPluginMessagingRouteCandidateV1[];
|
|
348
|
+
/** Exact stored-target resolution, independent from bounded list pagination. */
|
|
349
|
+
readonly resolveRoute: {
|
|
350
|
+
readonly operation: string;
|
|
351
|
+
readonly input: (
|
|
352
|
+
target: ProviderPluginMessagingTargetV1,
|
|
353
|
+
) => OperationInput;
|
|
354
|
+
readonly candidates: (
|
|
355
|
+
target: ProviderPluginMessagingTargetV1,
|
|
356
|
+
output: unknown,
|
|
357
|
+
) => readonly ProviderPluginMessagingRouteCandidateV1[];
|
|
358
|
+
/** Canonical source coordinate proved only by the exact resolver output. */
|
|
359
|
+
readonly sourceConversationCoordinate: (
|
|
360
|
+
target: ProviderPluginMessagingTargetV1,
|
|
361
|
+
output: unknown,
|
|
362
|
+
expectedAccountSubject: string,
|
|
363
|
+
) => MessageLikeMeSourceConversationCoordinateBindingV1 | null;
|
|
364
|
+
};
|
|
365
|
+
readonly parseTarget: (value: unknown) => ProviderPluginMessagingTargetV1;
|
|
366
|
+
readonly contextInput: (
|
|
367
|
+
target: ProviderPluginMessagingTargetV1,
|
|
368
|
+
limit: number,
|
|
369
|
+
) => OperationInput;
|
|
370
|
+
readonly action: ProviderPluginMessagingActionDefinitionV1;
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
export type ProviderApiPluginOperationDefinitionV1 =
|
|
374
|
+
ProviderPluginOperationDefinitionBaseV1 & {
|
|
375
|
+
readonly requiredScopeSets: readonly (readonly string[])[];
|
|
376
|
+
/** Bounded provider-owned coverage labels; not a kernel-owned closed union. */
|
|
377
|
+
readonly coverage: readonly string[];
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
export type WebSessionPluginOperationDefinitionV1 =
|
|
381
|
+
ProviderPluginOperationDefinitionBaseV1 & {
|
|
382
|
+
readonly requiredScopeSets?: never;
|
|
383
|
+
readonly coverage?: never;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
export type LocalCliPluginOperationDefinitionV1 =
|
|
387
|
+
WebSessionPluginOperationDefinitionV1;
|
|
388
|
+
|
|
389
|
+
export type ProviderPluginOperationDefinitionV1 =
|
|
390
|
+
| ProviderApiPluginOperationDefinitionV1
|
|
391
|
+
| WebSessionPluginOperationDefinitionV1
|
|
392
|
+
| LocalCliPluginOperationDefinitionV1;
|
|
393
|
+
|
|
394
|
+
/** Validated registry projection. `contractVersions` is CLI compatibility only. */
|
|
395
|
+
export type ProviderApiPluginOperationV1 =
|
|
396
|
+
ProviderApiPluginOperationDefinitionV1 & {
|
|
397
|
+
readonly contractVersions: readonly number[];
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
export type WebSessionPluginOperationV1 =
|
|
401
|
+
WebSessionPluginOperationDefinitionV1 & {
|
|
402
|
+
readonly contractVersions: readonly number[];
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
export type LocalCliPluginOperationV1 = WebSessionPluginOperationV1;
|
|
406
|
+
|
|
407
|
+
export type ProviderPluginOperationV1 =
|
|
408
|
+
| ProviderApiPluginOperationV1
|
|
409
|
+
| WebSessionPluginOperationV1
|
|
410
|
+
| LocalCliPluginOperationV1;
|
|
411
|
+
|
|
412
|
+
export const MAX_PROVIDER_PLUGIN_PLAN_DISPATCHES = 25;
|
|
413
|
+
export const MAX_PROVIDER_PLUGIN_PLAN_BYTES = 16 * 1024;
|
|
414
|
+
/** Trusted-source definitions match the portable manifest's V1 shape bounds. */
|
|
415
|
+
export const MAX_PROVIDER_PLUGIN_IMPLEMENTATION_SOURCES = 512;
|
|
416
|
+
export const MAX_PROVIDER_PLUGIN_BINDINGS = 64;
|
|
417
|
+
export const MAX_PROVIDER_PLUGIN_OPERATIONS_PER_BINDING = 256;
|
|
418
|
+
export const MAX_PROVIDER_PLUGIN_CONTRACT_VERSIONS_PER_OPERATION = 256;
|
|
419
|
+
/** A plugin may use many bindings without reaching the full 64 × 256 product. */
|
|
420
|
+
export const MAX_PROVIDER_PLUGIN_OPERATIONS = 4_096;
|
|
421
|
+
|
|
422
|
+
export type ProviderPluginPlanOperationV1 = Pick<
|
|
423
|
+
ProviderPluginOperationDefinitionV1,
|
|
424
|
+
"name" | "risk" | "dispatch" | "planDispatches"
|
|
425
|
+
>;
|
|
426
|
+
|
|
427
|
+
const providerPluginDispatchIdPattern =
|
|
428
|
+
/^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)*(?:\[[1-9][0-9]*\])?$/u;
|
|
429
|
+
const conformedProviderPluginPlanHooks =
|
|
430
|
+
new WeakSet<ProviderPluginPlanOperationV1["planDispatches"]>();
|
|
431
|
+
|
|
432
|
+
function hasUnpairedSurrogate(value: string): boolean {
|
|
433
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
434
|
+
const code = value.charCodeAt(index);
|
|
435
|
+
if (code >= 0xd800 && code <= 0xdbff) {
|
|
436
|
+
const next = value.charCodeAt(index + 1);
|
|
437
|
+
if (!Number.isFinite(next) || next < 0xdc00 || next > 0xdfff) return true;
|
|
438
|
+
index += 1;
|
|
439
|
+
} else if (code >= 0xdc00 && code <= 0xdfff) {
|
|
440
|
+
return true;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return false;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function ownDataProperty(
|
|
447
|
+
value: object,
|
|
448
|
+
key: string,
|
|
449
|
+
label: string,
|
|
450
|
+
): unknown {
|
|
451
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
452
|
+
if (
|
|
453
|
+
descriptor === undefined
|
|
454
|
+
|| !descriptor.enumerable
|
|
455
|
+
|| !("value" in descriptor)
|
|
456
|
+
) {
|
|
457
|
+
throw new Error(`${label} must be an enumerable data property`);
|
|
458
|
+
}
|
|
459
|
+
return descriptor.value as unknown;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function requirePlainRecord(value: object, label: string): void {
|
|
463
|
+
const prototype = Object.getPrototypeOf(value) as unknown;
|
|
464
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
465
|
+
throw new Error(`${label} must be a plain JSON object`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function cloneFrozenPlanInputValue(
|
|
470
|
+
value: unknown,
|
|
471
|
+
label: string,
|
|
472
|
+
): OperationInput[string] {
|
|
473
|
+
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
474
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
475
|
+
if (Array.isArray(value)) {
|
|
476
|
+
if ((Object.getPrototypeOf(value) as unknown) !== Array.prototype) {
|
|
477
|
+
throw new Error(`${label} must be a plain JSON array`);
|
|
478
|
+
}
|
|
479
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
|
|
480
|
+
const rawLength: unknown = lengthDescriptor !== undefined
|
|
481
|
+
&& "value" in lengthDescriptor
|
|
482
|
+
? lengthDescriptor.value as unknown
|
|
483
|
+
: Number.NaN;
|
|
484
|
+
if (
|
|
485
|
+
typeof rawLength !== "number"
|
|
486
|
+
|| !Number.isSafeInteger(rawLength)
|
|
487
|
+
|| rawLength < 0
|
|
488
|
+
|| rawLength > 1_000
|
|
489
|
+
) {
|
|
490
|
+
throw new Error(`${label} must be a bounded dense array`);
|
|
491
|
+
}
|
|
492
|
+
const length = rawLength;
|
|
493
|
+
const expectedKeys = new Set<PropertyKey>([
|
|
494
|
+
"length",
|
|
495
|
+
...Array.from({ length }, (_unused, index) => String(index)),
|
|
496
|
+
]);
|
|
497
|
+
const actualKeys = Reflect.ownKeys(value);
|
|
498
|
+
if (
|
|
499
|
+
actualKeys.length !== expectedKeys.size
|
|
500
|
+
|| actualKeys.some((key) => !expectedKeys.has(key))
|
|
501
|
+
) {
|
|
502
|
+
throw new Error(`${label} must be a bounded dense array`);
|
|
503
|
+
}
|
|
504
|
+
return Object.freeze(Array.from(
|
|
505
|
+
{ length },
|
|
506
|
+
(_unused, index) =>
|
|
507
|
+
cloneFrozenPlanInputScalar(
|
|
508
|
+
ownDataProperty(value, String(index), `${label}[${index}]`),
|
|
509
|
+
`${label}[${index}]`,
|
|
510
|
+
),
|
|
511
|
+
));
|
|
512
|
+
}
|
|
513
|
+
return cloneFrozenPlanInputFile(value, label);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function cloneFrozenPlanInputScalar(
|
|
517
|
+
value: unknown,
|
|
518
|
+
label: string,
|
|
519
|
+
): string | number | boolean | { readonly kind: "file"; readonly reference: string } {
|
|
520
|
+
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
521
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
522
|
+
return cloneFrozenPlanInputFile(value, label);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function cloneFrozenPlanInputFile(
|
|
526
|
+
value: unknown,
|
|
527
|
+
label: string,
|
|
528
|
+
): { readonly kind: "file"; readonly reference: string } {
|
|
529
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
530
|
+
throw new Error(`${label} is not JSON-compatible plugin input`);
|
|
531
|
+
}
|
|
532
|
+
requirePlainRecord(value, label);
|
|
533
|
+
const keys = Reflect.ownKeys(value);
|
|
534
|
+
if (
|
|
535
|
+
keys.length !== 2
|
|
536
|
+
|| !keys.includes("kind")
|
|
537
|
+
|| !keys.includes("reference")
|
|
538
|
+
) {
|
|
539
|
+
throw new Error(`${label} is not a valid file input`);
|
|
540
|
+
}
|
|
541
|
+
const kind = ownDataProperty(value, "kind", `${label}.kind`);
|
|
542
|
+
const reference = ownDataProperty(value, "reference", `${label}.reference`);
|
|
543
|
+
if (kind !== "file" || typeof reference !== "string") {
|
|
544
|
+
throw new Error(`${label} is not a valid file input`);
|
|
545
|
+
}
|
|
546
|
+
return Object.freeze({ kind: "file" as const, reference });
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function detachedFrozenPlanInput(input: OperationInput): OperationInput {
|
|
550
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) {
|
|
551
|
+
throw new Error("provider plugin plan input must be a JSON object");
|
|
552
|
+
}
|
|
553
|
+
requirePlainRecord(input, "provider plugin plan input");
|
|
554
|
+
const keys = Reflect.ownKeys(input);
|
|
555
|
+
if (
|
|
556
|
+
keys.length > 100
|
|
557
|
+
|| keys.some((key) => typeof key !== "string")
|
|
558
|
+
) {
|
|
559
|
+
throw new Error("provider plugin plan input must be a bounded JSON object");
|
|
560
|
+
}
|
|
561
|
+
const result: Record<string, OperationInput[string]> = {};
|
|
562
|
+
for (const key of keys as string[]) {
|
|
563
|
+
Object.defineProperty(result, key, {
|
|
564
|
+
configurable: true,
|
|
565
|
+
enumerable: true,
|
|
566
|
+
writable: true,
|
|
567
|
+
value: cloneFrozenPlanInputValue(
|
|
568
|
+
ownDataProperty(input, key, `provider plugin plan input.${key}`),
|
|
569
|
+
`provider plugin plan input.${key}`,
|
|
570
|
+
),
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
return Object.freeze(result);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function parseProviderPluginDispatches(
|
|
577
|
+
value: unknown,
|
|
578
|
+
operation: ProviderPluginPlanOperationV1,
|
|
579
|
+
input: OperationInput,
|
|
580
|
+
): readonly BrowserDispatchPlan[] {
|
|
581
|
+
const label = `provider plugin operation ${operation.name} planDispatches`;
|
|
582
|
+
if (
|
|
583
|
+
!Array.isArray(value)
|
|
584
|
+
|| (Object.getPrototypeOf(value) as unknown) !== Array.prototype
|
|
585
|
+
) {
|
|
586
|
+
throw new Error(`${label} must return a plain JSON array`);
|
|
587
|
+
}
|
|
588
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
|
|
589
|
+
const rawLength: unknown = lengthDescriptor !== undefined
|
|
590
|
+
&& "value" in lengthDescriptor
|
|
591
|
+
? lengthDescriptor.value as unknown
|
|
592
|
+
: Number.NaN;
|
|
593
|
+
if (
|
|
594
|
+
typeof rawLength !== "number"
|
|
595
|
+
|| !Number.isSafeInteger(rawLength)
|
|
596
|
+
|| rawLength < 0
|
|
597
|
+
|| rawLength > MAX_PROVIDER_PLUGIN_PLAN_DISPATCHES
|
|
598
|
+
) {
|
|
599
|
+
throw new Error(
|
|
600
|
+
`${label} may return at most ${MAX_PROVIDER_PLUGIN_PLAN_DISPATCHES} dispatches`,
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
const length = rawLength;
|
|
604
|
+
const expectedKeys = new Set<PropertyKey>([
|
|
605
|
+
"length",
|
|
606
|
+
...Array.from({ length }, (_unused, index) => String(index)),
|
|
607
|
+
]);
|
|
608
|
+
const actualKeys = Reflect.ownKeys(value);
|
|
609
|
+
if (
|
|
610
|
+
actualKeys.length !== expectedKeys.size
|
|
611
|
+
|| actualKeys.some((key) => !expectedKeys.has(key))
|
|
612
|
+
) {
|
|
613
|
+
throw new Error(`${label} must return a dense array without extra properties`);
|
|
614
|
+
}
|
|
615
|
+
const dispatches: BrowserDispatchPlan[] = [];
|
|
616
|
+
const ids = new Set<string>();
|
|
617
|
+
for (let index = 0; index < length; index += 1) {
|
|
618
|
+
const candidate = ownDataProperty(
|
|
619
|
+
value,
|
|
620
|
+
String(index),
|
|
621
|
+
`${label}[${index}]`,
|
|
622
|
+
);
|
|
623
|
+
if (
|
|
624
|
+
typeof candidate !== "object"
|
|
625
|
+
|| candidate === null
|
|
626
|
+
|| Array.isArray(candidate)
|
|
627
|
+
) {
|
|
628
|
+
throw new Error(`${label}[${index}] must be a plain JSON object`);
|
|
629
|
+
}
|
|
630
|
+
requirePlainRecord(candidate, `${label}[${index}]`);
|
|
631
|
+
const keys = Reflect.ownKeys(candidate);
|
|
632
|
+
if (
|
|
633
|
+
keys.length !== 2
|
|
634
|
+
|| !keys.includes("id")
|
|
635
|
+
|| !keys.includes("description")
|
|
636
|
+
) {
|
|
637
|
+
throw new Error(
|
|
638
|
+
`${label}[${index}] must contain only id and description`,
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
const id = ownDataProperty(candidate, "id", `${label}[${index}].id`);
|
|
642
|
+
const description = ownDataProperty(
|
|
643
|
+
candidate,
|
|
644
|
+
"description",
|
|
645
|
+
`${label}[${index}].description`,
|
|
646
|
+
);
|
|
647
|
+
if (
|
|
648
|
+
typeof id !== "string"
|
|
649
|
+
|| id.length > 80
|
|
650
|
+
|| !providerPluginDispatchIdPattern.test(id)
|
|
651
|
+
) {
|
|
652
|
+
throw new Error(`${label}[${index}] has an invalid dispatch ID`);
|
|
653
|
+
}
|
|
654
|
+
if (ids.has(id)) {
|
|
655
|
+
throw new Error(`${label} returned duplicate dispatch ID ${id}`);
|
|
656
|
+
}
|
|
657
|
+
if (
|
|
658
|
+
typeof description !== "string"
|
|
659
|
+
|| description.length < 1
|
|
660
|
+
|| description.length > 500
|
|
661
|
+
|| hasControlCharacters(description)
|
|
662
|
+
|| hasUnpairedSurrogate(description)
|
|
663
|
+
) {
|
|
664
|
+
throw new Error(`${label}[${index}] has an invalid description`);
|
|
665
|
+
}
|
|
666
|
+
ids.add(id);
|
|
667
|
+
dispatches.push(Object.freeze({ id, description }));
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (operation.risk === "R1" && dispatches.length !== 0) {
|
|
671
|
+
throw new Error("R1 operations must not schedule remote dispatches");
|
|
672
|
+
}
|
|
673
|
+
if (
|
|
674
|
+
(operation.risk === "R2" || operation.risk === "R3")
|
|
675
|
+
&& dispatches.length === 0
|
|
676
|
+
) {
|
|
677
|
+
throw new Error(`${label} must return at least one dispatch for ${operation.risk}`);
|
|
678
|
+
}
|
|
679
|
+
if (operation.dispatch === "none" && dispatches.length !== 0) {
|
|
680
|
+
throw new Error(`${label} must return no dispatches for dispatch policy none`);
|
|
681
|
+
}
|
|
682
|
+
if (operation.dispatch === "single" && dispatches.length !== 1) {
|
|
683
|
+
throw new Error(`${label} must return exactly one dispatch for dispatch policy single`);
|
|
684
|
+
}
|
|
685
|
+
if (operation.dispatch === "thread-items") {
|
|
686
|
+
const items = input.items;
|
|
687
|
+
if (
|
|
688
|
+
!Array.isArray(items)
|
|
689
|
+
|| items.length < 1
|
|
690
|
+
|| dispatches.length !== items.length
|
|
691
|
+
) {
|
|
692
|
+
throw new Error(
|
|
693
|
+
`${label} must return exactly one dispatch for each input.items value`,
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
const result = Object.freeze(dispatches);
|
|
698
|
+
const encoded = JSON.stringify(result);
|
|
699
|
+
if (Buffer.byteLength(encoded, "utf8") > MAX_PROVIDER_PLUGIN_PLAN_BYTES) {
|
|
700
|
+
throw new Error(
|
|
701
|
+
`${label} exceeds its ${MAX_PROVIDER_PLUGIN_PLAN_BYTES}-byte canonical JSON bound`,
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
return result;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Run one pure planner twice at its trust boundary and return only detached,
|
|
709
|
+
* deeply frozen kernel-owned data. Built-in and portable plugin hosts share
|
|
710
|
+
* this exact conformance check.
|
|
711
|
+
*/
|
|
712
|
+
function runUntrustedProviderPluginPlanConformance(
|
|
713
|
+
operation: ProviderPluginPlanOperationV1,
|
|
714
|
+
input: OperationInput,
|
|
715
|
+
): readonly BrowserDispatchPlan[] {
|
|
716
|
+
const plan = (): readonly BrowserDispatchPlan[] => {
|
|
717
|
+
const detachedInput = detachedFrozenPlanInput(input);
|
|
718
|
+
const returned: unknown = operation.planDispatches(detachedInput);
|
|
719
|
+
return parseProviderPluginDispatches(returned, operation, detachedInput);
|
|
720
|
+
};
|
|
721
|
+
const first = plan();
|
|
722
|
+
const second = plan();
|
|
723
|
+
if (JSON.stringify(first) !== JSON.stringify(second)) {
|
|
724
|
+
throw new Error(
|
|
725
|
+
`provider plugin operation ${operation.name} planDispatches is unstable for identical input`,
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
return first;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
export function runProviderPluginPlanConformance(
|
|
732
|
+
operation: ProviderPluginPlanOperationV1,
|
|
733
|
+
input: OperationInput,
|
|
734
|
+
): readonly BrowserDispatchPlan[] {
|
|
735
|
+
if (conformedProviderPluginPlanHooks.has(operation.planDispatches)) {
|
|
736
|
+
return operation.planDispatches(input);
|
|
737
|
+
}
|
|
738
|
+
return runUntrustedProviderPluginPlanConformance(operation, input);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function conformingProviderPluginPlanDispatches(
|
|
742
|
+
operation: ProviderPluginOperationDefinitionV1,
|
|
743
|
+
): ProviderPluginOperationDefinitionV1["planDispatches"] {
|
|
744
|
+
const cache = new WeakMap<OperationInput, readonly BrowserDispatchPlan[]>();
|
|
745
|
+
const planDispatches = (input: OperationInput): readonly BrowserDispatchPlan[] => {
|
|
746
|
+
const existing = cache.get(input);
|
|
747
|
+
if (existing !== undefined) return existing;
|
|
748
|
+
const planned = runUntrustedProviderPluginPlanConformance(operation, input);
|
|
749
|
+
cache.set(input, planned);
|
|
750
|
+
return planned;
|
|
751
|
+
};
|
|
752
|
+
conformedProviderPluginPlanHooks.add(planDispatches);
|
|
753
|
+
return planDispatches;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
export type ProviderPluginSubjectDefinitionV1 = {
|
|
757
|
+
/** Human-readable, value-free description of the accepted subject form. */
|
|
758
|
+
readonly format: string;
|
|
759
|
+
readonly matches: (value: string) => boolean;
|
|
760
|
+
};
|
|
761
|
+
|
|
762
|
+
export type ProviderPluginSubjectProbeOptionsV1 = {
|
|
763
|
+
/** Caller-owned cancellation propagated through probe network/process work. */
|
|
764
|
+
readonly signal?: AbortSignal;
|
|
765
|
+
readonly environment?: Readonly<Record<string, string | undefined>>;
|
|
766
|
+
/** Kernel-owned durable publication for operation-private resources. */
|
|
767
|
+
readonly registerCleanupBarrier?: ProviderPluginCleanupBarrierRegistrar;
|
|
768
|
+
};
|
|
769
|
+
|
|
770
|
+
export type ProviderPluginReconciliationOptionsV1 = {
|
|
771
|
+
readonly signal?: AbortSignal;
|
|
772
|
+
readonly environment?: Readonly<Record<string, string | undefined>>;
|
|
773
|
+
readonly registerCleanupBarrier?: ProviderPluginCleanupBarrierRegistrar;
|
|
774
|
+
};
|
|
775
|
+
|
|
776
|
+
export type ProviderPluginSubjectV1 = ProviderPluginSubjectDefinitionV1 & {
|
|
777
|
+
/** Lazy compatibility hook synthesized from the binding runtime. */
|
|
778
|
+
readonly probe?: (
|
|
779
|
+
auth: GhostgetAuth,
|
|
780
|
+
options?: ProviderPluginSubjectProbeOptionsV1,
|
|
781
|
+
) => Promise<string>;
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
export type ProviderPluginImplementationSourceDefinitionV1 = {
|
|
785
|
+
/** Stable plugin-relative identity included in implementation hashes. */
|
|
786
|
+
readonly label: string;
|
|
787
|
+
/** A reviewed source file. V1 accepts repository-owned file URLs only. */
|
|
788
|
+
readonly url: URL;
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
export type ProviderPluginImplementationSourceV1 = {
|
|
792
|
+
/** Stable plugin-relative identity included in implementation hashes. */
|
|
793
|
+
readonly label: string;
|
|
794
|
+
/** Canonical immutable path validated under Ghostget's source root. */
|
|
795
|
+
readonly path: string;
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
export type ProviderApiPluginRuntimeV1 = {
|
|
799
|
+
readonly execute: (context: ProviderActionContext) => Promise<void>;
|
|
800
|
+
readonly executeMessagingPart?: ProviderPluginMessagingActionExecutorV1;
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
export type ProviderPluginReconciliationReadbackV1 = {
|
|
804
|
+
readonly actualState: boolean;
|
|
805
|
+
readonly reason: string;
|
|
806
|
+
};
|
|
807
|
+
|
|
808
|
+
export type ProviderPluginAcceptedTargetReconciliationContextV1 = {
|
|
809
|
+
readonly schemaVersion: 1;
|
|
810
|
+
readonly kind: "provider-accepted-target-presence";
|
|
811
|
+
readonly dispatch: {
|
|
812
|
+
readonly id: string;
|
|
813
|
+
readonly index: number;
|
|
814
|
+
readonly planned: number;
|
|
815
|
+
};
|
|
816
|
+
readonly target: {
|
|
817
|
+
readonly schemaVersion: 1;
|
|
818
|
+
readonly identifier: string;
|
|
819
|
+
};
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
export type ProviderPluginBoundTargetDesiredStateReconciliationContextV1 =
|
|
823
|
+
Omit<ProviderPluginAcceptedTargetReconciliationContextV1, "kind"> & {
|
|
824
|
+
readonly kind: "provider-bound-target-desired-state";
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
export type ProviderPluginReconciliationContextV1 =
|
|
828
|
+
| ProviderPluginAcceptedTargetReconciliationContextV1
|
|
829
|
+
| ProviderPluginBoundTargetDesiredStateReconciliationContextV1;
|
|
830
|
+
|
|
831
|
+
export type ProviderPluginLinkedDeviceRuntimeStatusV1 = {
|
|
832
|
+
readonly ready: boolean;
|
|
833
|
+
readonly implementation: string;
|
|
834
|
+
readonly version: string;
|
|
835
|
+
readonly integrity: string;
|
|
836
|
+
readonly setupCommand?: string;
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
export type ProviderPluginLinkedDeviceSyncResultV1 = {
|
|
840
|
+
readonly itemsStored: number;
|
|
841
|
+
readonly projection: string;
|
|
842
|
+
readonly emitsProtocolAcknowledgements: boolean;
|
|
843
|
+
};
|
|
844
|
+
|
|
845
|
+
export type ProviderPluginLinkedDeviceAttemptBoundaryV1 = {
|
|
846
|
+
/** Durable attempt identity retained for later reconciliation. */
|
|
847
|
+
readonly journalId: string;
|
|
848
|
+
/**
|
|
849
|
+
* Bounded preflight that is contractually incapable of provider mutation or
|
|
850
|
+
* protocol acknowledgement may run first. This callback must be awaited
|
|
851
|
+
* before the first effect-capable process or externally observable action.
|
|
852
|
+
*/
|
|
853
|
+
readonly beforeExternalBegin: () => Promise<void>;
|
|
854
|
+
};
|
|
855
|
+
|
|
856
|
+
export type ProviderPluginLinkedDeviceLifecycleRuntimeV1 = {
|
|
857
|
+
readonly inspect: (
|
|
858
|
+
environment: Readonly<Record<string, string | undefined>>,
|
|
859
|
+
) => Promise<ProviderPluginLinkedDeviceRuntimeStatusV1>;
|
|
860
|
+
readonly pair: (
|
|
861
|
+
auth: GhostgetAuth,
|
|
862
|
+
options: {
|
|
863
|
+
readonly phone?: string;
|
|
864
|
+
readonly environment: Readonly<Record<string, string | undefined>>;
|
|
865
|
+
readonly attempt: ProviderPluginLinkedDeviceAttemptBoundaryV1;
|
|
866
|
+
},
|
|
867
|
+
) => Promise<string>;
|
|
868
|
+
readonly syncOnce: (
|
|
869
|
+
auth: GhostgetAuth,
|
|
870
|
+
options: {
|
|
871
|
+
readonly environment: Readonly<Record<string, string | undefined>>;
|
|
872
|
+
readonly attempt: ProviderPluginLinkedDeviceAttemptBoundaryV1;
|
|
873
|
+
},
|
|
874
|
+
) => Promise<ProviderPluginLinkedDeviceSyncResultV1>;
|
|
875
|
+
};
|
|
876
|
+
|
|
877
|
+
export type WebSessionPluginRuntimeV1 = {
|
|
878
|
+
readonly probe: (
|
|
879
|
+
auth: GhostgetAuth,
|
|
880
|
+
options?: ProviderPluginSubjectProbeOptionsV1,
|
|
881
|
+
) => Promise<string>;
|
|
882
|
+
readonly execute: WebSessionOperationExecutor;
|
|
883
|
+
readonly executeMessagingPart?: ProviderPluginMessagingActionExecutorV1;
|
|
884
|
+
readonly executePublic?: PublicWebSessionOperationExecutor;
|
|
885
|
+
readonly reconcile?: (
|
|
886
|
+
operation: string,
|
|
887
|
+
input: OperationInput,
|
|
888
|
+
auth: GhostgetAuth,
|
|
889
|
+
context?: ProviderPluginReconciliationContextV1,
|
|
890
|
+
) => Promise<ProviderPluginReconciliationReadbackV1>;
|
|
891
|
+
readonly linkedDeviceLifecycle?: ProviderPluginLinkedDeviceLifecycleRuntimeV1;
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
export type LocalCliPluginRuntimeV1 = {
|
|
895
|
+
readonly inspect: (
|
|
896
|
+
environment: Readonly<Record<string, string | undefined>>,
|
|
897
|
+
options?: {
|
|
898
|
+
readonly registerCleanupBarrier?: ProviderPluginCleanupBarrierRegistrar;
|
|
899
|
+
},
|
|
900
|
+
) => Promise<LocalCliPluginRuntimeStatusV1>;
|
|
901
|
+
readonly probe: (
|
|
902
|
+
auth: GhostgetAuth,
|
|
903
|
+
options?: ProviderPluginSubjectProbeOptionsV1,
|
|
904
|
+
) => Promise<string>;
|
|
905
|
+
readonly execute: LocalCliOperationExecutor;
|
|
906
|
+
readonly executeMessagingPart?: ProviderPluginMessagingActionExecutorV1;
|
|
907
|
+
readonly reconcile?: (
|
|
908
|
+
operation: string,
|
|
909
|
+
input: OperationInput,
|
|
910
|
+
auth: GhostgetAuth,
|
|
911
|
+
context?: ProviderPluginReconciliationContextV1,
|
|
912
|
+
options?: ProviderPluginReconciliationOptionsV1,
|
|
913
|
+
) => Promise<ProviderPluginReconciliationReadbackV1>;
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
export type LocalCliPluginRuntimeStatusV1 = {
|
|
917
|
+
readonly ready: boolean;
|
|
918
|
+
readonly platform: string;
|
|
919
|
+
readonly arch: string;
|
|
920
|
+
readonly version: string | null;
|
|
921
|
+
readonly executableSha256: string | null;
|
|
922
|
+
readonly reason: string | null;
|
|
923
|
+
};
|
|
924
|
+
|
|
925
|
+
export type ProviderApiPluginRuntimeHooksV1 = {
|
|
926
|
+
readonly loadRuntime: () => Promise<ProviderApiPluginRuntimeV1>;
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
export type WebSessionPluginRuntimeHooksV1 = {
|
|
930
|
+
readonly loadRuntime: () => Promise<WebSessionPluginRuntimeV1>;
|
|
931
|
+
};
|
|
932
|
+
|
|
933
|
+
export type LocalCliPluginRuntimeHooksV1 = {
|
|
934
|
+
readonly loadRuntime: () => Promise<LocalCliPluginRuntimeV1>;
|
|
935
|
+
};
|
|
936
|
+
|
|
937
|
+
type ProviderPluginBindingDefinitionBaseV1 = {
|
|
938
|
+
readonly surfaceId: string;
|
|
939
|
+
/**
|
|
940
|
+
* Canonical provider/service authority used by manifests, receipts, and
|
|
941
|
+
* account realms. For network transports this is also the primary endpoint;
|
|
942
|
+
* local-cli bindings keep child/loopback endpoints private to code-owned
|
|
943
|
+
* runtime logic and never expose them as manifest authority.
|
|
944
|
+
*/
|
|
945
|
+
readonly origin: `https://${string}`;
|
|
946
|
+
/**
|
|
947
|
+
* Exact public origins an adapter manifest must declare. Defaults to
|
|
948
|
+
* `[origin]`; official APIs can bind a public product origin separately
|
|
949
|
+
* from their API transport endpoint.
|
|
950
|
+
*/
|
|
951
|
+
readonly manifestOrigins?: readonly `https://${string}`[];
|
|
952
|
+
/**
|
|
953
|
+
* Hostname suffixes reserved from generic browser/template transports.
|
|
954
|
+
* Defaults to the exact transport and manifest hostnames.
|
|
955
|
+
*/
|
|
956
|
+
readonly protectedHostnameFamilies?: readonly string[];
|
|
957
|
+
readonly authKinds: readonly ProviderPluginAuthKind[];
|
|
958
|
+
readonly subject: ProviderPluginSubjectDefinitionV1;
|
|
959
|
+
readonly messaging?: ProviderPluginMessagingDefinitionV1;
|
|
960
|
+
};
|
|
961
|
+
|
|
962
|
+
export type ProviderApiPluginBindingDefinitionV1 =
|
|
963
|
+
ProviderPluginBindingDefinitionBaseV1 & {
|
|
964
|
+
readonly transport: "provider-api";
|
|
965
|
+
/**
|
|
966
|
+
* Every exact HTTPS origin that may receive this binding's OAuth bearer
|
|
967
|
+
* token. Defaults to `[origin]`; declare auxiliary provider APIs explicitly.
|
|
968
|
+
*/
|
|
969
|
+
readonly runtimeOrigins?: readonly `https://${string}`[];
|
|
970
|
+
readonly operations: readonly ProviderApiPluginOperationDefinitionV1[];
|
|
971
|
+
readonly runtime: ProviderApiPluginRuntimeHooksV1;
|
|
972
|
+
};
|
|
973
|
+
|
|
974
|
+
export type WebSessionApiPluginBindingDefinitionV1 =
|
|
975
|
+
ProviderPluginBindingDefinitionBaseV1 & {
|
|
976
|
+
readonly transport: "web-session-api";
|
|
977
|
+
readonly operations: readonly WebSessionPluginOperationDefinitionV1[];
|
|
978
|
+
readonly runtime: WebSessionPluginRuntimeHooksV1;
|
|
979
|
+
};
|
|
980
|
+
|
|
981
|
+
export type LinkedDevicePluginBindingDefinitionV1 =
|
|
982
|
+
ProviderPluginBindingDefinitionBaseV1 & {
|
|
983
|
+
readonly transport: "linked-device";
|
|
984
|
+
readonly operations: readonly WebSessionPluginOperationDefinitionV1[];
|
|
985
|
+
readonly linkedDeviceLifecycle?: {
|
|
986
|
+
readonly pair: true;
|
|
987
|
+
readonly syncOnce: true;
|
|
988
|
+
readonly inspect: true;
|
|
989
|
+
};
|
|
990
|
+
readonly runtime: WebSessionPluginRuntimeHooksV1;
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
export type LocalCliPluginBindingDefinitionV1 =
|
|
994
|
+
ProviderPluginBindingDefinitionBaseV1 & {
|
|
995
|
+
readonly transport: "local-cli";
|
|
996
|
+
readonly tool: LocalCliToolIdentityV1;
|
|
997
|
+
readonly operations: readonly LocalCliPluginOperationDefinitionV1[];
|
|
998
|
+
readonly runtime: LocalCliPluginRuntimeHooksV1;
|
|
999
|
+
};
|
|
1000
|
+
|
|
1001
|
+
export type ProviderPluginBindingDefinitionV1 =
|
|
1002
|
+
| ProviderApiPluginBindingDefinitionV1
|
|
1003
|
+
| WebSessionApiPluginBindingDefinitionV1
|
|
1004
|
+
| LinkedDevicePluginBindingDefinitionV1
|
|
1005
|
+
| LocalCliPluginBindingDefinitionV1;
|
|
1006
|
+
|
|
1007
|
+
export type ProviderPluginDefinitionV1 = {
|
|
1008
|
+
readonly apiVersion: typeof PROVIDER_PLUGIN_API_VERSION;
|
|
1009
|
+
readonly id: string;
|
|
1010
|
+
readonly version: string;
|
|
1011
|
+
readonly displayName: string;
|
|
1012
|
+
readonly sourceKind: "built-in" | "source";
|
|
1013
|
+
readonly implementationSources: readonly ProviderPluginImplementationSourceDefinitionV1[];
|
|
1014
|
+
readonly bindings: readonly ProviderPluginBindingDefinitionV1[];
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
export type PortableProviderPluginProjectionDefinitionV1 = {
|
|
1018
|
+
readonly apiVersion: typeof PROVIDER_PLUGIN_API_VERSION;
|
|
1019
|
+
readonly id: string;
|
|
1020
|
+
readonly version: string;
|
|
1021
|
+
readonly displayName: string;
|
|
1022
|
+
readonly sourceKind: "portable";
|
|
1023
|
+
readonly package: VerifiedPortableProviderPluginPackage;
|
|
1024
|
+
readonly bindings: readonly {
|
|
1025
|
+
readonly adapterId: string;
|
|
1026
|
+
readonly manifest: GhostgetManifest;
|
|
1027
|
+
readonly portableBinding: PortablePackageBindingV1;
|
|
1028
|
+
readonly binding: ProviderPluginBindingDefinitionV1;
|
|
1029
|
+
}[];
|
|
1030
|
+
};
|
|
1031
|
+
|
|
1032
|
+
type ProviderPluginBindingBaseV1 = Omit<
|
|
1033
|
+
ProviderPluginBindingDefinitionBaseV1,
|
|
1034
|
+
"manifestOrigins" | "protectedHostnameFamilies" | "subject" | "messaging"
|
|
1035
|
+
> & {
|
|
1036
|
+
readonly manifestOrigins: readonly `https://${string}`[];
|
|
1037
|
+
readonly protectedHostnameFamilies: readonly string[];
|
|
1038
|
+
readonly subject: ProviderPluginSubjectV1;
|
|
1039
|
+
readonly messaging?: ProviderPluginMessagingDefinitionV1;
|
|
1040
|
+
};
|
|
1041
|
+
|
|
1042
|
+
export type ProviderApiPluginBindingV1 = ProviderPluginBindingBaseV1 & {
|
|
1043
|
+
readonly transport: "provider-api";
|
|
1044
|
+
readonly runtimeOrigins: readonly `https://${string}`[];
|
|
1045
|
+
readonly operations: readonly ProviderApiPluginOperationV1[];
|
|
1046
|
+
readonly loadRuntime: ProviderApiPluginRuntimeHooksV1["loadRuntime"];
|
|
1047
|
+
readonly execute: ProviderApiPluginRuntimeV1["execute"];
|
|
1048
|
+
readonly executeMessagingPart?: ProviderPluginMessagingActionExecutorV1;
|
|
1049
|
+
};
|
|
1050
|
+
|
|
1051
|
+
export type WebSessionApiPluginBindingV1 = ProviderPluginBindingBaseV1 & {
|
|
1052
|
+
readonly transport: "web-session-api";
|
|
1053
|
+
readonly operations: readonly WebSessionPluginOperationV1[];
|
|
1054
|
+
readonly loadRuntime: WebSessionPluginRuntimeHooksV1["loadRuntime"];
|
|
1055
|
+
readonly execute: WebSessionPluginRuntimeV1["execute"];
|
|
1056
|
+
readonly executeMessagingPart?: ProviderPluginMessagingActionExecutorV1;
|
|
1057
|
+
readonly executePublic?: NonNullable<WebSessionPluginRuntimeV1["executePublic"]>;
|
|
1058
|
+
readonly reconcile?: NonNullable<WebSessionPluginRuntimeV1["reconcile"]>;
|
|
1059
|
+
};
|
|
1060
|
+
|
|
1061
|
+
export type LinkedDevicePluginBindingV1 = ProviderPluginBindingBaseV1 & {
|
|
1062
|
+
readonly transport: "linked-device";
|
|
1063
|
+
readonly operations: readonly WebSessionPluginOperationV1[];
|
|
1064
|
+
readonly loadRuntime: WebSessionPluginRuntimeHooksV1["loadRuntime"];
|
|
1065
|
+
readonly execute: WebSessionPluginRuntimeV1["execute"];
|
|
1066
|
+
readonly executeMessagingPart?: ProviderPluginMessagingActionExecutorV1;
|
|
1067
|
+
readonly reconcile?: NonNullable<WebSessionPluginRuntimeV1["reconcile"]>;
|
|
1068
|
+
readonly linkedDeviceLifecycle?: ProviderPluginLinkedDeviceLifecycleRuntimeV1;
|
|
1069
|
+
};
|
|
1070
|
+
|
|
1071
|
+
export type LocalCliPluginBindingV1 = ProviderPluginBindingBaseV1 & {
|
|
1072
|
+
readonly transport: "local-cli";
|
|
1073
|
+
readonly tool: LocalCliToolIdentityV1;
|
|
1074
|
+
readonly operations: readonly LocalCliPluginOperationV1[];
|
|
1075
|
+
readonly loadRuntime: LocalCliPluginRuntimeHooksV1["loadRuntime"];
|
|
1076
|
+
readonly inspect: LocalCliPluginRuntimeV1["inspect"];
|
|
1077
|
+
readonly execute: LocalCliPluginRuntimeV1["execute"];
|
|
1078
|
+
readonly executeMessagingPart?: ProviderPluginMessagingActionExecutorV1;
|
|
1079
|
+
readonly reconcile?: NonNullable<LocalCliPluginRuntimeV1["reconcile"]>;
|
|
1080
|
+
};
|
|
1081
|
+
|
|
1082
|
+
export type ProviderPluginBindingV1 =
|
|
1083
|
+
| ProviderApiPluginBindingV1
|
|
1084
|
+
| WebSessionApiPluginBindingV1
|
|
1085
|
+
| LinkedDevicePluginBindingV1
|
|
1086
|
+
| LocalCliPluginBindingV1;
|
|
1087
|
+
|
|
1088
|
+
|
|
1089
|
+
export type ProviderPluginV1 = Omit<
|
|
1090
|
+
ProviderPluginDefinitionV1,
|
|
1091
|
+
"bindings" | "implementationSources" | "sourceKind"
|
|
1092
|
+
> & {
|
|
1093
|
+
readonly sourceKind: "built-in" | "source" | "portable";
|
|
1094
|
+
readonly implementationSources: readonly ProviderPluginImplementationSourceV1[];
|
|
1095
|
+
readonly bindings: readonly ProviderPluginBindingV1[];
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
const pluginVersionPattern = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/u;
|
|
1099
|
+
const sourceLabelPattern = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,255}$/u;
|
|
1100
|
+
const authKinds = new Set<ProviderPluginAuthKind>([
|
|
1101
|
+
"cookie-source",
|
|
1102
|
+
"cookies-file",
|
|
1103
|
+
"browser-profile",
|
|
1104
|
+
"oauth-token-file",
|
|
1105
|
+
"linked-device-store",
|
|
1106
|
+
]);
|
|
1107
|
+
const validatedProviderPlugins = new WeakSet<object>();
|
|
1108
|
+
const portableProviderPluginArtifacts = new WeakMap<object, string>();
|
|
1109
|
+
const portableProviderPluginAdapters = new WeakMap<
|
|
1110
|
+
ProviderPluginBindingV1,
|
|
1111
|
+
{ readonly adapterId: string; readonly manifest: GhostgetManifest }
|
|
1112
|
+
>();
|
|
1113
|
+
const portableProviderPluginOperationIdentities = new WeakMap<
|
|
1114
|
+
ProviderPluginBindingV1,
|
|
1115
|
+
ReadonlyMap<string, PortableOperationIdentityV1>
|
|
1116
|
+
>();
|
|
1117
|
+
const portableProviderPluginSubjectProbeIdentities = new WeakMap<
|
|
1118
|
+
ProviderPluginBindingV1,
|
|
1119
|
+
PortableOperationIdentityV1
|
|
1120
|
+
>();
|
|
1121
|
+
const providerPluginEvaluationSourceDigests = new WeakMap<
|
|
1122
|
+
ProviderPluginV1,
|
|
1123
|
+
ReadonlyMap<string, string>
|
|
1124
|
+
>();
|
|
1125
|
+
const providerPluginEvaluationInstalledPackageDigests = new WeakMap<
|
|
1126
|
+
ProviderPluginV1,
|
|
1127
|
+
ReadonlyMap<string, string>
|
|
1128
|
+
>();
|
|
1129
|
+
const providerPluginSourceRoot = realpathSync(fileURLToPath(new URL(".", import.meta.url)));
|
|
1130
|
+
|
|
1131
|
+
function recordValue(value: unknown): value is Readonly<Record<string, unknown>> {
|
|
1132
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function packageUsesWorkspaceDependencies(packageRoot: string): boolean {
|
|
1136
|
+
let value: unknown;
|
|
1137
|
+
try {
|
|
1138
|
+
value = JSON.parse(readFileSync(resolve(packageRoot, "package.json"), "utf8")) as unknown;
|
|
1139
|
+
} catch {
|
|
1140
|
+
return false;
|
|
1141
|
+
}
|
|
1142
|
+
if (!recordValue(value)) return false;
|
|
1143
|
+
const protocol = ["workspace", ":"].join("");
|
|
1144
|
+
return ["dependencies", "devDependencies", "optionalDependencies"]
|
|
1145
|
+
.some((field) => {
|
|
1146
|
+
const dependencies = value[field];
|
|
1147
|
+
return recordValue(dependencies)
|
|
1148
|
+
&& Object.values(dependencies).some(
|
|
1149
|
+
(specifier) => typeof specifier === "string" && specifier.startsWith(protocol),
|
|
1150
|
+
);
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function enclosingWorkspaceRoot(packageRoot: string): string {
|
|
1155
|
+
if (!packageUsesWorkspaceDependencies(packageRoot)) return packageRoot;
|
|
1156
|
+
let candidate = dirname(packageRoot);
|
|
1157
|
+
for (;;) {
|
|
1158
|
+
const manifestPath = resolve(candidate, "package.json");
|
|
1159
|
+
if (existsSync(manifestPath)) {
|
|
1160
|
+
try {
|
|
1161
|
+
const value = JSON.parse(readFileSync(manifestPath, "utf8")) as unknown;
|
|
1162
|
+
if (
|
|
1163
|
+
recordValue(value)
|
|
1164
|
+
&& (Array.isArray(value.workspaces) || recordValue(value.workspaces))
|
|
1165
|
+
) {
|
|
1166
|
+
return realpathSync(candidate);
|
|
1167
|
+
}
|
|
1168
|
+
} catch {
|
|
1169
|
+
// A malformed ancestor manifest cannot define the trusted boundary.
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
const parent = dirname(candidate);
|
|
1173
|
+
if (parent === candidate) {
|
|
1174
|
+
throw new Error("Ghostget workspace dependencies have no enclosing workspace root");
|
|
1175
|
+
}
|
|
1176
|
+
candidate = parent;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function isWithinProviderPluginPhysicalRoot(
|
|
1181
|
+
root: string,
|
|
1182
|
+
candidate: string,
|
|
1183
|
+
): boolean {
|
|
1184
|
+
const path = relative(root, candidate);
|
|
1185
|
+
return path === ""
|
|
1186
|
+
|| (path !== ".." && !path.startsWith(`..${sep}`));
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
type PhysicalNodeModulesPackage = {
|
|
1190
|
+
readonly nodeModulesDirectory: string;
|
|
1191
|
+
readonly root: string;
|
|
1192
|
+
};
|
|
1193
|
+
|
|
1194
|
+
function physicalNodeModulesPackage(
|
|
1195
|
+
entryPath: string,
|
|
1196
|
+
): PhysicalNodeModulesPackage | undefined {
|
|
1197
|
+
const parsed = parse(entryPath);
|
|
1198
|
+
const segments = entryPath.slice(parsed.root.length).split(sep);
|
|
1199
|
+
for (let index = segments.length - 1; index >= 0; index -= 1) {
|
|
1200
|
+
if (segments[index] !== "node_modules") continue;
|
|
1201
|
+
const first = segments[index + 1];
|
|
1202
|
+
if (first === undefined || first === "") continue;
|
|
1203
|
+
const packageSegments = first.startsWith("@")
|
|
1204
|
+
? (
|
|
1205
|
+
segments[index + 2] === undefined
|
|
1206
|
+
? undefined
|
|
1207
|
+
: [first, segments[index + 2]!]
|
|
1208
|
+
)
|
|
1209
|
+
: [first];
|
|
1210
|
+
if (packageSegments === undefined) continue;
|
|
1211
|
+
const root = resolve(
|
|
1212
|
+
parsed.root,
|
|
1213
|
+
...segments.slice(0, index + 1 + packageSegments.length),
|
|
1214
|
+
);
|
|
1215
|
+
if (
|
|
1216
|
+
isWithinProviderPluginPhysicalRoot(root, entryPath)
|
|
1217
|
+
&& existsSync(resolve(root, "package.json"))
|
|
1218
|
+
) {
|
|
1219
|
+
return Object.freeze({
|
|
1220
|
+
nodeModulesDirectory: resolve(
|
|
1221
|
+
parsed.root,
|
|
1222
|
+
...segments.slice(0, index + 1),
|
|
1223
|
+
),
|
|
1224
|
+
root,
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
return undefined;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
function enclosingNodeModulesBoundary(packageRoot: string): string | undefined {
|
|
1232
|
+
const owner = physicalNodeModulesPackage(packageRoot);
|
|
1233
|
+
if (owner === undefined || owner.root !== packageRoot) return undefined;
|
|
1234
|
+
const bunInstanceDirectory = dirname(owner.nodeModulesDirectory);
|
|
1235
|
+
const bunStoreDirectory = dirname(bunInstanceDirectory);
|
|
1236
|
+
const bunInstallNodeModules = dirname(bunStoreDirectory);
|
|
1237
|
+
return basename(bunStoreDirectory) === ".bun"
|
|
1238
|
+
&& basename(bunInstallNodeModules) === "node_modules"
|
|
1239
|
+
? bunInstallNodeModules
|
|
1240
|
+
: owner.nodeModulesDirectory;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
/** Checked source boundary: package root standalone, enclosing workspace in development. */
|
|
1244
|
+
export const providerPluginPackageRoot = realpathSync(
|
|
1245
|
+
resolve(providerPluginSourceRoot, ".."),
|
|
1246
|
+
);
|
|
1247
|
+
export const providerPluginRepositoryRoot = enclosingWorkspaceRoot(
|
|
1248
|
+
providerPluginPackageRoot,
|
|
1249
|
+
);
|
|
1250
|
+
const providerPluginInstallationNodeModulesBoundary =
|
|
1251
|
+
enclosingNodeModulesBoundary(providerPluginPackageRoot);
|
|
1252
|
+
|
|
1253
|
+
type ProviderPluginPhysicalPath =
|
|
1254
|
+
| { readonly kind: "repository" }
|
|
1255
|
+
| { readonly kind: "installed-package"; readonly root: string };
|
|
1256
|
+
|
|
1257
|
+
export function isProviderPluginRepositorySourcePath(
|
|
1258
|
+
repositoryRoot: string,
|
|
1259
|
+
path: string,
|
|
1260
|
+
): boolean {
|
|
1261
|
+
const repositoryRelative = relative(repositoryRoot, path);
|
|
1262
|
+
return (
|
|
1263
|
+
repositoryRelative === ""
|
|
1264
|
+
|| (
|
|
1265
|
+
repositoryRelative !== ".."
|
|
1266
|
+
&& !repositoryRelative.startsWith(`..${sep}`)
|
|
1267
|
+
)
|
|
1268
|
+
)
|
|
1269
|
+
&& (
|
|
1270
|
+
repositoryRelative === ""
|
|
1271
|
+
|| !repositoryRelative.split(sep).includes("node_modules")
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
/**
|
|
1276
|
+
* Classify a real module path without confusing an installed Ghostget package's
|
|
1277
|
+
* own outer `node_modules` segments with one of its dependencies. Standalone
|
|
1278
|
+
* dependencies may be hoisted beside Ghostget, while development dependencies
|
|
1279
|
+
* remain constrained to the checked repository.
|
|
1280
|
+
*/
|
|
1281
|
+
export function classifyProviderPluginPhysicalPath(
|
|
1282
|
+
path: string,
|
|
1283
|
+
): ProviderPluginPhysicalPath {
|
|
1284
|
+
const repositoryRelative = relative(providerPluginRepositoryRoot, path);
|
|
1285
|
+
const withinRepository = repositoryRelative === ""
|
|
1286
|
+
|| (
|
|
1287
|
+
repositoryRelative !== ".."
|
|
1288
|
+
&& !repositoryRelative.startsWith(`..${sep}`)
|
|
1289
|
+
);
|
|
1290
|
+
if (isProviderPluginRepositorySourcePath(
|
|
1291
|
+
providerPluginRepositoryRoot,
|
|
1292
|
+
path,
|
|
1293
|
+
)) {
|
|
1294
|
+
return Object.freeze({ kind: "repository" });
|
|
1295
|
+
}
|
|
1296
|
+
if (
|
|
1297
|
+
!withinRepository
|
|
1298
|
+
&& (
|
|
1299
|
+
providerPluginInstallationNodeModulesBoundary === undefined
|
|
1300
|
+
|| !isWithinProviderPluginPhysicalRoot(
|
|
1301
|
+
providerPluginInstallationNodeModulesBoundary,
|
|
1302
|
+
path,
|
|
1303
|
+
)
|
|
1304
|
+
)
|
|
1305
|
+
) {
|
|
1306
|
+
throw new Error(
|
|
1307
|
+
"provider plugin physical path resolves outside the repository and checked package installation",
|
|
1308
|
+
);
|
|
1309
|
+
}
|
|
1310
|
+
const owner = physicalNodeModulesPackage(path);
|
|
1311
|
+
if (
|
|
1312
|
+
owner === undefined
|
|
1313
|
+
|| (
|
|
1314
|
+
providerPluginInstallationNodeModulesBoundary !== undefined
|
|
1315
|
+
&& !withinRepository
|
|
1316
|
+
&& !isWithinProviderPluginPhysicalRoot(
|
|
1317
|
+
providerPluginInstallationNodeModulesBoundary,
|
|
1318
|
+
owner.root,
|
|
1319
|
+
)
|
|
1320
|
+
)
|
|
1321
|
+
) {
|
|
1322
|
+
throw new Error(
|
|
1323
|
+
"provider plugin physical path has no physical node_modules owner",
|
|
1324
|
+
);
|
|
1325
|
+
}
|
|
1326
|
+
return Object.freeze({ kind: "installed-package", root: owner.root });
|
|
1327
|
+
}
|
|
1328
|
+
const providerPluginApiBoundaryPath = realpathSync(
|
|
1329
|
+
resolve(providerPluginSourceRoot, "provider-plugin.ts"),
|
|
1330
|
+
);
|
|
1331
|
+
const MAX_PROVIDER_PLUGIN_EVALUATION_SOURCE_BYTES = 16 * 1024 * 1024;
|
|
1332
|
+
const MAX_PROVIDER_PLUGIN_EVALUATION_CLOSURE_SOURCES = 2_000;
|
|
1333
|
+
const MAX_PROVIDER_PLUGIN_EVALUATION_CLOSURE_BYTES = 128 * 1024 * 1024;
|
|
1334
|
+
const MAX_PROVIDER_PLUGIN_EVALUATION_IMPORTS_PER_MODULE = 4_096;
|
|
1335
|
+
const MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGES = 128;
|
|
1336
|
+
const MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_FILES = 4_096;
|
|
1337
|
+
const MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_DIRECTORIES = 1_024;
|
|
1338
|
+
const MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_DEPTH = 32;
|
|
1339
|
+
const MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_PATH_BYTES = 1_024;
|
|
1340
|
+
const MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_BYTES = 128 * 1024 * 1024;
|
|
1341
|
+
const MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_CACHE_ENTRIES = 256;
|
|
1342
|
+
const providerPluginEvaluationImportScanners = Object.freeze({
|
|
1343
|
+
js: new Bun.Transpiler({ loader: "js" }),
|
|
1344
|
+
ts: new Bun.Transpiler({ loader: "ts" }),
|
|
1345
|
+
});
|
|
1346
|
+
const providerPluginEvaluationModuleExtensions = new Set([
|
|
1347
|
+
".cjs",
|
|
1348
|
+
".cts",
|
|
1349
|
+
".js",
|
|
1350
|
+
".mjs",
|
|
1351
|
+
".mts",
|
|
1352
|
+
".ts",
|
|
1353
|
+
]);
|
|
1354
|
+
const providerPluginEvaluationRuntimeBuiltins = new Set(
|
|
1355
|
+
builtinModules.flatMap((name) =>
|
|
1356
|
+
name.startsWith("node:") ? [name, name.slice("node:".length)] : [name]),
|
|
1357
|
+
);
|
|
1358
|
+
|
|
1359
|
+
type ProviderPluginEvaluationPackageTreeEntry =
|
|
1360
|
+
| {
|
|
1361
|
+
readonly path: string;
|
|
1362
|
+
readonly kind: "directory" | "guard-directory" | "file";
|
|
1363
|
+
readonly stats: BigIntStats;
|
|
1364
|
+
}
|
|
1365
|
+
| {
|
|
1366
|
+
readonly path: string;
|
|
1367
|
+
readonly kind: "link";
|
|
1368
|
+
readonly stats: BigIntStats;
|
|
1369
|
+
readonly targetPath: string;
|
|
1370
|
+
};
|
|
1371
|
+
|
|
1372
|
+
type ProviderPluginEvaluationPackageTreeWalk = {
|
|
1373
|
+
readonly entries: readonly ProviderPluginEvaluationPackageTreeEntry[];
|
|
1374
|
+
readonly fileCount: number;
|
|
1375
|
+
readonly linkCount: number;
|
|
1376
|
+
readonly directoryCount: number;
|
|
1377
|
+
readonly totalBytes: number;
|
|
1378
|
+
};
|
|
1379
|
+
|
|
1380
|
+
type ProviderPluginEvaluationPackageSnapshot = {
|
|
1381
|
+
readonly treeSha256: string;
|
|
1382
|
+
readonly totalBytes: number;
|
|
1383
|
+
readonly verificationWalk: ProviderPluginEvaluationPackageTreeWalk;
|
|
1384
|
+
};
|
|
1385
|
+
|
|
1386
|
+
const providerPluginEvaluationPackageCache =
|
|
1387
|
+
new Map<string, ProviderPluginEvaluationPackageSnapshot>();
|
|
1388
|
+
|
|
1389
|
+
function sameEvaluationSourceSnapshot(
|
|
1390
|
+
left: BigIntStats,
|
|
1391
|
+
right: BigIntStats,
|
|
1392
|
+
): boolean {
|
|
1393
|
+
return left.dev === right.dev
|
|
1394
|
+
&& left.ino === right.ino
|
|
1395
|
+
&& left.size === right.size
|
|
1396
|
+
&& left.mode === right.mode
|
|
1397
|
+
&& left.mtimeNs === right.mtimeNs
|
|
1398
|
+
&& left.ctimeNs === right.ctimeNs;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
export function isProviderPluginInstalledBinaryDirectory(
|
|
1402
|
+
relativeDirectory: string,
|
|
1403
|
+
): boolean {
|
|
1404
|
+
return relativeDirectory === "node_modules/.bin"
|
|
1405
|
+
|| relativeDirectory.endsWith("/node_modules/.bin");
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
export type ProviderPluginInstalledBinaryLinkTarget = Readonly<{
|
|
1409
|
+
path: string;
|
|
1410
|
+
stats: BigIntStats;
|
|
1411
|
+
}>;
|
|
1412
|
+
|
|
1413
|
+
/**
|
|
1414
|
+
* Bind a directory that is security-relevant to an installed binary target
|
|
1415
|
+
* without making installer topology part of the package's durable identity.
|
|
1416
|
+
*/
|
|
1417
|
+
export function providerPluginInstalledGuardDirectorySnapshot(
|
|
1418
|
+
path: string,
|
|
1419
|
+
label: string,
|
|
1420
|
+
): BigIntStats {
|
|
1421
|
+
let before: BigIntStats;
|
|
1422
|
+
try {
|
|
1423
|
+
before = lstatSync(path, { bigint: true });
|
|
1424
|
+
} catch {
|
|
1425
|
+
throw new Error(`${label} changed while it was bound`);
|
|
1426
|
+
}
|
|
1427
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
1428
|
+
if (
|
|
1429
|
+
before.isSymbolicLink()
|
|
1430
|
+
|| !before.isDirectory()
|
|
1431
|
+
|| (uid !== undefined && before.uid !== BigInt(uid))
|
|
1432
|
+
|| (before.mode & 0o022n) !== 0n
|
|
1433
|
+
) {
|
|
1434
|
+
throw new Error(`${label} is unsafe`);
|
|
1435
|
+
}
|
|
1436
|
+
let descriptor: number;
|
|
1437
|
+
try {
|
|
1438
|
+
descriptor = openSync(
|
|
1439
|
+
path,
|
|
1440
|
+
constants.O_RDONLY
|
|
1441
|
+
| ("O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0)
|
|
1442
|
+
| ("O_DIRECTORY" in constants ? constants.O_DIRECTORY : 0),
|
|
1443
|
+
);
|
|
1444
|
+
} catch {
|
|
1445
|
+
throw new Error(`${label} changed while it was bound`);
|
|
1446
|
+
}
|
|
1447
|
+
try {
|
|
1448
|
+
const bound = fstatSync(descriptor, { bigint: true });
|
|
1449
|
+
let current: BigIntStats;
|
|
1450
|
+
try {
|
|
1451
|
+
current = lstatSync(path, { bigint: true });
|
|
1452
|
+
} catch {
|
|
1453
|
+
throw new Error(`${label} changed while it was bound`);
|
|
1454
|
+
}
|
|
1455
|
+
if (
|
|
1456
|
+
current.isSymbolicLink()
|
|
1457
|
+
|| !current.isDirectory()
|
|
1458
|
+
|| !sameEvaluationSourceSnapshot(before, bound)
|
|
1459
|
+
|| !sameEvaluationSourceSnapshot(bound, current)
|
|
1460
|
+
) {
|
|
1461
|
+
throw new Error(`${label} changed while it was bound`);
|
|
1462
|
+
}
|
|
1463
|
+
return before;
|
|
1464
|
+
} finally {
|
|
1465
|
+
closeSync(descriptor);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
/**
|
|
1470
|
+
* Bind an installer-generated npm binary link to one canonical regular file
|
|
1471
|
+
* inside the same captured package tree. Wrapper files remain ordinary files.
|
|
1472
|
+
*/
|
|
1473
|
+
export function providerPluginCanonicalInstalledBinaryLinkTarget(
|
|
1474
|
+
root: string,
|
|
1475
|
+
linkPath: string,
|
|
1476
|
+
initialStats: BigIntStats,
|
|
1477
|
+
maximumPathBytes: number,
|
|
1478
|
+
label: string,
|
|
1479
|
+
): ProviderPluginInstalledBinaryLinkTarget {
|
|
1480
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
1481
|
+
if (
|
|
1482
|
+
!initialStats.isSymbolicLink()
|
|
1483
|
+
|| (uid !== undefined && initialStats.uid !== BigInt(uid))
|
|
1484
|
+
) {
|
|
1485
|
+
throw new Error(`${label} is not an owned symbolic link`);
|
|
1486
|
+
}
|
|
1487
|
+
let firstTargetBytes: Buffer;
|
|
1488
|
+
let secondTargetBytes: Buffer;
|
|
1489
|
+
let middleStats: BigIntStats;
|
|
1490
|
+
let finalStats: BigIntStats;
|
|
1491
|
+
try {
|
|
1492
|
+
firstTargetBytes = readlinkSync(linkPath, { encoding: "buffer" });
|
|
1493
|
+
middleStats = lstatSync(linkPath, { bigint: true });
|
|
1494
|
+
secondTargetBytes = readlinkSync(linkPath, { encoding: "buffer" });
|
|
1495
|
+
finalStats = lstatSync(linkPath, { bigint: true });
|
|
1496
|
+
} catch {
|
|
1497
|
+
throw new Error(`${label} changed while it was read`);
|
|
1498
|
+
}
|
|
1499
|
+
if (
|
|
1500
|
+
firstTargetBytes.byteLength < 1
|
|
1501
|
+
|| firstTargetBytes.byteLength > maximumPathBytes
|
|
1502
|
+
|| !firstTargetBytes.equals(secondTargetBytes)
|
|
1503
|
+
|| !middleStats.isSymbolicLink()
|
|
1504
|
+
|| !finalStats.isSymbolicLink()
|
|
1505
|
+
|| !sameEvaluationSourceSnapshot(initialStats, middleStats)
|
|
1506
|
+
|| !sameEvaluationSourceSnapshot(middleStats, finalStats)
|
|
1507
|
+
) {
|
|
1508
|
+
throw new Error(`${label} changed while it was read`);
|
|
1509
|
+
}
|
|
1510
|
+
let targetText: string;
|
|
1511
|
+
try {
|
|
1512
|
+
targetText = new TextDecoder("utf-8", { fatal: true }).decode(
|
|
1513
|
+
firstTargetBytes,
|
|
1514
|
+
);
|
|
1515
|
+
} catch {
|
|
1516
|
+
throw new Error(`${label} has invalid target text`);
|
|
1517
|
+
}
|
|
1518
|
+
if (
|
|
1519
|
+
targetText.includes("\u0000")
|
|
1520
|
+
|| targetText.includes("\\")
|
|
1521
|
+
|| isAbsolute(targetText)
|
|
1522
|
+
) {
|
|
1523
|
+
throw new Error(`${label} has an unsafe target`);
|
|
1524
|
+
}
|
|
1525
|
+
const lexicalTarget = resolve(dirname(linkPath), targetText);
|
|
1526
|
+
const targetPath = relative(root, lexicalTarget).split(sep).join("/");
|
|
1527
|
+
const canonicalTargetText = relative(dirname(linkPath), lexicalTarget)
|
|
1528
|
+
.split(sep).join("/");
|
|
1529
|
+
if (
|
|
1530
|
+
targetText !== canonicalTargetText
|
|
1531
|
+
|| targetPath === ""
|
|
1532
|
+
|| targetPath === "."
|
|
1533
|
+
|| targetPath.includes("\\")
|
|
1534
|
+
|| targetPath.includes("\u0000")
|
|
1535
|
+
|| Buffer.byteLength(targetPath, "utf8") > maximumPathBytes
|
|
1536
|
+
|| !isWithinProviderPluginPhysicalRoot(root, lexicalTarget)
|
|
1537
|
+
) {
|
|
1538
|
+
throw new Error(`${label} has a noncanonical or escaping target`);
|
|
1539
|
+
}
|
|
1540
|
+
let physicalTarget: string;
|
|
1541
|
+
let targetStats: BigIntStats;
|
|
1542
|
+
try {
|
|
1543
|
+
physicalTarget = realpathSync(linkPath);
|
|
1544
|
+
targetStats = lstatSync(lexicalTarget, { bigint: true });
|
|
1545
|
+
} catch {
|
|
1546
|
+
throw new Error(`${label} target is missing`);
|
|
1547
|
+
}
|
|
1548
|
+
if (
|
|
1549
|
+
physicalTarget !== lexicalTarget
|
|
1550
|
+
|| !targetStats.isFile()
|
|
1551
|
+
|| targetStats.isSymbolicLink()
|
|
1552
|
+
|| (uid !== undefined && targetStats.uid !== BigInt(uid))
|
|
1553
|
+
) {
|
|
1554
|
+
throw new Error(`${label} target is not one canonical owned regular file`);
|
|
1555
|
+
}
|
|
1556
|
+
return Object.freeze({ path: targetPath, stats: targetStats });
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
function readOwnedBoundedProviderPluginEvaluationFile(
|
|
1560
|
+
path: string,
|
|
1561
|
+
maximumBytes: number,
|
|
1562
|
+
label: string,
|
|
1563
|
+
): Buffer {
|
|
1564
|
+
let descriptor: number;
|
|
1565
|
+
try {
|
|
1566
|
+
descriptor = openSync(
|
|
1567
|
+
path,
|
|
1568
|
+
constants.O_RDONLY
|
|
1569
|
+
| ("O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0)
|
|
1570
|
+
| ("O_NONBLOCK" in constants ? constants.O_NONBLOCK : 0),
|
|
1571
|
+
);
|
|
1572
|
+
} catch {
|
|
1573
|
+
throw new Error(`${label} is unreadable`);
|
|
1574
|
+
}
|
|
1575
|
+
try {
|
|
1576
|
+
const before = fstatSync(descriptor, { bigint: true });
|
|
1577
|
+
const uid =
|
|
1578
|
+
typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
1579
|
+
if (
|
|
1580
|
+
!before.isFile()
|
|
1581
|
+
|| (uid !== undefined && before.uid !== BigInt(uid))
|
|
1582
|
+
|| before.size < 0n
|
|
1583
|
+
|| before.size > BigInt(maximumBytes)
|
|
1584
|
+
) {
|
|
1585
|
+
throw new Error(`${label} is not an owned bounded regular file`);
|
|
1586
|
+
}
|
|
1587
|
+
const bytes = Buffer.alloc(Number(before.size));
|
|
1588
|
+
let offset = 0;
|
|
1589
|
+
while (offset < bytes.byteLength) {
|
|
1590
|
+
const count = readSync(
|
|
1591
|
+
descriptor,
|
|
1592
|
+
bytes,
|
|
1593
|
+
offset,
|
|
1594
|
+
bytes.byteLength - offset,
|
|
1595
|
+
offset,
|
|
1596
|
+
);
|
|
1597
|
+
if (count === 0) break;
|
|
1598
|
+
offset += count;
|
|
1599
|
+
}
|
|
1600
|
+
const after = fstatSync(descriptor, { bigint: true });
|
|
1601
|
+
let current: BigIntStats;
|
|
1602
|
+
try {
|
|
1603
|
+
current = lstatSync(path, { bigint: true });
|
|
1604
|
+
} catch {
|
|
1605
|
+
throw new Error(`${label} changed while its definition was evaluated`);
|
|
1606
|
+
}
|
|
1607
|
+
if (
|
|
1608
|
+
offset !== bytes.byteLength
|
|
1609
|
+
|| current.isSymbolicLink()
|
|
1610
|
+
|| !current.isFile()
|
|
1611
|
+
|| !sameEvaluationSourceSnapshot(before, after)
|
|
1612
|
+
|| !sameEvaluationSourceSnapshot(after, current)
|
|
1613
|
+
) {
|
|
1614
|
+
throw new Error(`${label} changed while its definition was evaluated`);
|
|
1615
|
+
}
|
|
1616
|
+
return bytes;
|
|
1617
|
+
} finally {
|
|
1618
|
+
closeSync(descriptor);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
function readProviderPluginEvaluationSource(
|
|
1623
|
+
pluginId: string,
|
|
1624
|
+
path: string,
|
|
1625
|
+
label: string,
|
|
1626
|
+
): Buffer {
|
|
1627
|
+
return readOwnedBoundedProviderPluginEvaluationFile(
|
|
1628
|
+
path,
|
|
1629
|
+
MAX_PROVIDER_PLUGIN_EVALUATION_SOURCE_BYTES,
|
|
1630
|
+
`provider plugin ${pluginId} implementation source ${label}`,
|
|
1631
|
+
);
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
function compareProviderPluginEvaluationText(
|
|
1635
|
+
left: string,
|
|
1636
|
+
right: string,
|
|
1637
|
+
): number {
|
|
1638
|
+
return Buffer.compare(Buffer.from(left, "utf8"), Buffer.from(right, "utf8"));
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
function walkProviderPluginEvaluationPackageTree(
|
|
1642
|
+
root: string,
|
|
1643
|
+
): ProviderPluginEvaluationPackageTreeWalk {
|
|
1644
|
+
const entries: ProviderPluginEvaluationPackageTreeEntry[] = [];
|
|
1645
|
+
const capturedFiles = new Map<string, BigIntStats>();
|
|
1646
|
+
const capturedGuardDirectories = new Map<string, BigIntStats>();
|
|
1647
|
+
let fileCount = 0;
|
|
1648
|
+
let linkCount = 0;
|
|
1649
|
+
let directoryCount = 0;
|
|
1650
|
+
let totalBytes = 0;
|
|
1651
|
+
const captureFile = (
|
|
1652
|
+
relativePath: string,
|
|
1653
|
+
stats: BigIntStats,
|
|
1654
|
+
): void => {
|
|
1655
|
+
const existing = capturedFiles.get(relativePath);
|
|
1656
|
+
if (existing !== undefined) {
|
|
1657
|
+
if (!sameEvaluationSourceSnapshot(existing, stats)) {
|
|
1658
|
+
throw new Error(
|
|
1659
|
+
`provider plugin evaluation package file ${relativePath} changed while it was walked`,
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
const uid =
|
|
1665
|
+
typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
1666
|
+
if (
|
|
1667
|
+
!stats.isFile()
|
|
1668
|
+
|| stats.isSymbolicLink()
|
|
1669
|
+
|| (uid !== undefined && stats.uid !== BigInt(uid))
|
|
1670
|
+
) {
|
|
1671
|
+
throw new Error(
|
|
1672
|
+
`provider plugin evaluation package tree contains unsupported entry ${relativePath}`,
|
|
1673
|
+
);
|
|
1674
|
+
}
|
|
1675
|
+
fileCount += 1;
|
|
1676
|
+
if (
|
|
1677
|
+
fileCount + linkCount
|
|
1678
|
+
> MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_FILES
|
|
1679
|
+
) {
|
|
1680
|
+
throw new Error(
|
|
1681
|
+
"provider plugin evaluation package tree exceeds its file bound",
|
|
1682
|
+
);
|
|
1683
|
+
}
|
|
1684
|
+
if (
|
|
1685
|
+
stats.size < 0n
|
|
1686
|
+
|| stats.size
|
|
1687
|
+
> BigInt(MAX_PROVIDER_PLUGIN_EVALUATION_SOURCE_BYTES)
|
|
1688
|
+
) {
|
|
1689
|
+
throw new Error(
|
|
1690
|
+
`provider plugin evaluation package file ${relativePath} exceeds its byte bound`,
|
|
1691
|
+
);
|
|
1692
|
+
}
|
|
1693
|
+
totalBytes += Number(stats.size);
|
|
1694
|
+
if (totalBytes > MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_BYTES) {
|
|
1695
|
+
throw new Error(
|
|
1696
|
+
"provider plugin evaluation package tree exceeds its total byte bound",
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
capturedFiles.set(relativePath, stats);
|
|
1700
|
+
entries.push(Object.freeze({
|
|
1701
|
+
path: relativePath,
|
|
1702
|
+
kind: "file",
|
|
1703
|
+
stats,
|
|
1704
|
+
}));
|
|
1705
|
+
};
|
|
1706
|
+
const captureGuardDirectory = (relativePath: string): void => {
|
|
1707
|
+
const stats = providerPluginInstalledGuardDirectorySnapshot(
|
|
1708
|
+
resolve(root, relativePath),
|
|
1709
|
+
`provider plugin evaluation package directory ${relativePath}`,
|
|
1710
|
+
);
|
|
1711
|
+
const existing = capturedGuardDirectories.get(relativePath);
|
|
1712
|
+
if (existing !== undefined) {
|
|
1713
|
+
if (!sameEvaluationSourceSnapshot(existing, stats)) {
|
|
1714
|
+
throw new Error(
|
|
1715
|
+
`provider plugin evaluation installed binary directory ${relativePath} changed while it was walked`,
|
|
1716
|
+
);
|
|
1717
|
+
}
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
directoryCount += 1;
|
|
1721
|
+
if (
|
|
1722
|
+
directoryCount
|
|
1723
|
+
> MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_DIRECTORIES
|
|
1724
|
+
) {
|
|
1725
|
+
throw new Error(
|
|
1726
|
+
"provider plugin evaluation package tree exceeds its directory bound",
|
|
1727
|
+
);
|
|
1728
|
+
}
|
|
1729
|
+
capturedGuardDirectories.set(relativePath, stats);
|
|
1730
|
+
entries.push(Object.freeze({
|
|
1731
|
+
path: relativePath,
|
|
1732
|
+
kind: "guard-directory",
|
|
1733
|
+
stats,
|
|
1734
|
+
}));
|
|
1735
|
+
};
|
|
1736
|
+
const captureTargetGuardDirectories = (targetPath: string): void => {
|
|
1737
|
+
const parent = dirname(targetPath).split(sep).join("/");
|
|
1738
|
+
if (parent === ".") return;
|
|
1739
|
+
const segments = parent.split("/");
|
|
1740
|
+
if (segments.length > MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_DEPTH) {
|
|
1741
|
+
throw new Error(
|
|
1742
|
+
"provider plugin evaluation package tree exceeds its depth bound",
|
|
1743
|
+
);
|
|
1744
|
+
}
|
|
1745
|
+
for (let index = 1; index <= segments.length; index += 1) {
|
|
1746
|
+
captureGuardDirectory(segments.slice(0, index).join("/"));
|
|
1747
|
+
}
|
|
1748
|
+
};
|
|
1749
|
+
const visit = (
|
|
1750
|
+
directoryPath: string,
|
|
1751
|
+
relativeDirectory: string,
|
|
1752
|
+
depth: number,
|
|
1753
|
+
): void => {
|
|
1754
|
+
if (depth > MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_DEPTH) {
|
|
1755
|
+
throw new Error("provider plugin evaluation package tree exceeds its depth bound");
|
|
1756
|
+
}
|
|
1757
|
+
let before: BigIntStats;
|
|
1758
|
+
try {
|
|
1759
|
+
before = lstatSync(directoryPath, { bigint: true });
|
|
1760
|
+
} catch {
|
|
1761
|
+
throw new Error(
|
|
1762
|
+
`provider plugin evaluation package directory ${relativeDirectory || "."} changed while it was walked`,
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
const uid =
|
|
1766
|
+
typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
1767
|
+
if (
|
|
1768
|
+
before.isSymbolicLink()
|
|
1769
|
+
|| !before.isDirectory()
|
|
1770
|
+
|| (uid !== undefined && before.uid !== BigInt(uid))
|
|
1771
|
+
|| (before.mode & 0o022n) !== 0n
|
|
1772
|
+
) {
|
|
1773
|
+
throw new Error(
|
|
1774
|
+
`provider plugin evaluation package directory ${relativeDirectory || "."} is unsafe`,
|
|
1775
|
+
);
|
|
1776
|
+
}
|
|
1777
|
+
directoryCount += 1;
|
|
1778
|
+
if (
|
|
1779
|
+
directoryCount
|
|
1780
|
+
> MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_DIRECTORIES
|
|
1781
|
+
) {
|
|
1782
|
+
throw new Error(
|
|
1783
|
+
"provider plugin evaluation package tree exceeds its directory bound",
|
|
1784
|
+
);
|
|
1785
|
+
}
|
|
1786
|
+
entries.push(Object.freeze({
|
|
1787
|
+
path: relativeDirectory === "" ? "." : relativeDirectory,
|
|
1788
|
+
kind: "directory",
|
|
1789
|
+
stats: before,
|
|
1790
|
+
}));
|
|
1791
|
+
const descriptor = openSync(
|
|
1792
|
+
directoryPath,
|
|
1793
|
+
constants.O_RDONLY
|
|
1794
|
+
| ("O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0)
|
|
1795
|
+
| ("O_DIRECTORY" in constants ? constants.O_DIRECTORY : 0),
|
|
1796
|
+
);
|
|
1797
|
+
try {
|
|
1798
|
+
const bound = fstatSync(descriptor, { bigint: true });
|
|
1799
|
+
if (!bound.isDirectory() || !sameEvaluationSourceSnapshot(before, bound)) {
|
|
1800
|
+
throw new Error(
|
|
1801
|
+
`provider plugin evaluation package directory ${relativeDirectory || "."} changed while it was bound`,
|
|
1802
|
+
);
|
|
1803
|
+
}
|
|
1804
|
+
const names: string[] = [];
|
|
1805
|
+
const directory = opendirSync(directoryPath);
|
|
1806
|
+
try {
|
|
1807
|
+
for (;;) {
|
|
1808
|
+
const entry = directory.readSync();
|
|
1809
|
+
if (entry === null) break;
|
|
1810
|
+
names.push(entry.name);
|
|
1811
|
+
if (
|
|
1812
|
+
names.length
|
|
1813
|
+
> MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_FILES
|
|
1814
|
+
+ MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_DIRECTORIES
|
|
1815
|
+
|| entries.length + names.length
|
|
1816
|
+
> MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_FILES
|
|
1817
|
+
+ MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_DIRECTORIES
|
|
1818
|
+
) {
|
|
1819
|
+
throw new Error(
|
|
1820
|
+
"provider plugin evaluation package tree exceeds its entry bound",
|
|
1821
|
+
);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
} finally {
|
|
1825
|
+
directory.closeSync();
|
|
1826
|
+
}
|
|
1827
|
+
names.sort(compareProviderPluginEvaluationText);
|
|
1828
|
+
for (const name of names) {
|
|
1829
|
+
const relativePath = relativeDirectory === ""
|
|
1830
|
+
? name
|
|
1831
|
+
: `${relativeDirectory}/${name}`;
|
|
1832
|
+
if (
|
|
1833
|
+
name === "."
|
|
1834
|
+
|| name === ".."
|
|
1835
|
+
|| name.includes("/")
|
|
1836
|
+
|| relativePath.includes("\\")
|
|
1837
|
+
|| relativePath.includes("\u0000")
|
|
1838
|
+
|| Buffer.byteLength(relativePath, "utf8")
|
|
1839
|
+
> MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_PATH_BYTES
|
|
1840
|
+
) {
|
|
1841
|
+
throw new Error(
|
|
1842
|
+
"provider plugin evaluation package tree contains an unsafe path",
|
|
1843
|
+
);
|
|
1844
|
+
}
|
|
1845
|
+
const path = resolve(directoryPath, name);
|
|
1846
|
+
let stats: BigIntStats;
|
|
1847
|
+
try {
|
|
1848
|
+
stats = lstatSync(path, { bigint: true });
|
|
1849
|
+
} catch {
|
|
1850
|
+
throw new Error(
|
|
1851
|
+
`provider plugin evaluation package entry ${relativePath} changed while it was walked`,
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
if (stats.isSymbolicLink()) {
|
|
1855
|
+
if (!isProviderPluginInstalledBinaryDirectory(relativeDirectory)) {
|
|
1856
|
+
throw new Error(
|
|
1857
|
+
`provider plugin evaluation package tree contains symlink ${relativePath}`,
|
|
1858
|
+
);
|
|
1859
|
+
}
|
|
1860
|
+
linkCount += 1;
|
|
1861
|
+
if (
|
|
1862
|
+
fileCount + linkCount
|
|
1863
|
+
> MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_FILES
|
|
1864
|
+
) {
|
|
1865
|
+
throw new Error(
|
|
1866
|
+
"provider plugin evaluation package tree exceeds its file bound",
|
|
1867
|
+
);
|
|
1868
|
+
}
|
|
1869
|
+
const target = providerPluginCanonicalInstalledBinaryLinkTarget(
|
|
1870
|
+
root,
|
|
1871
|
+
path,
|
|
1872
|
+
stats,
|
|
1873
|
+
MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_PATH_BYTES,
|
|
1874
|
+
`provider plugin evaluation npm binary link ${relativePath}`,
|
|
1875
|
+
);
|
|
1876
|
+
totalBytes += Buffer.byteLength(target.path, "utf8");
|
|
1877
|
+
if (totalBytes > MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_BYTES) {
|
|
1878
|
+
throw new Error(
|
|
1879
|
+
"provider plugin evaluation package tree exceeds its total byte bound",
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
entries.push(Object.freeze({
|
|
1883
|
+
path: relativePath,
|
|
1884
|
+
kind: "link",
|
|
1885
|
+
stats,
|
|
1886
|
+
targetPath: target.path,
|
|
1887
|
+
}));
|
|
1888
|
+
captureTargetGuardDirectories(target.path);
|
|
1889
|
+
captureFile(target.path, target.stats);
|
|
1890
|
+
continue;
|
|
1891
|
+
}
|
|
1892
|
+
if (stats.isDirectory()) {
|
|
1893
|
+
if (isProviderPluginInstalledBinaryDirectory(relativeDirectory)) {
|
|
1894
|
+
throw new Error(
|
|
1895
|
+
`provider plugin evaluation package tree contains unsupported entry ${relativePath}`,
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
// Only installer-owned binary shims and their canonical regular-file
|
|
1899
|
+
// targets contribute from nested node_modules. Statically imported
|
|
1900
|
+
// packages are snapshotted independently below.
|
|
1901
|
+
if (name === "node_modules") {
|
|
1902
|
+
const binaryDirectory = resolve(path, ".bin");
|
|
1903
|
+
const binaryStats = lstatSync(binaryDirectory, {
|
|
1904
|
+
bigint: true,
|
|
1905
|
+
throwIfNoEntry: false,
|
|
1906
|
+
});
|
|
1907
|
+
if (binaryStats !== undefined) {
|
|
1908
|
+
captureGuardDirectory(relativePath);
|
|
1909
|
+
visit(
|
|
1910
|
+
binaryDirectory,
|
|
1911
|
+
`${relativePath}/.bin`,
|
|
1912
|
+
depth + 2,
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
continue;
|
|
1916
|
+
}
|
|
1917
|
+
visit(path, relativePath, depth + 1);
|
|
1918
|
+
continue;
|
|
1919
|
+
}
|
|
1920
|
+
captureFile(relativePath, stats);
|
|
1921
|
+
}
|
|
1922
|
+
const after = fstatSync(descriptor, { bigint: true });
|
|
1923
|
+
let current: BigIntStats;
|
|
1924
|
+
try {
|
|
1925
|
+
current = lstatSync(directoryPath, { bigint: true });
|
|
1926
|
+
} catch {
|
|
1927
|
+
throw new Error(
|
|
1928
|
+
`provider plugin evaluation package directory ${relativeDirectory || "."} changed while it was walked`,
|
|
1929
|
+
);
|
|
1930
|
+
}
|
|
1931
|
+
if (
|
|
1932
|
+
current.isSymbolicLink()
|
|
1933
|
+
|| !current.isDirectory()
|
|
1934
|
+
|| !sameEvaluationSourceSnapshot(before, after)
|
|
1935
|
+
|| !sameEvaluationSourceSnapshot(after, current)
|
|
1936
|
+
) {
|
|
1937
|
+
throw new Error(
|
|
1938
|
+
`provider plugin evaluation package directory ${relativeDirectory || "."} changed while it was walked`,
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
} finally {
|
|
1942
|
+
closeSync(descriptor);
|
|
1943
|
+
}
|
|
1944
|
+
};
|
|
1945
|
+
visit(root, "", 0);
|
|
1946
|
+
entries.sort((left, right) =>
|
|
1947
|
+
compareProviderPluginEvaluationText(left.path, right.path)
|
|
1948
|
+
|| compareProviderPluginEvaluationText(left.kind, right.kind));
|
|
1949
|
+
return Object.freeze({
|
|
1950
|
+
entries: Object.freeze(entries),
|
|
1951
|
+
fileCount,
|
|
1952
|
+
linkCount,
|
|
1953
|
+
directoryCount,
|
|
1954
|
+
totalBytes,
|
|
1955
|
+
});
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
function sameProviderPluginEvaluationPackageTree(
|
|
1959
|
+
left: ProviderPluginEvaluationPackageTreeWalk,
|
|
1960
|
+
right: ProviderPluginEvaluationPackageTreeWalk,
|
|
1961
|
+
): boolean {
|
|
1962
|
+
return left.fileCount === right.fileCount
|
|
1963
|
+
&& left.linkCount === right.linkCount
|
|
1964
|
+
&& left.directoryCount === right.directoryCount
|
|
1965
|
+
&& left.totalBytes === right.totalBytes
|
|
1966
|
+
&& left.entries.length === right.entries.length
|
|
1967
|
+
&& left.entries.every((entry, index) => {
|
|
1968
|
+
const current = right.entries[index];
|
|
1969
|
+
return current !== undefined
|
|
1970
|
+
&& entry.path === current.path
|
|
1971
|
+
&& entry.kind === current.kind
|
|
1972
|
+
&& (
|
|
1973
|
+
entry.kind !== "link"
|
|
1974
|
+
|| (current.kind === "link" && entry.targetPath === current.targetPath)
|
|
1975
|
+
)
|
|
1976
|
+
&& sameEvaluationSourceSnapshot(entry.stats, current.stats);
|
|
1977
|
+
});
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
function updateProviderPluginEvaluationTreeHash(
|
|
1981
|
+
hash: ReturnType<typeof createHash>,
|
|
1982
|
+
label: string,
|
|
1983
|
+
bytes: Buffer,
|
|
1984
|
+
): void {
|
|
1985
|
+
const labelBytes = Buffer.from(label, "utf8");
|
|
1986
|
+
const lengths = Buffer.alloc(12);
|
|
1987
|
+
lengths.writeUInt32BE(labelBytes.byteLength, 0);
|
|
1988
|
+
lengths.writeBigUInt64BE(BigInt(bytes.byteLength), 4);
|
|
1989
|
+
hash.update(lengths).update(labelBytes).update(bytes);
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
/**
|
|
1993
|
+
* Canonical extraction mode for relocation-stable installed package identity.
|
|
1994
|
+
* Exact observed modes stay in stat snapshots so TOCTOU checks remain exact.
|
|
1995
|
+
*/
|
|
1996
|
+
export function providerPluginDurableInstalledPackageMode(
|
|
1997
|
+
kind: "directory" | "file",
|
|
1998
|
+
mode: number,
|
|
1999
|
+
): 0o644 | 0o755 {
|
|
2000
|
+
if (kind === "directory") return 0o755;
|
|
2001
|
+
return (mode & 0o111) === 0 ? 0o644 : 0o755;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
function snapshotProviderPluginEvaluationPackage(
|
|
2005
|
+
root: string,
|
|
2006
|
+
): ProviderPluginEvaluationPackageSnapshot {
|
|
2007
|
+
const firstWalk = walkProviderPluginEvaluationPackageTree(root);
|
|
2008
|
+
const cached = providerPluginEvaluationPackageCache.get(root);
|
|
2009
|
+
if (
|
|
2010
|
+
cached !== undefined
|
|
2011
|
+
&& sameProviderPluginEvaluationPackageTree(
|
|
2012
|
+
cached.verificationWalk,
|
|
2013
|
+
firstWalk,
|
|
2014
|
+
)
|
|
2015
|
+
) return cached;
|
|
2016
|
+
|
|
2017
|
+
const files: {
|
|
2018
|
+
readonly path: string;
|
|
2019
|
+
readonly bytes: Buffer;
|
|
2020
|
+
readonly mode: number;
|
|
2021
|
+
}[] = [];
|
|
2022
|
+
const links: { readonly path: string; readonly targetPath: string }[] = [];
|
|
2023
|
+
const directories: { readonly path: string; readonly mode: number }[] = [];
|
|
2024
|
+
for (const entry of firstWalk.entries) {
|
|
2025
|
+
if (entry.kind === "guard-directory") continue;
|
|
2026
|
+
if (entry.kind === "directory") {
|
|
2027
|
+
directories.push(Object.freeze({
|
|
2028
|
+
path: entry.path,
|
|
2029
|
+
mode: Number(entry.stats.mode & 0o777n),
|
|
2030
|
+
}));
|
|
2031
|
+
continue;
|
|
2032
|
+
}
|
|
2033
|
+
if (entry.kind === "link") {
|
|
2034
|
+
links.push(Object.freeze({
|
|
2035
|
+
path: entry.path,
|
|
2036
|
+
targetPath: entry.targetPath,
|
|
2037
|
+
}));
|
|
2038
|
+
continue;
|
|
2039
|
+
}
|
|
2040
|
+
const path = resolve(root, entry.path);
|
|
2041
|
+
const bytes = readOwnedBoundedProviderPluginEvaluationFile(
|
|
2042
|
+
path,
|
|
2043
|
+
MAX_PROVIDER_PLUGIN_EVALUATION_SOURCE_BYTES,
|
|
2044
|
+
`provider plugin evaluation package file ${entry.path}`,
|
|
2045
|
+
);
|
|
2046
|
+
let current: BigIntStats;
|
|
2047
|
+
try {
|
|
2048
|
+
current = lstatSync(path, { bigint: true });
|
|
2049
|
+
} catch {
|
|
2050
|
+
throw new Error(
|
|
2051
|
+
`provider plugin evaluation package file ${entry.path} changed while it was snapshotted`,
|
|
2052
|
+
);
|
|
2053
|
+
}
|
|
2054
|
+
if (
|
|
2055
|
+
bytes.byteLength !== Number(entry.stats.size)
|
|
2056
|
+
|| current.isSymbolicLink()
|
|
2057
|
+
|| !current.isFile()
|
|
2058
|
+
|| !sameEvaluationSourceSnapshot(entry.stats, current)
|
|
2059
|
+
) {
|
|
2060
|
+
throw new Error(
|
|
2061
|
+
`provider plugin evaluation package file ${entry.path} changed while it was snapshotted`,
|
|
2062
|
+
);
|
|
2063
|
+
}
|
|
2064
|
+
files.push(Object.freeze({
|
|
2065
|
+
path: entry.path,
|
|
2066
|
+
bytes,
|
|
2067
|
+
mode: Number(entry.stats.mode & 0o777n),
|
|
2068
|
+
}));
|
|
2069
|
+
}
|
|
2070
|
+
const filesByPath = new Map(files.map((file) => [file.path, file]));
|
|
2071
|
+
for (const link of links) {
|
|
2072
|
+
if (!filesByPath.has(link.targetPath)) {
|
|
2073
|
+
throw new Error(
|
|
2074
|
+
`provider plugin evaluation npm binary link ${link.path} target is not part of the captured package tree`,
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
const secondWalk = walkProviderPluginEvaluationPackageTree(root);
|
|
2079
|
+
if (!sameProviderPluginEvaluationPackageTree(firstWalk, secondWalk)) {
|
|
2080
|
+
throw new Error(
|
|
2081
|
+
"provider plugin evaluation package tree changed while it was snapshotted",
|
|
2082
|
+
);
|
|
2083
|
+
}
|
|
2084
|
+
const manifest = files.find((file) => file.path === "package.json");
|
|
2085
|
+
let manifestValue: unknown;
|
|
2086
|
+
try {
|
|
2087
|
+
manifestValue = manifest === undefined
|
|
2088
|
+
? undefined
|
|
2089
|
+
: JSON.parse(
|
|
2090
|
+
new TextDecoder("utf-8", { fatal: true }).decode(manifest.bytes),
|
|
2091
|
+
) as unknown;
|
|
2092
|
+
} catch {
|
|
2093
|
+
throw new Error(
|
|
2094
|
+
`provider plugin evaluation package manifest ${root} is invalid`,
|
|
2095
|
+
);
|
|
2096
|
+
}
|
|
2097
|
+
if (
|
|
2098
|
+
typeof manifestValue !== "object"
|
|
2099
|
+
|| manifestValue === null
|
|
2100
|
+
|| Array.isArray(manifestValue)
|
|
2101
|
+
) {
|
|
2102
|
+
throw new Error(
|
|
2103
|
+
`provider plugin evaluation package manifest ${root} has no identity`,
|
|
2104
|
+
);
|
|
2105
|
+
}
|
|
2106
|
+
const packageName = (manifestValue as Record<string, unknown>).name;
|
|
2107
|
+
const packageVersion = (manifestValue as Record<string, unknown>).version;
|
|
2108
|
+
if (typeof packageName !== "string" || typeof packageVersion !== "string") {
|
|
2109
|
+
throw new Error(
|
|
2110
|
+
`provider plugin evaluation package manifest ${root} has no identity`,
|
|
2111
|
+
);
|
|
2112
|
+
}
|
|
2113
|
+
const treeHash = createHash("sha256");
|
|
2114
|
+
treeHash.update("provider-plugin-installed-package-tree@2\0");
|
|
2115
|
+
updateProviderPluginEvaluationTreeHash(
|
|
2116
|
+
treeHash,
|
|
2117
|
+
"identity",
|
|
2118
|
+
Buffer.from(`${packageName}@${packageVersion}`, "utf8"),
|
|
2119
|
+
);
|
|
2120
|
+
for (const directory of directories) {
|
|
2121
|
+
const durableMode = providerPluginDurableInstalledPackageMode(
|
|
2122
|
+
"directory",
|
|
2123
|
+
directory.mode,
|
|
2124
|
+
);
|
|
2125
|
+
updateProviderPluginEvaluationTreeHash(
|
|
2126
|
+
treeHash,
|
|
2127
|
+
`directory/${directory.path}`,
|
|
2128
|
+
Buffer.from(`mode:${durableMode.toString(8)}`, "utf8"),
|
|
2129
|
+
);
|
|
2130
|
+
}
|
|
2131
|
+
for (const file of files) {
|
|
2132
|
+
const durableMode = providerPluginDurableInstalledPackageMode(
|
|
2133
|
+
"file",
|
|
2134
|
+
file.mode,
|
|
2135
|
+
);
|
|
2136
|
+
updateProviderPluginEvaluationTreeHash(
|
|
2137
|
+
treeHash,
|
|
2138
|
+
`file-mode/${file.path}`,
|
|
2139
|
+
Buffer.from(`mode:${durableMode.toString(8)}`, "utf8"),
|
|
2140
|
+
);
|
|
2141
|
+
updateProviderPluginEvaluationTreeHash(
|
|
2142
|
+
treeHash,
|
|
2143
|
+
`file/${file.path}`,
|
|
2144
|
+
file.bytes,
|
|
2145
|
+
);
|
|
2146
|
+
}
|
|
2147
|
+
for (const link of links) {
|
|
2148
|
+
if (!filesByPath.has(link.targetPath)) {
|
|
2149
|
+
throw new Error(
|
|
2150
|
+
`provider plugin evaluation npm binary link ${link.path} target is unavailable`,
|
|
2151
|
+
);
|
|
2152
|
+
}
|
|
2153
|
+
updateProviderPluginEvaluationTreeHash(
|
|
2154
|
+
treeHash,
|
|
2155
|
+
`link/${link.path}`,
|
|
2156
|
+
Buffer.from(link.targetPath, "utf8"),
|
|
2157
|
+
);
|
|
2158
|
+
}
|
|
2159
|
+
const snapshot = Object.freeze({
|
|
2160
|
+
treeSha256: treeHash.digest("hex"),
|
|
2161
|
+
totalBytes: firstWalk.totalBytes,
|
|
2162
|
+
verificationWalk: secondWalk,
|
|
2163
|
+
});
|
|
2164
|
+
if (
|
|
2165
|
+
!providerPluginEvaluationPackageCache.has(root)
|
|
2166
|
+
&& providerPluginEvaluationPackageCache.size
|
|
2167
|
+
>= MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_CACHE_ENTRIES
|
|
2168
|
+
) {
|
|
2169
|
+
providerPluginEvaluationPackageCache.clear();
|
|
2170
|
+
}
|
|
2171
|
+
providerPluginEvaluationPackageCache.set(root, snapshot);
|
|
2172
|
+
return snapshot;
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
function providerPluginEvaluationValueImports(
|
|
2176
|
+
bytes: Buffer,
|
|
2177
|
+
path: string,
|
|
2178
|
+
): readonly { readonly kind: string; readonly path: string }[] {
|
|
2179
|
+
const extension = extname(path);
|
|
2180
|
+
if (extension === ".jsx" || extension === ".tsx") {
|
|
2181
|
+
throw new Error(
|
|
2182
|
+
`provider plugin evaluation module ${path} uses configuration-dependent JSX or TSX`,
|
|
2183
|
+
);
|
|
2184
|
+
}
|
|
2185
|
+
if (!providerPluginEvaluationModuleExtensions.has(extension)) {
|
|
2186
|
+
return Object.freeze([]);
|
|
2187
|
+
}
|
|
2188
|
+
let source: string;
|
|
2189
|
+
try {
|
|
2190
|
+
source = new TextDecoder("utf-8", { fatal: true }).decode(bytes)
|
|
2191
|
+
.replace(/^#![^\r\n]*(?:\r?\n|$)/u, "");
|
|
2192
|
+
} catch {
|
|
2193
|
+
throw new Error(
|
|
2194
|
+
`provider plugin evaluation module ${path} must be valid UTF-8`,
|
|
2195
|
+
);
|
|
2196
|
+
}
|
|
2197
|
+
const scanner =
|
|
2198
|
+
extension === ".ts" || extension === ".mts" || extension === ".cts"
|
|
2199
|
+
? providerPluginEvaluationImportScanners.ts
|
|
2200
|
+
: providerPluginEvaluationImportScanners.js;
|
|
2201
|
+
const imports = Object.freeze([...scanner.scanImports(source)]);
|
|
2202
|
+
if (imports.length > MAX_PROVIDER_PLUGIN_EVALUATION_IMPORTS_PER_MODULE) {
|
|
2203
|
+
throw new Error(
|
|
2204
|
+
`provider plugin evaluation module ${path} has too many static imports`,
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
2207
|
+
return imports;
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
function resolveProviderPluginEvaluationImport(
|
|
2211
|
+
importerPath: string,
|
|
2212
|
+
specifier: string,
|
|
2213
|
+
importKind: string,
|
|
2214
|
+
): string | undefined {
|
|
2215
|
+
if (
|
|
2216
|
+
specifier.startsWith("node:")
|
|
2217
|
+
|| specifier.startsWith("bun:")
|
|
2218
|
+
|| providerPluginEvaluationRuntimeBuiltins.has(specifier)
|
|
2219
|
+
) return undefined;
|
|
2220
|
+
if (/^[A-Za-z][A-Za-z0-9+.-]*:/u.test(specifier)) {
|
|
2221
|
+
throw new Error(
|
|
2222
|
+
`provider plugin evaluation module ${importerPath} imports unsupported absolute or URL dependency ${specifier}`,
|
|
2223
|
+
);
|
|
2224
|
+
}
|
|
2225
|
+
let resolvedPath: string;
|
|
2226
|
+
try {
|
|
2227
|
+
if (isAbsolute(specifier)) {
|
|
2228
|
+
resolvedPath = specifier;
|
|
2229
|
+
} else {
|
|
2230
|
+
resolvedPath = importKind === "require-call"
|
|
2231
|
+
? createRequire(importerPath).resolve(specifier)
|
|
2232
|
+
: Bun.resolveSync(specifier, dirname(importerPath));
|
|
2233
|
+
}
|
|
2234
|
+
} catch (error) {
|
|
2235
|
+
throw new Error(
|
|
2236
|
+
`provider plugin evaluation module ${importerPath} has unresolved static dependency ${specifier}`,
|
|
2237
|
+
{ cause: error },
|
|
2238
|
+
);
|
|
2239
|
+
}
|
|
2240
|
+
if (
|
|
2241
|
+
/^[A-Za-z][A-Za-z0-9+.-]*:/u.test(resolvedPath)
|
|
2242
|
+
|| !isAbsolute(resolvedPath)
|
|
2243
|
+
) {
|
|
2244
|
+
throw new Error(
|
|
2245
|
+
`provider plugin evaluation dependency ${specifier} did not resolve to a local file`,
|
|
2246
|
+
);
|
|
2247
|
+
}
|
|
2248
|
+
let path: string;
|
|
2249
|
+
try {
|
|
2250
|
+
path = realpathSync(resolvedPath);
|
|
2251
|
+
} catch (error) {
|
|
2252
|
+
throw new Error(
|
|
2253
|
+
`provider plugin evaluation dependency ${specifier} resolved to an unreadable path`,
|
|
2254
|
+
{ cause: error },
|
|
2255
|
+
);
|
|
2256
|
+
}
|
|
2257
|
+
classifyProviderPluginPhysicalPath(path);
|
|
2258
|
+
if (path === providerPluginApiBoundaryPath) return undefined;
|
|
2259
|
+
return path;
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
function captureProviderPluginEvaluationIdentity(
|
|
2263
|
+
pluginId: string,
|
|
2264
|
+
sources: readonly ProviderPluginImplementationSourceV1[],
|
|
2265
|
+
): {
|
|
2266
|
+
readonly sourceDigests: ReadonlyMap<string, string>;
|
|
2267
|
+
readonly installedPackageDigests: ReadonlyMap<string, string>;
|
|
2268
|
+
} {
|
|
2269
|
+
const sourceDigests = new Map<string, string>();
|
|
2270
|
+
const sourceBytes = new Map<string, Buffer>();
|
|
2271
|
+
let totalSourceBytes = 0;
|
|
2272
|
+
const captureRepositorySource = (
|
|
2273
|
+
path: string,
|
|
2274
|
+
label: string,
|
|
2275
|
+
): Buffer => {
|
|
2276
|
+
const existing = sourceBytes.get(path);
|
|
2277
|
+
if (existing !== undefined) return existing;
|
|
2278
|
+
if (
|
|
2279
|
+
sourceDigests.size >= MAX_PROVIDER_PLUGIN_EVALUATION_CLOSURE_SOURCES
|
|
2280
|
+
) {
|
|
2281
|
+
throw new Error(
|
|
2282
|
+
`provider plugin ${pluginId} evaluation source closure exceeds its file bound`,
|
|
2283
|
+
);
|
|
2284
|
+
}
|
|
2285
|
+
const bytes = readProviderPluginEvaluationSource(pluginId, path, label);
|
|
2286
|
+
if (
|
|
2287
|
+
totalSourceBytes + bytes.byteLength
|
|
2288
|
+
> MAX_PROVIDER_PLUGIN_EVALUATION_CLOSURE_BYTES
|
|
2289
|
+
) {
|
|
2290
|
+
throw new Error(
|
|
2291
|
+
`provider plugin ${pluginId} evaluation source closure exceeds its byte bound`,
|
|
2292
|
+
);
|
|
2293
|
+
}
|
|
2294
|
+
totalSourceBytes += bytes.byteLength;
|
|
2295
|
+
sourceBytes.set(path, bytes);
|
|
2296
|
+
sourceDigests.set(
|
|
2297
|
+
path,
|
|
2298
|
+
createHash("sha256").update(bytes).digest("hex"),
|
|
2299
|
+
);
|
|
2300
|
+
return bytes;
|
|
2301
|
+
};
|
|
2302
|
+
for (const source of sources) {
|
|
2303
|
+
captureRepositorySource(source.path, source.label);
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
const pluginEntry = sources.find((source) => source.label === "plugin.ts");
|
|
2307
|
+
if (pluginEntry === undefined) {
|
|
2308
|
+
throw new Error(
|
|
2309
|
+
`provider plugin ${pluginId} must bind its plugin.ts implementation source`,
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
2312
|
+
const pending = [pluginEntry.path];
|
|
2313
|
+
const analyzedModules = new Set<string>();
|
|
2314
|
+
const installedPackageDigests = new Map<string, string>();
|
|
2315
|
+
let installedPackageBytes = 0;
|
|
2316
|
+
while (pending.length > 0) {
|
|
2317
|
+
pending.sort(compareProviderPluginEvaluationText);
|
|
2318
|
+
const path = pending.pop();
|
|
2319
|
+
if (path === undefined || analyzedModules.has(path)) continue;
|
|
2320
|
+
analyzedModules.add(path);
|
|
2321
|
+
|
|
2322
|
+
let bytes: Buffer;
|
|
2323
|
+
const physicalPath = classifyProviderPluginPhysicalPath(path);
|
|
2324
|
+
if (physicalPath.kind === "installed-package") {
|
|
2325
|
+
const root = physicalPath.root;
|
|
2326
|
+
if (!installedPackageDigests.has(root)) {
|
|
2327
|
+
if (
|
|
2328
|
+
installedPackageDigests.size
|
|
2329
|
+
>= MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGES
|
|
2330
|
+
) {
|
|
2331
|
+
throw new Error(
|
|
2332
|
+
`provider plugin ${pluginId} evaluation package closure exceeds its package bound`,
|
|
2333
|
+
);
|
|
2334
|
+
}
|
|
2335
|
+
const snapshot = snapshotProviderPluginEvaluationPackage(root);
|
|
2336
|
+
if (
|
|
2337
|
+
installedPackageBytes + snapshot.totalBytes
|
|
2338
|
+
> MAX_PROVIDER_PLUGIN_EVALUATION_PACKAGE_BYTES
|
|
2339
|
+
) {
|
|
2340
|
+
throw new Error(
|
|
2341
|
+
`provider plugin ${pluginId} evaluation package closure exceeds its byte bound`,
|
|
2342
|
+
);
|
|
2343
|
+
}
|
|
2344
|
+
installedPackageBytes += snapshot.totalBytes;
|
|
2345
|
+
installedPackageDigests.set(root, snapshot.treeSha256);
|
|
2346
|
+
}
|
|
2347
|
+
bytes = readOwnedBoundedProviderPluginEvaluationFile(
|
|
2348
|
+
path,
|
|
2349
|
+
MAX_PROVIDER_PLUGIN_EVALUATION_SOURCE_BYTES,
|
|
2350
|
+
`provider plugin ${pluginId} evaluation package module ${relative(providerPluginRepositoryRoot, path)}`,
|
|
2351
|
+
);
|
|
2352
|
+
} else {
|
|
2353
|
+
bytes = captureRepositorySource(
|
|
2354
|
+
path,
|
|
2355
|
+
relative(providerPluginRepositoryRoot, path).split(sep).join("/"),
|
|
2356
|
+
);
|
|
2357
|
+
}
|
|
2358
|
+
for (const dependency of providerPluginEvaluationValueImports(bytes, path)) {
|
|
2359
|
+
// Literal dynamic imports can execute at module top level before the
|
|
2360
|
+
// definition is frozen. Bind them here even when they also sit behind a
|
|
2361
|
+
// branded lazy loader; the later runtime check is intentionally a
|
|
2362
|
+
// second guard, not a substitute for evaluation-time provenance.
|
|
2363
|
+
const child = resolveProviderPluginEvaluationImport(
|
|
2364
|
+
path,
|
|
2365
|
+
dependency.path,
|
|
2366
|
+
dependency.kind,
|
|
2367
|
+
);
|
|
2368
|
+
if (child !== undefined && !analyzedModules.has(child)) {
|
|
2369
|
+
pending.push(child);
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
return Object.freeze({
|
|
2374
|
+
sourceDigests: new Map(
|
|
2375
|
+
[...sourceDigests.entries()].sort(([left], [right]) =>
|
|
2376
|
+
compareProviderPluginEvaluationText(left, right)),
|
|
2377
|
+
),
|
|
2378
|
+
installedPackageDigests: new Map(
|
|
2379
|
+
[...installedPackageDigests.entries()].sort(([left], [right]) =>
|
|
2380
|
+
compareProviderPluginEvaluationText(left, right)),
|
|
2381
|
+
),
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
/** Internal registry guard against forged post-validation projections. */
|
|
2386
|
+
export function isValidatedProviderPlugin(
|
|
2387
|
+
value: ProviderPluginDefinitionV1 | ProviderPluginV1,
|
|
2388
|
+
): value is ProviderPluginV1 {
|
|
2389
|
+
return validatedProviderPlugins.has(value);
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
/**
|
|
2393
|
+
* Registry-only evidence captured when a trusted source definition becomes an
|
|
2394
|
+
* executable object. It detects ordinary drift in every declared source and
|
|
2395
|
+
* the recursively imported repository descriptor closure before the registry's
|
|
2396
|
+
* later coherent snapshot. It cannot turn trusted in-process code into a
|
|
2397
|
+
* same-account hostile-writer sandbox or prove what Bun loaded during a race.
|
|
2398
|
+
*/
|
|
2399
|
+
export function providerPluginEvaluationSourceSha256(
|
|
2400
|
+
plugin: ProviderPluginV1,
|
|
2401
|
+
): ReadonlyMap<string, string> | undefined {
|
|
2402
|
+
return providerPluginEvaluationSourceDigests.get(plugin);
|
|
2403
|
+
}
|
|
2404
|
+
|
|
2405
|
+
/**
|
|
2406
|
+
* Registry-only exact package-tree identities for every statically
|
|
2407
|
+
* discoverable value import, including literal dynamic imports that could run
|
|
2408
|
+
* at module top level before the definition is frozen.
|
|
2409
|
+
*/
|
|
2410
|
+
export function providerPluginEvaluationInstalledPackageSha256(
|
|
2411
|
+
plugin: ProviderPluginV1,
|
|
2412
|
+
): ReadonlyMap<string, string> | undefined {
|
|
2413
|
+
return providerPluginEvaluationInstalledPackageDigests.get(plugin);
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
/** Kernel-only immutable artifact identity for a validated portable projection. */
|
|
2417
|
+
export function portableProviderPluginArtifactSha256(
|
|
2418
|
+
plugin: ProviderPluginV1,
|
|
2419
|
+
): string | null {
|
|
2420
|
+
return portableProviderPluginArtifacts.get(plugin) ?? null;
|
|
2421
|
+
}
|
|
2422
|
+
|
|
2423
|
+
/** Kernel-only virtual adapter owned by one validated portable binding. */
|
|
2424
|
+
export function portableProviderPluginAdapter(
|
|
2425
|
+
binding: ProviderPluginBindingV1,
|
|
2426
|
+
): { readonly adapterId: string; readonly manifest: GhostgetManifest } | null {
|
|
2427
|
+
return portableProviderPluginAdapters.get(binding) ?? null;
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
/** Kernel-only exact portable identity for one validated operation route. */
|
|
2431
|
+
export function portableProviderPluginOperationIdentity(
|
|
2432
|
+
binding: ProviderPluginBindingV1,
|
|
2433
|
+
operation: ProviderPluginOperationV1,
|
|
2434
|
+
contractVersion: number,
|
|
2435
|
+
): PortableOperationIdentityV1 | null {
|
|
2436
|
+
return portableProviderPluginOperationIdentities.get(binding)?.get(
|
|
2437
|
+
`${operation.name}@${contractVersion}`,
|
|
2438
|
+
) ?? null;
|
|
2439
|
+
}
|
|
2440
|
+
|
|
2441
|
+
/** Exact immutable package operation that implements a portable subject probe. */
|
|
2442
|
+
export function portableProviderPluginSubjectProbeIdentity(
|
|
2443
|
+
binding: ProviderPluginBindingV1,
|
|
2444
|
+
): PortableOperationIdentityV1 | null {
|
|
2445
|
+
return portableProviderPluginSubjectProbeIdentities.get(binding) ?? null;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
function requireExactKeys(
|
|
2449
|
+
value: object,
|
|
2450
|
+
allowed: readonly string[],
|
|
2451
|
+
label: string,
|
|
2452
|
+
): void {
|
|
2453
|
+
const allowedKeys = new Set(allowed);
|
|
2454
|
+
const unexpected = Object.keys(value).filter((key) => !allowedKeys.has(key)).sort();
|
|
2455
|
+
if (unexpected.length > 0) {
|
|
2456
|
+
throw new Error(`${label} contains unsupported keys: ${unexpected.join(", ")}`);
|
|
2457
|
+
}
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
function snapshotExactEnumerableDataProperties(
|
|
2461
|
+
value: unknown,
|
|
2462
|
+
keys: readonly string[],
|
|
2463
|
+
label: string,
|
|
2464
|
+
): readonly unknown[] {
|
|
2465
|
+
if (
|
|
2466
|
+
typeof value !== "object"
|
|
2467
|
+
|| value === null
|
|
2468
|
+
|| Array.isArray(value)
|
|
2469
|
+
) {
|
|
2470
|
+
throw new Error(`${label} must be a plain data object`);
|
|
2471
|
+
}
|
|
2472
|
+
const prototype = Object.getPrototypeOf(value) as unknown;
|
|
2473
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
2474
|
+
throw new Error(`${label} must be a plain data object`);
|
|
2475
|
+
}
|
|
2476
|
+
const expectedKeys = new Set<PropertyKey>(keys);
|
|
2477
|
+
const actualKeys = Reflect.ownKeys(value);
|
|
2478
|
+
if (
|
|
2479
|
+
actualKeys.length !== expectedKeys.size
|
|
2480
|
+
|| actualKeys.some((key) => !expectedKeys.has(key))
|
|
2481
|
+
) {
|
|
2482
|
+
throw new Error(`${label} must contain exactly ${keys.join(", ")}`);
|
|
2483
|
+
}
|
|
2484
|
+
return Object.freeze(keys.map((key) =>
|
|
2485
|
+
ownDataProperty(value, key, `${label}.${key}`)));
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2488
|
+
export function parseProviderPluginReconciliationContextV1(
|
|
2489
|
+
value: unknown,
|
|
2490
|
+
): ProviderPluginReconciliationContextV1 {
|
|
2491
|
+
const [schemaVersion, kind, dispatchValue, targetValue] =
|
|
2492
|
+
snapshotExactEnumerableDataProperties(
|
|
2493
|
+
value,
|
|
2494
|
+
["schemaVersion", "kind", "dispatch", "target"],
|
|
2495
|
+
"provider plugin reconciliation context",
|
|
2496
|
+
);
|
|
2497
|
+
if (
|
|
2498
|
+
schemaVersion !== 1
|
|
2499
|
+
|| (
|
|
2500
|
+
kind !== "provider-accepted-target-presence"
|
|
2501
|
+
&& kind !== "provider-bound-target-desired-state"
|
|
2502
|
+
)
|
|
2503
|
+
) {
|
|
2504
|
+
throw new Error("provider plugin reconciliation context is malformed");
|
|
2505
|
+
}
|
|
2506
|
+
const [dispatchId, dispatchIndex, dispatchPlanned] =
|
|
2507
|
+
snapshotExactEnumerableDataProperties(
|
|
2508
|
+
dispatchValue,
|
|
2509
|
+
["id", "index", "planned"],
|
|
2510
|
+
"provider plugin reconciliation context dispatch",
|
|
2511
|
+
);
|
|
2512
|
+
if (
|
|
2513
|
+
typeof dispatchId !== "string"
|
|
2514
|
+
|| dispatchId.length > 256
|
|
2515
|
+
|| !providerPluginDispatchIdPattern.test(dispatchId)
|
|
2516
|
+
|| !Number.isSafeInteger(dispatchIndex)
|
|
2517
|
+
|| !Number.isSafeInteger(dispatchPlanned)
|
|
2518
|
+
|| (dispatchIndex as number) < 1
|
|
2519
|
+
|| (dispatchPlanned as number) !== 1
|
|
2520
|
+
|| dispatchIndex !== dispatchPlanned
|
|
2521
|
+
) {
|
|
2522
|
+
throw new Error(
|
|
2523
|
+
"provider plugin reconciliation context dispatch is malformed",
|
|
2524
|
+
);
|
|
2525
|
+
}
|
|
2526
|
+
const [targetSchemaVersion, targetIdentifier] =
|
|
2527
|
+
snapshotExactEnumerableDataProperties(
|
|
2528
|
+
targetValue,
|
|
2529
|
+
["schemaVersion", "identifier"],
|
|
2530
|
+
"provider plugin reconciliation context target",
|
|
2531
|
+
);
|
|
2532
|
+
if (
|
|
2533
|
+
targetSchemaVersion !== 1
|
|
2534
|
+
|| typeof targetIdentifier !== "string"
|
|
2535
|
+
|| targetIdentifier.length < 1
|
|
2536
|
+
|| Buffer.byteLength(targetIdentifier, "utf8") > 8 * 1024
|
|
2537
|
+
|| hasUnpairedSurrogate(targetIdentifier)
|
|
2538
|
+
|| [...targetIdentifier].some((character) => {
|
|
2539
|
+
const codePoint = character.codePointAt(0);
|
|
2540
|
+
return codePoint === undefined
|
|
2541
|
+
|| codePoint === 0x7f
|
|
2542
|
+
|| codePoint < 0x20;
|
|
2543
|
+
})
|
|
2544
|
+
) {
|
|
2545
|
+
throw new Error(
|
|
2546
|
+
"provider plugin reconciliation context target is malformed",
|
|
2547
|
+
);
|
|
2548
|
+
}
|
|
2549
|
+
return Object.freeze({
|
|
2550
|
+
schemaVersion: 1,
|
|
2551
|
+
kind,
|
|
2552
|
+
dispatch: Object.freeze({
|
|
2553
|
+
id: dispatchId,
|
|
2554
|
+
index: dispatchIndex as number,
|
|
2555
|
+
planned: dispatchPlanned as number,
|
|
2556
|
+
}),
|
|
2557
|
+
target: Object.freeze({
|
|
2558
|
+
schemaVersion: 1,
|
|
2559
|
+
identifier: targetIdentifier,
|
|
2560
|
+
}),
|
|
2561
|
+
});
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
function snapshotBoundedDenseArray(
|
|
2565
|
+
value: unknown,
|
|
2566
|
+
label: string,
|
|
2567
|
+
maximumItems: number,
|
|
2568
|
+
): readonly unknown[] {
|
|
2569
|
+
if (
|
|
2570
|
+
!Array.isArray(value)
|
|
2571
|
+
|| (Object.getPrototypeOf(value) as unknown) !== Array.prototype
|
|
2572
|
+
) {
|
|
2573
|
+
throw new Error(`${label} must be a bounded dense array`);
|
|
2574
|
+
}
|
|
2575
|
+
const lengthDescriptor = Object.getOwnPropertyDescriptor(value, "length");
|
|
2576
|
+
const rawLength: unknown = lengthDescriptor !== undefined
|
|
2577
|
+
&& "value" in lengthDescriptor
|
|
2578
|
+
? lengthDescriptor.value as unknown
|
|
2579
|
+
: Number.NaN;
|
|
2580
|
+
if (
|
|
2581
|
+
typeof rawLength !== "number"
|
|
2582
|
+
|| !Number.isSafeInteger(rawLength)
|
|
2583
|
+
|| rawLength < 1
|
|
2584
|
+
|| rawLength > maximumItems
|
|
2585
|
+
) {
|
|
2586
|
+
throw new Error(
|
|
2587
|
+
`${label} must contain between 1 and ${maximumItems} items`,
|
|
2588
|
+
);
|
|
2589
|
+
}
|
|
2590
|
+
const expectedKeys = new Set<PropertyKey>([
|
|
2591
|
+
"length",
|
|
2592
|
+
...Array.from({ length: rawLength }, (_unused, index) => String(index)),
|
|
2593
|
+
]);
|
|
2594
|
+
const actualKeys = Reflect.ownKeys(value);
|
|
2595
|
+
if (
|
|
2596
|
+
actualKeys.length !== expectedKeys.size
|
|
2597
|
+
|| actualKeys.some((key) => !expectedKeys.has(key))
|
|
2598
|
+
) {
|
|
2599
|
+
throw new Error(`${label} must be a bounded dense array`);
|
|
2600
|
+
}
|
|
2601
|
+
return Object.freeze(Array.from(
|
|
2602
|
+
{ length: rawLength },
|
|
2603
|
+
(_unused, index) =>
|
|
2604
|
+
ownDataProperty(value, String(index), `${label}[${index}]`),
|
|
2605
|
+
));
|
|
2606
|
+
}
|
|
2607
|
+
|
|
2608
|
+
function requireNonEmptyArray(value: unknown, label: string): void {
|
|
2609
|
+
if (!Array.isArray(value) || value.length < 1) {
|
|
2610
|
+
throw new Error(`${label} must be a non-empty array`);
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
function requireBoundedNonEmptyArray(
|
|
2615
|
+
value: unknown,
|
|
2616
|
+
label: string,
|
|
2617
|
+
maximumItems: number,
|
|
2618
|
+
): void {
|
|
2619
|
+
requireNonEmptyArray(value, label);
|
|
2620
|
+
if ((value as readonly unknown[]).length > maximumItems) {
|
|
2621
|
+
throw new Error(`${label} may contain at most ${maximumItems} items`);
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
function freezeStringList(
|
|
2626
|
+
values: readonly string[],
|
|
2627
|
+
label: string,
|
|
2628
|
+
maximumItems: number,
|
|
2629
|
+
allowEmpty = false,
|
|
2630
|
+
): readonly string[] {
|
|
2631
|
+
if (
|
|
2632
|
+
!Array.isArray(values)
|
|
2633
|
+
|| (!allowEmpty && values.length < 1)
|
|
2634
|
+
|| values.length > maximumItems
|
|
2635
|
+
) {
|
|
2636
|
+
throw new Error(
|
|
2637
|
+
`${label} must contain ${allowEmpty ? "at most" : "between 1 and"} ${maximumItems} strings`,
|
|
2638
|
+
);
|
|
2639
|
+
}
|
|
2640
|
+
const result: string[] = [];
|
|
2641
|
+
for (const value of values) {
|
|
2642
|
+
if (typeof value !== "string" || value.length < 1) {
|
|
2643
|
+
throw new Error(`${label} must contain non-empty strings`);
|
|
2644
|
+
}
|
|
2645
|
+
result.push(value);
|
|
2646
|
+
}
|
|
2647
|
+
if (new Set(result).size !== result.length) throw new Error(`${label} must not contain duplicates`);
|
|
2648
|
+
return Object.freeze(result);
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
function freezeInputField(
|
|
2652
|
+
field: ScalarInputField | FileInputField,
|
|
2653
|
+
label: string,
|
|
2654
|
+
maximumArrayItems: 25 | 100,
|
|
2655
|
+
nested?: boolean,
|
|
2656
|
+
): ScalarInputField | FileInputField;
|
|
2657
|
+
function freezeInputField(
|
|
2658
|
+
field: ArrayInputField,
|
|
2659
|
+
label: string,
|
|
2660
|
+
maximumArrayItems: 25 | 100,
|
|
2661
|
+
nested?: boolean,
|
|
2662
|
+
): ArrayInputField;
|
|
2663
|
+
function freezeInputField(
|
|
2664
|
+
field: InputField,
|
|
2665
|
+
label: string,
|
|
2666
|
+
maximumArrayItems: 25 | 100,
|
|
2667
|
+
nested?: boolean,
|
|
2668
|
+
): InputField;
|
|
2669
|
+
function freezeInputField(
|
|
2670
|
+
field: InputField,
|
|
2671
|
+
label: string,
|
|
2672
|
+
maximumArrayItems: 25 | 100,
|
|
2673
|
+
nested = false,
|
|
2674
|
+
): InputField {
|
|
2675
|
+
if (
|
|
2676
|
+
typeof field !== "object"
|
|
2677
|
+
|| field === null
|
|
2678
|
+
|| typeof field.description !== "string"
|
|
2679
|
+
|| field.description.length < 1
|
|
2680
|
+
|| field.description.length > 500
|
|
2681
|
+
) {
|
|
2682
|
+
throw new Error(`${label} must declare a bounded description`);
|
|
2683
|
+
}
|
|
2684
|
+
if (field.type === "file") {
|
|
2685
|
+
requireExactKeys(field, ["type", "description", "maxBytes", "mediaTypes"], label);
|
|
2686
|
+
if (
|
|
2687
|
+
!Number.isSafeInteger(field.maxBytes)
|
|
2688
|
+
|| field.maxBytes < 1
|
|
2689
|
+
|| field.maxBytes > 1024 * 1024 * 1024
|
|
2690
|
+
) {
|
|
2691
|
+
throw new Error(`${label}.maxBytes must be a bounded positive integer`);
|
|
2692
|
+
}
|
|
2693
|
+
if (
|
|
2694
|
+
field.mediaTypes !== undefined
|
|
2695
|
+
&& (
|
|
2696
|
+
field.mediaTypes.length < 1
|
|
2697
|
+
|| field.mediaTypes.length > 32
|
|
2698
|
+
|| field.mediaTypes.some((value) =>
|
|
2699
|
+
!/^[a-z0-9!#$&^_.+-]+\/(?:[a-z0-9!#$&^_.+-]+|\*)$/u.test(value))
|
|
2700
|
+
)
|
|
2701
|
+
) {
|
|
2702
|
+
throw new Error(`${label}.mediaTypes must contain bounded media types`);
|
|
2703
|
+
}
|
|
2704
|
+
return Object.freeze({
|
|
2705
|
+
...field,
|
|
2706
|
+
...(field.mediaTypes === undefined
|
|
2707
|
+
? {}
|
|
2708
|
+
: {
|
|
2709
|
+
mediaTypes: freezeStringList(
|
|
2710
|
+
field.mediaTypes,
|
|
2711
|
+
`${label}.mediaTypes`,
|
|
2712
|
+
32,
|
|
2713
|
+
),
|
|
2714
|
+
}),
|
|
2715
|
+
});
|
|
2716
|
+
}
|
|
2717
|
+
if (field.type === "array") {
|
|
2718
|
+
requireExactKeys(field, ["type", "description", "items", "minItems", "maxItems"], label);
|
|
2719
|
+
if (
|
|
2720
|
+
!Number.isSafeInteger(field.minItems)
|
|
2721
|
+
|| !Number.isSafeInteger(field.maxItems)
|
|
2722
|
+
|| nested
|
|
2723
|
+
|| field.minItems < 0
|
|
2724
|
+
|| field.maxItems < field.minItems
|
|
2725
|
+
|| field.maxItems < 1
|
|
2726
|
+
|| field.maxItems > maximumArrayItems
|
|
2727
|
+
) {
|
|
2728
|
+
throw new Error(`${label} must declare valid bounded item counts`);
|
|
2729
|
+
}
|
|
2730
|
+
return Object.freeze({
|
|
2731
|
+
...field,
|
|
2732
|
+
items: freezeInputField(
|
|
2733
|
+
field.items,
|
|
2734
|
+
`${label}.items`,
|
|
2735
|
+
maximumArrayItems,
|
|
2736
|
+
true,
|
|
2737
|
+
),
|
|
2738
|
+
});
|
|
2739
|
+
}
|
|
2740
|
+
requireExactKeys(
|
|
2741
|
+
field,
|
|
2742
|
+
[
|
|
2743
|
+
"type",
|
|
2744
|
+
"description",
|
|
2745
|
+
"minLength",
|
|
2746
|
+
"maxLength",
|
|
2747
|
+
"minimum",
|
|
2748
|
+
"maximum",
|
|
2749
|
+
"enum",
|
|
2750
|
+
"format",
|
|
2751
|
+
"urlPathPrefixes",
|
|
2752
|
+
],
|
|
2753
|
+
label,
|
|
2754
|
+
);
|
|
2755
|
+
if (
|
|
2756
|
+
field.type !== "string"
|
|
2757
|
+
&& field.type !== "number"
|
|
2758
|
+
&& field.type !== "boolean"
|
|
2759
|
+
) {
|
|
2760
|
+
throw new Error(`${label}.type is unsupported`);
|
|
2761
|
+
}
|
|
2762
|
+
const lengthBounds = field.minLength !== undefined
|
|
2763
|
+
|| field.maxLength !== undefined;
|
|
2764
|
+
if (
|
|
2765
|
+
lengthBounds
|
|
2766
|
+
&& (
|
|
2767
|
+
field.type !== "string"
|
|
2768
|
+
|| (
|
|
2769
|
+
field.minLength !== undefined
|
|
2770
|
+
&& (
|
|
2771
|
+
!Number.isSafeInteger(field.minLength)
|
|
2772
|
+
|| field.minLength < 0
|
|
2773
|
+
|| field.minLength > 1_000_000
|
|
2774
|
+
)
|
|
2775
|
+
)
|
|
2776
|
+
|| (
|
|
2777
|
+
field.maxLength !== undefined
|
|
2778
|
+
&& (
|
|
2779
|
+
!Number.isSafeInteger(field.maxLength)
|
|
2780
|
+
|| field.maxLength < 1
|
|
2781
|
+
|| field.maxLength > 1_000_000
|
|
2782
|
+
)
|
|
2783
|
+
)
|
|
2784
|
+
|| (
|
|
2785
|
+
field.minLength !== undefined
|
|
2786
|
+
&& field.maxLength !== undefined
|
|
2787
|
+
&& field.minLength > field.maxLength
|
|
2788
|
+
)
|
|
2789
|
+
)
|
|
2790
|
+
) {
|
|
2791
|
+
throw new Error(`${label} has invalid string length bounds`);
|
|
2792
|
+
}
|
|
2793
|
+
const numericBounds = field.minimum !== undefined
|
|
2794
|
+
|| field.maximum !== undefined;
|
|
2795
|
+
if (
|
|
2796
|
+
numericBounds
|
|
2797
|
+
&& (
|
|
2798
|
+
field.type !== "number"
|
|
2799
|
+
|| (field.minimum !== undefined && !Number.isFinite(field.minimum))
|
|
2800
|
+
|| (field.maximum !== undefined && !Number.isFinite(field.maximum))
|
|
2801
|
+
|| (
|
|
2802
|
+
field.minimum !== undefined
|
|
2803
|
+
&& field.maximum !== undefined
|
|
2804
|
+
&& field.minimum > field.maximum
|
|
2805
|
+
)
|
|
2806
|
+
)
|
|
2807
|
+
) {
|
|
2808
|
+
throw new Error(`${label} has invalid numeric bounds`);
|
|
2809
|
+
}
|
|
2810
|
+
if (
|
|
2811
|
+
field.format !== undefined
|
|
2812
|
+
&& (
|
|
2813
|
+
field.type !== "string"
|
|
2814
|
+
|| (field.format !== "url" && field.format !== "path-segment")
|
|
2815
|
+
)
|
|
2816
|
+
) {
|
|
2817
|
+
throw new Error(`${label}.format is invalid`);
|
|
2818
|
+
}
|
|
2819
|
+
if (
|
|
2820
|
+
field.urlPathPrefixes !== undefined
|
|
2821
|
+
&& (
|
|
2822
|
+
field.type !== "string"
|
|
2823
|
+
|| field.format !== "url"
|
|
2824
|
+
|| field.urlPathPrefixes.length < 1
|
|
2825
|
+
|| field.urlPathPrefixes.length > 20
|
|
2826
|
+
|| field.urlPathPrefixes.some((prefix) =>
|
|
2827
|
+
!prefix.startsWith("/")
|
|
2828
|
+
|| prefix.startsWith("//")
|
|
2829
|
+
|| prefix.length > 2_048
|
|
2830
|
+
|| prefix.includes(String.fromCharCode(0)))
|
|
2831
|
+
|| field.urlPathPrefixes.some((prefix) =>
|
|
2832
|
+
prefix.includes("\\")
|
|
2833
|
+
|| prefix.includes("?")
|
|
2834
|
+
|| prefix.includes("#")
|
|
2835
|
+
|| /%(?:25|2e|2f|5c)/iu.test(prefix)
|
|
2836
|
+
|| prefix.split("/").some((segment) =>
|
|
2837
|
+
segment === "." || segment === ".."))
|
|
2838
|
+
)
|
|
2839
|
+
) {
|
|
2840
|
+
throw new Error(`${label}.urlPathPrefixes is invalid`);
|
|
2841
|
+
}
|
|
2842
|
+
if (field.enum !== undefined) {
|
|
2843
|
+
if (
|
|
2844
|
+
field.enum.length < 1
|
|
2845
|
+
|| field.enum.length > 100
|
|
2846
|
+
|| field.enum.some((value) =>
|
|
2847
|
+
typeof value !== field.type
|
|
2848
|
+
|| (typeof value === "number" && !Number.isFinite(value)))
|
|
2849
|
+
|| new Set(field.enum.map((value) => JSON.stringify(value))).size
|
|
2850
|
+
!== field.enum.length
|
|
2851
|
+
) {
|
|
2852
|
+
throw new Error(`${label}.enum is invalid`);
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
return Object.freeze({
|
|
2856
|
+
...field,
|
|
2857
|
+
...(field.enum === undefined ? {} : { enum: Object.freeze([...field.enum]) }),
|
|
2858
|
+
...(field.urlPathPrefixes === undefined
|
|
2859
|
+
? {}
|
|
2860
|
+
: {
|
|
2861
|
+
urlPathPrefixes: freezeStringList(
|
|
2862
|
+
field.urlPathPrefixes,
|
|
2863
|
+
`${label}.urlPathPrefixes`,
|
|
2864
|
+
20,
|
|
2865
|
+
),
|
|
2866
|
+
}),
|
|
2867
|
+
});
|
|
2868
|
+
}
|
|
2869
|
+
|
|
2870
|
+
function freezeInputSchema(
|
|
2871
|
+
schema: InputSchema,
|
|
2872
|
+
label: string,
|
|
2873
|
+
maximumArrayItems: 25 | 100,
|
|
2874
|
+
): InputSchema {
|
|
2875
|
+
requireExactKeys(schema, ["properties", "required"], label);
|
|
2876
|
+
if (
|
|
2877
|
+
typeof schema.properties !== "object"
|
|
2878
|
+
|| schema.properties === null
|
|
2879
|
+
|| Array.isArray(schema.properties)
|
|
2880
|
+
) {
|
|
2881
|
+
throw new Error(`${label}.properties must be an object`);
|
|
2882
|
+
}
|
|
2883
|
+
const propertyEntries: [string, InputField][] = [];
|
|
2884
|
+
for (const name in schema.properties) {
|
|
2885
|
+
if (!Object.hasOwn(schema.properties, name)) continue;
|
|
2886
|
+
if (propertyEntries.length === 100) {
|
|
2887
|
+
throw new Error(`${label}.properties may contain at most 100 fields`);
|
|
2888
|
+
}
|
|
2889
|
+
propertyEntries.push([name, schema.properties[name]!]);
|
|
2890
|
+
}
|
|
2891
|
+
propertyEntries.sort(([left], [right]) => left.localeCompare(right));
|
|
2892
|
+
const properties: Record<string, InputField> = {};
|
|
2893
|
+
for (const [name, field] of propertyEntries) {
|
|
2894
|
+
if (!/^[a-z][a-z0-9_]{0,63}$/u.test(name)) {
|
|
2895
|
+
throw new Error(`${label}.properties contains invalid field ${name}`);
|
|
2896
|
+
}
|
|
2897
|
+
properties[name] = freezeInputField(
|
|
2898
|
+
field,
|
|
2899
|
+
`${label}.properties.${name}`,
|
|
2900
|
+
maximumArrayItems,
|
|
2901
|
+
);
|
|
2902
|
+
}
|
|
2903
|
+
const required = freezeStringList(
|
|
2904
|
+
schema.required,
|
|
2905
|
+
`${label}.required`,
|
|
2906
|
+
100,
|
|
2907
|
+
true,
|
|
2908
|
+
);
|
|
2909
|
+
for (const name of required) {
|
|
2910
|
+
if (properties[name] === undefined) throw new Error(`${label}.required references ${name}`);
|
|
2911
|
+
}
|
|
2912
|
+
return Object.freeze({
|
|
2913
|
+
properties: Object.freeze(properties),
|
|
2914
|
+
required,
|
|
2915
|
+
});
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
function containsFileInput(field: InputField): boolean {
|
|
2919
|
+
return field.type === "file"
|
|
2920
|
+
|| (field.type === "array" && field.items.type === "file");
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
function hasControlCharacters(value: string): boolean {
|
|
2924
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
2925
|
+
const code = value.charCodeAt(index);
|
|
2926
|
+
if (code <= 0x1f || code === 0x7f) return true;
|
|
2927
|
+
}
|
|
2928
|
+
return false;
|
|
2929
|
+
}
|
|
2930
|
+
|
|
2931
|
+
function freezeOperation(
|
|
2932
|
+
operation: ProviderPluginOperationDefinitionV1,
|
|
2933
|
+
transport: ProviderPluginTransport,
|
|
2934
|
+
sourceKind: ProviderPluginV1["sourceKind"],
|
|
2935
|
+
): ProviderPluginOperationV1 {
|
|
2936
|
+
const official = transport === "provider-api";
|
|
2937
|
+
requireExactKeys(
|
|
2938
|
+
operation,
|
|
2939
|
+
[
|
|
2940
|
+
"name",
|
|
2941
|
+
"contractVersion",
|
|
2942
|
+
"historicalContractVersions",
|
|
2943
|
+
"risk",
|
|
2944
|
+
"input",
|
|
2945
|
+
"sideEffect",
|
|
2946
|
+
"idempotency",
|
|
2947
|
+
"dedupeWindowMs",
|
|
2948
|
+
"state",
|
|
2949
|
+
"dispatch",
|
|
2950
|
+
"implementation",
|
|
2951
|
+
"planDispatches",
|
|
2952
|
+
"validateInput",
|
|
2953
|
+
"validateSubjectInput",
|
|
2954
|
+
"reconciliation",
|
|
2955
|
+
"omni",
|
|
2956
|
+
"access",
|
|
2957
|
+
...(official ? ["requiredScopeSets", "coverage"] : []),
|
|
2958
|
+
],
|
|
2959
|
+
"provider plugin operation",
|
|
2960
|
+
);
|
|
2961
|
+
if (
|
|
2962
|
+
!isProviderPluginOperationName(operation.name)
|
|
2963
|
+
) {
|
|
2964
|
+
throw new Error(`provider plugin operation ${String(operation.name)} must be a bounded dotted semantic name`);
|
|
2965
|
+
}
|
|
2966
|
+
if (
|
|
2967
|
+
!Number.isSafeInteger(operation.contractVersion)
|
|
2968
|
+
|| operation.contractVersion < 1
|
|
2969
|
+
|| operation.contractVersion > 1_000_000
|
|
2970
|
+
) {
|
|
2971
|
+
throw new Error(`provider plugin operation ${operation.name} has an invalid contract version`);
|
|
2972
|
+
}
|
|
2973
|
+
if (
|
|
2974
|
+
operation.historicalContractVersions !== undefined
|
|
2975
|
+
&& (
|
|
2976
|
+
!Array.isArray(operation.historicalContractVersions)
|
|
2977
|
+
|| operation.historicalContractVersions.length
|
|
2978
|
+
>= MAX_PROVIDER_PLUGIN_CONTRACT_VERSIONS_PER_OPERATION
|
|
2979
|
+
)
|
|
2980
|
+
) {
|
|
2981
|
+
throw new Error(
|
|
2982
|
+
`provider plugin operation ${operation.name} may declare at most ${MAX_PROVIDER_PLUGIN_CONTRACT_VERSIONS_PER_OPERATION - 1} historical contract versions`,
|
|
2983
|
+
);
|
|
2984
|
+
}
|
|
2985
|
+
const rawHistoricalContractVersions: readonly unknown[] =
|
|
2986
|
+
operation.historicalContractVersions === undefined
|
|
2987
|
+
? []
|
|
2988
|
+
: operation.historicalContractVersions;
|
|
2989
|
+
const copiedHistoricalContractVersions: number[] = [];
|
|
2990
|
+
for (const version of rawHistoricalContractVersions) {
|
|
2991
|
+
if (
|
|
2992
|
+
typeof version !== "number"
|
|
2993
|
+
|| !Number.isSafeInteger(version)
|
|
2994
|
+
|| version < 1
|
|
2995
|
+
|| version > 1_000_000
|
|
2996
|
+
|| version >= operation.contractVersion
|
|
2997
|
+
) {
|
|
2998
|
+
throw new Error(`provider plugin operation ${operation.name} has an invalid historical contract version`);
|
|
2999
|
+
}
|
|
3000
|
+
copiedHistoricalContractVersions.push(version);
|
|
3001
|
+
}
|
|
3002
|
+
const historicalContractVersions = Object.freeze(
|
|
3003
|
+
copiedHistoricalContractVersions,
|
|
3004
|
+
);
|
|
3005
|
+
if (new Set(historicalContractVersions).size !== historicalContractVersions.length) {
|
|
3006
|
+
throw new Error(`provider plugin operation ${operation.name} repeats a historical contract version`);
|
|
3007
|
+
}
|
|
3008
|
+
if (!["R1", "R2", "R3", "R4"].includes(operation.risk)) {
|
|
3009
|
+
throw new Error(`provider plugin operation ${operation.name} has an invalid risk`);
|
|
3010
|
+
}
|
|
3011
|
+
if (operation.state !== "observed" && operation.state !== "capture-required") {
|
|
3012
|
+
throw new Error(`provider plugin operation ${operation.name} has an invalid state`);
|
|
3013
|
+
}
|
|
3014
|
+
if (
|
|
3015
|
+
operation.access !== undefined
|
|
3016
|
+
&& (
|
|
3017
|
+
operation.access !== "public"
|
|
3018
|
+
|| official
|
|
3019
|
+
|| sourceKind !== "built-in"
|
|
3020
|
+
|| transport !== "web-session-api"
|
|
3021
|
+
|| operation.risk !== "R1"
|
|
3022
|
+
|| operation.state !== "observed"
|
|
3023
|
+
|| operation.dispatch !== "none"
|
|
3024
|
+
)
|
|
3025
|
+
) {
|
|
3026
|
+
throw new Error(
|
|
3027
|
+
`provider plugin operation ${operation.name} public access requires an observed dispatch-free built-in web-session R1 operation`,
|
|
3028
|
+
);
|
|
3029
|
+
}
|
|
3030
|
+
if (
|
|
3031
|
+
operation.dispatch !== "none"
|
|
3032
|
+
&& operation.dispatch !== "single"
|
|
3033
|
+
&& operation.dispatch !== "thread-items"
|
|
3034
|
+
&& operation.dispatch !== "bounded-items"
|
|
3035
|
+
) {
|
|
3036
|
+
throw new Error(`provider plugin operation ${operation.name} has an invalid dispatch policy`);
|
|
3037
|
+
}
|
|
3038
|
+
if (
|
|
3039
|
+
typeof operation.sideEffect !== "string"
|
|
3040
|
+
|| operation.sideEffect.length < 1
|
|
3041
|
+
|| operation.sideEffect.length > 500
|
|
3042
|
+
|| hasControlCharacters(operation.sideEffect)
|
|
3043
|
+
|| typeof operation.implementation !== "string"
|
|
3044
|
+
|| operation.implementation.length < 1
|
|
3045
|
+
|| operation.implementation.length > 500
|
|
3046
|
+
|| hasControlCharacters(operation.implementation)
|
|
3047
|
+
) {
|
|
3048
|
+
throw new Error(`provider plugin operation ${operation.name} has incomplete semantics`);
|
|
3049
|
+
}
|
|
3050
|
+
if (
|
|
3051
|
+
(operation.idempotency !== "none" && operation.idempotency !== "local-at-most-once")
|
|
3052
|
+
|| !Number.isSafeInteger(operation.dedupeWindowMs)
|
|
3053
|
+
|| operation.dedupeWindowMs < 0
|
|
3054
|
+
|| operation.dedupeWindowMs > 30 * 24 * 60 * 60_000
|
|
3055
|
+
|| typeof operation.planDispatches !== "function"
|
|
3056
|
+
|| typeof operation.validateInput !== "function"
|
|
3057
|
+
) {
|
|
3058
|
+
throw new Error(`provider plugin operation ${operation.name} has invalid host hooks`);
|
|
3059
|
+
}
|
|
3060
|
+
if (
|
|
3061
|
+
operation.risk === "R1"
|
|
3062
|
+
&& (
|
|
3063
|
+
operation.sideEffect !== "none"
|
|
3064
|
+
|| operation.dispatch !== "none"
|
|
3065
|
+
|| operation.idempotency !== "none"
|
|
3066
|
+
|| operation.dedupeWindowMs !== 0
|
|
3067
|
+
|| operation.reconciliation !== undefined
|
|
3068
|
+
|| Object.values(operation.input.properties).some(containsFileInput)
|
|
3069
|
+
)
|
|
3070
|
+
) {
|
|
3071
|
+
throw new Error(
|
|
3072
|
+
`provider plugin operation ${operation.name} must keep R1 read semantics side-effect-free and dispatch-free`,
|
|
3073
|
+
);
|
|
3074
|
+
}
|
|
3075
|
+
if (
|
|
3076
|
+
(operation.risk === "R2" || operation.risk === "R3")
|
|
3077
|
+
&& (
|
|
3078
|
+
operation.dispatch === "none"
|
|
3079
|
+
|| operation.idempotency !== "local-at-most-once"
|
|
3080
|
+
|| operation.dedupeWindowMs < 60_000
|
|
3081
|
+
)
|
|
3082
|
+
) {
|
|
3083
|
+
throw new Error(
|
|
3084
|
+
`provider plugin operation ${operation.name} must bind every R2/R3 write to a confirmed dispatch and positive at-most-once window`,
|
|
3085
|
+
);
|
|
3086
|
+
}
|
|
3087
|
+
if (
|
|
3088
|
+
operation.risk === "R4"
|
|
3089
|
+
&& operation.state !== "capture-required"
|
|
3090
|
+
) {
|
|
3091
|
+
throw new Error(
|
|
3092
|
+
`provider plugin operation ${operation.name} must keep R4 authority capture-required`,
|
|
3093
|
+
);
|
|
3094
|
+
}
|
|
3095
|
+
if (
|
|
3096
|
+
operation.validateSubjectInput !== undefined
|
|
3097
|
+
&& typeof operation.validateSubjectInput !== "function"
|
|
3098
|
+
) {
|
|
3099
|
+
throw new Error(
|
|
3100
|
+
`provider plugin operation ${operation.name} has an invalid subject-input hook`,
|
|
3101
|
+
);
|
|
3102
|
+
}
|
|
3103
|
+
if (operation.reconciliation !== undefined) {
|
|
3104
|
+
if (
|
|
3105
|
+
typeof operation.reconciliation !== "object"
|
|
3106
|
+
|| operation.reconciliation === null
|
|
3107
|
+
) {
|
|
3108
|
+
throw new Error(
|
|
3109
|
+
`provider plugin operation ${operation.name} has an invalid reconciliation contract`,
|
|
3110
|
+
);
|
|
3111
|
+
}
|
|
3112
|
+
const reconciliationLabel =
|
|
3113
|
+
`provider plugin operation ${operation.name} reconciliation`;
|
|
3114
|
+
if (operation.reconciliation.kind === "boolean-desired-state") {
|
|
3115
|
+
requireExactKeys(
|
|
3116
|
+
operation.reconciliation,
|
|
3117
|
+
["kind", "desiredState"],
|
|
3118
|
+
reconciliationLabel,
|
|
3119
|
+
);
|
|
3120
|
+
if (typeof operation.reconciliation.desiredState !== "function") {
|
|
3121
|
+
throw new Error(
|
|
3122
|
+
`provider plugin operation ${operation.name} has an invalid reconciliation contract`,
|
|
3123
|
+
);
|
|
3124
|
+
}
|
|
3125
|
+
} else if (
|
|
3126
|
+
operation.reconciliation.kind === "provider-accepted-target-presence"
|
|
3127
|
+
|| operation.reconciliation.kind
|
|
3128
|
+
=== "provider-bound-target-desired-state"
|
|
3129
|
+
) {
|
|
3130
|
+
requireExactKeys(
|
|
3131
|
+
operation.reconciliation,
|
|
3132
|
+
operation.reconciliation.kind === "provider-accepted-target-presence"
|
|
3133
|
+
? ["kind"]
|
|
3134
|
+
: ["kind", "desiredState"],
|
|
3135
|
+
reconciliationLabel,
|
|
3136
|
+
);
|
|
3137
|
+
if (
|
|
3138
|
+
operation.reconciliation.kind
|
|
3139
|
+
=== "provider-bound-target-desired-state"
|
|
3140
|
+
&& operation.reconciliation.desiredState !== false
|
|
3141
|
+
) {
|
|
3142
|
+
throw new Error(
|
|
3143
|
+
`provider plugin operation ${operation.name} has an invalid reconciliation contract`,
|
|
3144
|
+
);
|
|
3145
|
+
}
|
|
3146
|
+
if (operation.dispatch !== "single") {
|
|
3147
|
+
const targetKind = operation.reconciliation.kind
|
|
3148
|
+
=== "provider-accepted-target-presence"
|
|
3149
|
+
? "provider-accepted"
|
|
3150
|
+
: "provider-bound";
|
|
3151
|
+
throw new Error(
|
|
3152
|
+
`provider plugin operation ${operation.name} ${targetKind} target reconciliation requires one exact dispatch`,
|
|
3153
|
+
);
|
|
3154
|
+
}
|
|
3155
|
+
} else {
|
|
3156
|
+
throw new Error(
|
|
3157
|
+
`provider plugin operation ${operation.name} has an invalid reconciliation contract`,
|
|
3158
|
+
);
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
const isOmniInboxRead = operation.name === "messaging.list"
|
|
3162
|
+
|| operation.name === "messaging.read";
|
|
3163
|
+
if (isOmniInboxRead && operation.omni === undefined) {
|
|
3164
|
+
throw new Error(
|
|
3165
|
+
`provider plugin operation ${operation.name} must declare supported or unsupported omni normalization`,
|
|
3166
|
+
);
|
|
3167
|
+
}
|
|
3168
|
+
if (!isOmniInboxRead && operation.omni !== undefined) {
|
|
3169
|
+
throw new Error(
|
|
3170
|
+
`provider plugin operation ${operation.name} cannot declare inbox normalization`,
|
|
3171
|
+
);
|
|
3172
|
+
}
|
|
3173
|
+
let omni: ProviderPluginOmniDefinitionV1 | undefined;
|
|
3174
|
+
if (operation.omni !== undefined) {
|
|
3175
|
+
if (
|
|
3176
|
+
typeof operation.omni !== "object"
|
|
3177
|
+
|| operation.omni === null
|
|
3178
|
+
) {
|
|
3179
|
+
throw new Error(
|
|
3180
|
+
`provider plugin operation ${operation.name} has an invalid omni normalization contract`,
|
|
3181
|
+
);
|
|
3182
|
+
}
|
|
3183
|
+
if (operation.omni.state === "supported") {
|
|
3184
|
+
requireExactKeys(
|
|
3185
|
+
operation.omni,
|
|
3186
|
+
["state", "schemaVersion", "materializerId", "materializerVersion", "materialize"],
|
|
3187
|
+
`provider plugin operation ${operation.name} omni normalization`,
|
|
3188
|
+
);
|
|
3189
|
+
if (
|
|
3190
|
+
operation.state !== "observed"
|
|
3191
|
+
|| operation.risk !== "R1"
|
|
3192
|
+
|| operation.omni.schemaVersion !== 1
|
|
3193
|
+
|| typeof operation.omni.materializerId !== "string"
|
|
3194
|
+
|| !/^[a-z][a-z0-9-]{0,63}$/u.test(operation.omni.materializerId)
|
|
3195
|
+
|| !Number.isSafeInteger(operation.omni.materializerVersion)
|
|
3196
|
+
|| operation.omni.materializerVersion < 1
|
|
3197
|
+
|| operation.omni.materializerVersion > 1_000_000
|
|
3198
|
+
|| typeof operation.omni.materialize !== "function"
|
|
3199
|
+
) {
|
|
3200
|
+
throw new Error(
|
|
3201
|
+
`provider plugin operation ${operation.name} has an invalid supported omni normalization contract`,
|
|
3202
|
+
);
|
|
3203
|
+
}
|
|
3204
|
+
omni = Object.freeze({
|
|
3205
|
+
state: "supported",
|
|
3206
|
+
schemaVersion: 1,
|
|
3207
|
+
materializerId: operation.omni.materializerId,
|
|
3208
|
+
materializerVersion: operation.omni.materializerVersion,
|
|
3209
|
+
materialize: operation.omni.materialize,
|
|
3210
|
+
});
|
|
3211
|
+
} else if (operation.omni.state === "unsupported") {
|
|
3212
|
+
requireExactKeys(
|
|
3213
|
+
operation.omni,
|
|
3214
|
+
["state", "reason"],
|
|
3215
|
+
`provider plugin operation ${operation.name} omni normalization`,
|
|
3216
|
+
);
|
|
3217
|
+
if (
|
|
3218
|
+
typeof operation.omni.reason !== "string"
|
|
3219
|
+
|| operation.omni.reason.length < 1
|
|
3220
|
+
|| operation.omni.reason.length > 1_000
|
|
3221
|
+
|| hasControlCharacters(operation.omni.reason)
|
|
3222
|
+
) {
|
|
3223
|
+
throw new Error(
|
|
3224
|
+
`provider plugin operation ${operation.name} has an invalid unsupported omni normalization reason`,
|
|
3225
|
+
);
|
|
3226
|
+
}
|
|
3227
|
+
omni = Object.freeze({
|
|
3228
|
+
state: "unsupported",
|
|
3229
|
+
reason: operation.omni.reason,
|
|
3230
|
+
});
|
|
3231
|
+
} else {
|
|
3232
|
+
throw new Error(
|
|
3233
|
+
`provider plugin operation ${operation.name} has an invalid omni normalization state`,
|
|
3234
|
+
);
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
const {
|
|
3238
|
+
historicalContractVersions: ignoredHistoricalContractVersions,
|
|
3239
|
+
...operationWithoutHistory
|
|
3240
|
+
} = operation;
|
|
3241
|
+
void ignoredHistoricalContractVersions;
|
|
3242
|
+
const common = {
|
|
3243
|
+
...operationWithoutHistory,
|
|
3244
|
+
...(historicalContractVersions.length === 0 ? {} : { historicalContractVersions }),
|
|
3245
|
+
contractVersions: Object.freeze([
|
|
3246
|
+
...historicalContractVersions,
|
|
3247
|
+
operation.contractVersion,
|
|
3248
|
+
].sort((left, right) => left - right)),
|
|
3249
|
+
input: freezeInputSchema(
|
|
3250
|
+
operation.input,
|
|
3251
|
+
`provider plugin operation ${operation.name}.input`,
|
|
3252
|
+
official ? 100 : 25,
|
|
3253
|
+
),
|
|
3254
|
+
planDispatches: conformingProviderPluginPlanDispatches(operation),
|
|
3255
|
+
...(operation.reconciliation === undefined
|
|
3256
|
+
? {}
|
|
3257
|
+
: operation.reconciliation.kind === "boolean-desired-state"
|
|
3258
|
+
? {
|
|
3259
|
+
reconciliation: Object.freeze({
|
|
3260
|
+
kind: operation.reconciliation.kind,
|
|
3261
|
+
desiredState: operation.reconciliation.desiredState,
|
|
3262
|
+
}),
|
|
3263
|
+
}
|
|
3264
|
+
: operation.reconciliation.kind
|
|
3265
|
+
=== "provider-bound-target-desired-state"
|
|
3266
|
+
? {
|
|
3267
|
+
reconciliation: Object.freeze({
|
|
3268
|
+
kind: operation.reconciliation.kind,
|
|
3269
|
+
desiredState: operation.reconciliation.desiredState,
|
|
3270
|
+
}),
|
|
3271
|
+
}
|
|
3272
|
+
: {
|
|
3273
|
+
reconciliation: Object.freeze({
|
|
3274
|
+
kind: operation.reconciliation.kind,
|
|
3275
|
+
}),
|
|
3276
|
+
}),
|
|
3277
|
+
...(omni === undefined ? {} : { omni }),
|
|
3278
|
+
};
|
|
3279
|
+
if (!official) return Object.freeze(common);
|
|
3280
|
+
const providerOperation = operation as ProviderApiPluginOperationDefinitionV1;
|
|
3281
|
+
requireBoundedNonEmptyArray(
|
|
3282
|
+
providerOperation.requiredScopeSets,
|
|
3283
|
+
`provider plugin operation ${operation.name} requiredScopeSets`,
|
|
3284
|
+
32,
|
|
3285
|
+
);
|
|
3286
|
+
for (const scopeSet of providerOperation.requiredScopeSets) {
|
|
3287
|
+
requireBoundedNonEmptyArray(
|
|
3288
|
+
scopeSet,
|
|
3289
|
+
`provider plugin operation ${operation.name} scope set`,
|
|
3290
|
+
32,
|
|
3291
|
+
);
|
|
3292
|
+
}
|
|
3293
|
+
const requiredScopeSets = providerOperation.requiredScopeSets.map(
|
|
3294
|
+
(scopeSet) =>
|
|
3295
|
+
freezeStringList(
|
|
3296
|
+
scopeSet,
|
|
3297
|
+
`provider plugin operation ${operation.name} scope set`,
|
|
3298
|
+
32,
|
|
3299
|
+
),
|
|
3300
|
+
);
|
|
3301
|
+
requireBoundedNonEmptyArray(
|
|
3302
|
+
providerOperation.coverage,
|
|
3303
|
+
`provider plugin operation ${operation.name} coverage`,
|
|
3304
|
+
64,
|
|
3305
|
+
);
|
|
3306
|
+
return Object.freeze({
|
|
3307
|
+
...common,
|
|
3308
|
+
requiredScopeSets: Object.freeze(requiredScopeSets),
|
|
3309
|
+
coverage: freezeStringList(
|
|
3310
|
+
providerOperation.coverage,
|
|
3311
|
+
`provider plugin operation ${operation.name} coverage`,
|
|
3312
|
+
64,
|
|
3313
|
+
),
|
|
3314
|
+
});
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
function validateProviderRuntime(value: ProviderApiPluginRuntimeV1): ProviderApiPluginRuntimeV1 {
|
|
3318
|
+
requireExactKeys(value, ["execute", "executeMessagingPart"], "provider plugin runtime");
|
|
3319
|
+
if (typeof value.execute !== "function") throw new Error("provider plugin runtime must declare execute");
|
|
3320
|
+
if (
|
|
3321
|
+
value.executeMessagingPart !== undefined
|
|
3322
|
+
&& typeof value.executeMessagingPart !== "function"
|
|
3323
|
+
) throw new Error("provider plugin messaging action runtime hook is invalid");
|
|
3324
|
+
return Object.freeze({
|
|
3325
|
+
execute: value.execute,
|
|
3326
|
+
...(value.executeMessagingPart === undefined
|
|
3327
|
+
? {}
|
|
3328
|
+
: { executeMessagingPart: value.executeMessagingPart }),
|
|
3329
|
+
});
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
function validateWebRuntime(value: WebSessionPluginRuntimeV1): WebSessionPluginRuntimeV1 {
|
|
3333
|
+
requireExactKeys(
|
|
3334
|
+
value,
|
|
3335
|
+
["probe", "execute", "executeMessagingPart", "executePublic", "reconcile", "linkedDeviceLifecycle"],
|
|
3336
|
+
"web-session plugin runtime",
|
|
3337
|
+
);
|
|
3338
|
+
if (typeof value.probe !== "function" || typeof value.execute !== "function") {
|
|
3339
|
+
throw new Error("web-session plugin runtime must declare probe and execute");
|
|
3340
|
+
}
|
|
3341
|
+
if (
|
|
3342
|
+
value.executePublic !== undefined
|
|
3343
|
+
&& typeof value.executePublic !== "function"
|
|
3344
|
+
) {
|
|
3345
|
+
throw new Error("web-session plugin public runtime hook is invalid");
|
|
3346
|
+
}
|
|
3347
|
+
if (
|
|
3348
|
+
value.executeMessagingPart !== undefined
|
|
3349
|
+
&& typeof value.executeMessagingPart !== "function"
|
|
3350
|
+
) throw new Error("web-session plugin messaging action runtime hook is invalid");
|
|
3351
|
+
if (value.reconcile !== undefined && typeof value.reconcile !== "function") {
|
|
3352
|
+
throw new Error("web-session plugin runtime reconciliation hook is invalid");
|
|
3353
|
+
}
|
|
3354
|
+
let linkedDeviceLifecycle:
|
|
3355
|
+
| ProviderPluginLinkedDeviceLifecycleRuntimeV1
|
|
3356
|
+
| undefined;
|
|
3357
|
+
if (value.linkedDeviceLifecycle !== undefined) {
|
|
3358
|
+
requireExactKeys(
|
|
3359
|
+
value.linkedDeviceLifecycle,
|
|
3360
|
+
["inspect", "pair", "syncOnce"],
|
|
3361
|
+
"linked-device plugin lifecycle runtime",
|
|
3362
|
+
);
|
|
3363
|
+
if (
|
|
3364
|
+
typeof value.linkedDeviceLifecycle.inspect !== "function"
|
|
3365
|
+
|| typeof value.linkedDeviceLifecycle.pair !== "function"
|
|
3366
|
+
|| typeof value.linkedDeviceLifecycle.syncOnce !== "function"
|
|
3367
|
+
) {
|
|
3368
|
+
throw new Error("linked-device plugin lifecycle runtime is invalid");
|
|
3369
|
+
}
|
|
3370
|
+
linkedDeviceLifecycle = Object.freeze({
|
|
3371
|
+
inspect: value.linkedDeviceLifecycle.inspect,
|
|
3372
|
+
pair: value.linkedDeviceLifecycle.pair,
|
|
3373
|
+
syncOnce: value.linkedDeviceLifecycle.syncOnce,
|
|
3374
|
+
});
|
|
3375
|
+
}
|
|
3376
|
+
return Object.freeze({
|
|
3377
|
+
probe: value.probe,
|
|
3378
|
+
execute: value.execute,
|
|
3379
|
+
...(value.executeMessagingPart === undefined
|
|
3380
|
+
? {}
|
|
3381
|
+
: { executeMessagingPart: value.executeMessagingPart }),
|
|
3382
|
+
...(value.executePublic === undefined
|
|
3383
|
+
? {}
|
|
3384
|
+
: { executePublic: value.executePublic }),
|
|
3385
|
+
...(value.reconcile === undefined ? {} : { reconcile: value.reconcile }),
|
|
3386
|
+
...(linkedDeviceLifecycle === undefined ? {} : { linkedDeviceLifecycle }),
|
|
3387
|
+
});
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3390
|
+
function validateLocalCliRuntime(
|
|
3391
|
+
value: LocalCliPluginRuntimeV1,
|
|
3392
|
+
): LocalCliPluginRuntimeV1 {
|
|
3393
|
+
requireExactKeys(
|
|
3394
|
+
value,
|
|
3395
|
+
["inspect", "probe", "execute", "executeMessagingPart", "reconcile"],
|
|
3396
|
+
"local CLI plugin runtime",
|
|
3397
|
+
);
|
|
3398
|
+
if (
|
|
3399
|
+
typeof value.inspect !== "function"
|
|
3400
|
+
|| typeof value.probe !== "function"
|
|
3401
|
+
|| typeof value.execute !== "function"
|
|
3402
|
+
) {
|
|
3403
|
+
throw new Error("local CLI plugin runtime must declare inspect, probe, and execute");
|
|
3404
|
+
}
|
|
3405
|
+
if (value.reconcile !== undefined && typeof value.reconcile !== "function") {
|
|
3406
|
+
throw new Error("local CLI plugin runtime reconciliation hook is invalid");
|
|
3407
|
+
}
|
|
3408
|
+
if (
|
|
3409
|
+
value.executeMessagingPart !== undefined
|
|
3410
|
+
&& typeof value.executeMessagingPart !== "function"
|
|
3411
|
+
) throw new Error("local CLI plugin messaging action runtime hook is invalid");
|
|
3412
|
+
return Object.freeze({
|
|
3413
|
+
inspect: value.inspect,
|
|
3414
|
+
probe: value.probe,
|
|
3415
|
+
execute: value.execute,
|
|
3416
|
+
...(value.executeMessagingPart === undefined
|
|
3417
|
+
? {}
|
|
3418
|
+
: { executeMessagingPart: value.executeMessagingPart }),
|
|
3419
|
+
...(value.reconcile === undefined ? {} : { reconcile: value.reconcile }),
|
|
3420
|
+
});
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
function memoizedRuntime<T>(
|
|
3424
|
+
loader: () => Promise<T>,
|
|
3425
|
+
validate: (value: T) => T,
|
|
3426
|
+
): () => Promise<T> {
|
|
3427
|
+
let pending: Promise<T> | undefined;
|
|
3428
|
+
const loadRuntime = () => {
|
|
3429
|
+
providerPluginStartedRuntimeLoaders.add(loadRuntime);
|
|
3430
|
+
if (pending === undefined) {
|
|
3431
|
+
pending = Promise.resolve().then(async () => {
|
|
3432
|
+
const identity = providerPluginRuntimeLoadIdentities.get(loadRuntime);
|
|
3433
|
+
await identity?.verify("before");
|
|
3434
|
+
const runtime = validate(await loader());
|
|
3435
|
+
await identity?.verify("after");
|
|
3436
|
+
return runtime;
|
|
3437
|
+
});
|
|
3438
|
+
}
|
|
3439
|
+
return pending;
|
|
3440
|
+
};
|
|
3441
|
+
return loadRuntime;
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
export type ProviderPluginRuntimeLoadIdentityPhase = "before" | "after";
|
|
3445
|
+
|
|
3446
|
+
type ProviderPluginRuntimeLoadIdentity = {
|
|
3447
|
+
readonly token: string;
|
|
3448
|
+
readonly verify: (
|
|
3449
|
+
phase: ProviderPluginRuntimeLoadIdentityPhase,
|
|
3450
|
+
) => void | Promise<void>;
|
|
3451
|
+
};
|
|
3452
|
+
|
|
3453
|
+
const providerPluginRuntimeLoadIdentities = new WeakMap<
|
|
3454
|
+
() => Promise<unknown>,
|
|
3455
|
+
ProviderPluginRuntimeLoadIdentity
|
|
3456
|
+
>();
|
|
3457
|
+
const providerPluginLazyRuntimeLoaders = new WeakSet<
|
|
3458
|
+
() => Promise<unknown>
|
|
3459
|
+
>();
|
|
3460
|
+
const providerPluginStartedRuntimeLoaders = new WeakSet<
|
|
3461
|
+
() => Promise<unknown>
|
|
3462
|
+
>();
|
|
3463
|
+
|
|
3464
|
+
/**
|
|
3465
|
+
* Registry-only binding between a lazy loader and the exact startup source
|
|
3466
|
+
* snapshot whose hash is published in plans and receipts.
|
|
3467
|
+
*/
|
|
3468
|
+
export function bindProviderPluginRuntimeLoadIdentity(
|
|
3469
|
+
loadRuntime: () => Promise<unknown>,
|
|
3470
|
+
identity: ProviderPluginRuntimeLoadIdentity,
|
|
3471
|
+
): void {
|
|
3472
|
+
const current = providerPluginRuntimeLoadIdentities.get(loadRuntime);
|
|
3473
|
+
if (current !== undefined) {
|
|
3474
|
+
if (current.token !== identity.token) {
|
|
3475
|
+
throw new Error(
|
|
3476
|
+
"provider plugin runtime loader is already bound to a different implementation identity",
|
|
3477
|
+
);
|
|
3478
|
+
}
|
|
3479
|
+
return;
|
|
3480
|
+
}
|
|
3481
|
+
if (providerPluginStartedRuntimeLoaders.has(loadRuntime)) {
|
|
3482
|
+
throw new Error(
|
|
3483
|
+
"provider plugin runtime loader was invoked before its implementation identity was bound",
|
|
3484
|
+
);
|
|
3485
|
+
}
|
|
3486
|
+
providerPluginRuntimeLoadIdentities.set(
|
|
3487
|
+
loadRuntime,
|
|
3488
|
+
Object.freeze(identity),
|
|
3489
|
+
);
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3492
|
+
export function lazyProviderApiRuntime(
|
|
3493
|
+
loader: () => Promise<ProviderApiPluginRuntimeV1>,
|
|
3494
|
+
): ProviderApiPluginRuntimeHooksV1 {
|
|
3495
|
+
const loadRuntime = memoizedRuntime(loader, validateProviderRuntime);
|
|
3496
|
+
providerPluginLazyRuntimeLoaders.add(loadRuntime);
|
|
3497
|
+
return Object.freeze({ loadRuntime });
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3500
|
+
export function lazyWebSessionRuntime(
|
|
3501
|
+
loader: () => Promise<WebSessionPluginRuntimeV1>,
|
|
3502
|
+
): WebSessionPluginRuntimeHooksV1 {
|
|
3503
|
+
const loadRuntime = memoizedRuntime(loader, validateWebRuntime);
|
|
3504
|
+
providerPluginLazyRuntimeLoaders.add(loadRuntime);
|
|
3505
|
+
return Object.freeze({ loadRuntime });
|
|
3506
|
+
}
|
|
3507
|
+
|
|
3508
|
+
export function lazyLocalCliRuntime(
|
|
3509
|
+
loader: () => Promise<LocalCliPluginRuntimeV1>,
|
|
3510
|
+
): LocalCliPluginRuntimeHooksV1 {
|
|
3511
|
+
const loadRuntime = memoizedRuntime(loader, validateLocalCliRuntime);
|
|
3512
|
+
providerPluginLazyRuntimeLoaders.add(loadRuntime);
|
|
3513
|
+
return Object.freeze({ loadRuntime });
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3516
|
+
function freezeProviderPluginMessaging(
|
|
3517
|
+
value: ProviderPluginMessagingDefinitionV1 | undefined,
|
|
3518
|
+
operations: readonly ProviderPluginOperationV1[],
|
|
3519
|
+
surfaceId: string,
|
|
3520
|
+
): ProviderPluginMessagingDefinitionV1 | undefined {
|
|
3521
|
+
if (value === undefined) return undefined;
|
|
3522
|
+
requireExactKeys(value, [
|
|
3523
|
+
"schemaVersion",
|
|
3524
|
+
"contractId",
|
|
3525
|
+
"network",
|
|
3526
|
+
"contextLiveness",
|
|
3527
|
+
"listOperation",
|
|
3528
|
+
"contextOperation",
|
|
3529
|
+
"enumerateRoutes",
|
|
3530
|
+
"resolveRoute",
|
|
3531
|
+
"parseTarget",
|
|
3532
|
+
"contextInput",
|
|
3533
|
+
"action",
|
|
3534
|
+
], `provider plugin surface ${surfaceId} messaging SPI`);
|
|
3535
|
+
if (
|
|
3536
|
+
value.schemaVersion !== 1
|
|
3537
|
+
|| typeof value.contractId !== "string"
|
|
3538
|
+
|| !/^[a-z][a-z0-9.-]{0,127}\.v1$/u.test(value.contractId)
|
|
3539
|
+
|| typeof value.network !== "string"
|
|
3540
|
+
|| !/^[a-z][a-z0-9-]{0,63}$/u.test(value.network)
|
|
3541
|
+
|| value.contextLiveness !== "fresh-as-of-live-preflight"
|
|
3542
|
+
&& value.contextLiveness !== "freshness-unproven"
|
|
3543
|
+
|| value.listOperation !== "messaging.list"
|
|
3544
|
+
|| value.contextOperation !== "messaging.read"
|
|
3545
|
+
|| typeof value.enumerateRoutes !== "function"
|
|
3546
|
+
|| typeof value.resolveRoute !== "object"
|
|
3547
|
+
|| value.resolveRoute === null
|
|
3548
|
+
|| typeof value.parseTarget !== "function"
|
|
3549
|
+
|| typeof value.contextInput !== "function"
|
|
3550
|
+
) {
|
|
3551
|
+
throw new Error(
|
|
3552
|
+
`provider plugin surface ${surfaceId} has an invalid messaging SPI`,
|
|
3553
|
+
);
|
|
3554
|
+
}
|
|
3555
|
+
requireExactKeys(
|
|
3556
|
+
value.resolveRoute,
|
|
3557
|
+
["operation", "input", "candidates", "sourceConversationCoordinate"],
|
|
3558
|
+
`provider plugin surface ${surfaceId} messaging exact route resolution`,
|
|
3559
|
+
);
|
|
3560
|
+
if (
|
|
3561
|
+
typeof value.resolveRoute.operation !== "string"
|
|
3562
|
+
|| !isProviderPluginOperationName(value.resolveRoute.operation)
|
|
3563
|
+
|| typeof value.resolveRoute.input !== "function"
|
|
3564
|
+
|| typeof value.resolveRoute.candidates !== "function"
|
|
3565
|
+
|| typeof value.resolveRoute.sourceConversationCoordinate !== "function"
|
|
3566
|
+
) throw new Error(
|
|
3567
|
+
`provider plugin surface ${surfaceId} has an invalid exact messaging route resolver`,
|
|
3568
|
+
);
|
|
3569
|
+
const exactResolutionOperation = operations.find((candidate) =>
|
|
3570
|
+
candidate.name === value.resolveRoute.operation
|
|
3571
|
+
&& candidate.contractVersion === 1);
|
|
3572
|
+
if (
|
|
3573
|
+
exactResolutionOperation === undefined
|
|
3574
|
+
|| exactResolutionOperation.state !== "observed"
|
|
3575
|
+
|| exactResolutionOperation.risk !== "R1"
|
|
3576
|
+
) {
|
|
3577
|
+
throw new Error(
|
|
3578
|
+
`provider plugin surface ${surfaceId} messaging SPI requires one observed R1 exact route resolver`,
|
|
3579
|
+
);
|
|
3580
|
+
}
|
|
3581
|
+
for (const operationName of [value.listOperation, value.contextOperation]) {
|
|
3582
|
+
const operation = operations.find((candidate) =>
|
|
3583
|
+
candidate.name === operationName && candidate.contractVersion === 1);
|
|
3584
|
+
if (
|
|
3585
|
+
operation === undefined
|
|
3586
|
+
|| operation.state !== "observed"
|
|
3587
|
+
|| operation.risk !== "R1"
|
|
3588
|
+
|| operation.omni?.state !== "supported"
|
|
3589
|
+
) {
|
|
3590
|
+
throw new Error(
|
|
3591
|
+
`provider plugin surface ${surfaceId} messaging SPI requires observed normalized ${operationName}@1`,
|
|
3592
|
+
);
|
|
3593
|
+
}
|
|
3594
|
+
}
|
|
3595
|
+
if (typeof value.action !== "object" || value.action === null) {
|
|
3596
|
+
throw new Error(
|
|
3597
|
+
`provider plugin surface ${surfaceId} messaging SPI has an invalid action declaration`,
|
|
3598
|
+
);
|
|
3599
|
+
}
|
|
3600
|
+
if (value.action.state === "unavailable") {
|
|
3601
|
+
requireExactKeys(
|
|
3602
|
+
value.action,
|
|
3603
|
+
["state", "reason", "reply"],
|
|
3604
|
+
`provider plugin surface ${surfaceId} messaging action`,
|
|
3605
|
+
);
|
|
3606
|
+
if (
|
|
3607
|
+
value.action.reply !== "unsupported"
|
|
3608
|
+
|| typeof value.action.reason !== "string"
|
|
3609
|
+
|| value.action.reason.length < 1
|
|
3610
|
+
|| value.action.reason.length > 1_000
|
|
3611
|
+
|| hasControlCharacters(value.action.reason)
|
|
3612
|
+
) {
|
|
3613
|
+
throw new Error(
|
|
3614
|
+
`provider plugin surface ${surfaceId} messaging action unavailability is invalid`,
|
|
3615
|
+
);
|
|
3616
|
+
}
|
|
3617
|
+
return Object.freeze({
|
|
3618
|
+
schemaVersion: 1,
|
|
3619
|
+
contractId: value.contractId,
|
|
3620
|
+
network: value.network,
|
|
3621
|
+
contextLiveness: value.contextLiveness,
|
|
3622
|
+
listOperation: value.listOperation,
|
|
3623
|
+
contextOperation: value.contextOperation,
|
|
3624
|
+
enumerateRoutes: value.enumerateRoutes,
|
|
3625
|
+
resolveRoute: Object.freeze({
|
|
3626
|
+
operation: value.resolveRoute.operation,
|
|
3627
|
+
input: value.resolveRoute.input,
|
|
3628
|
+
candidates: value.resolveRoute.candidates,
|
|
3629
|
+
sourceConversationCoordinate:
|
|
3630
|
+
value.resolveRoute.sourceConversationCoordinate,
|
|
3631
|
+
}),
|
|
3632
|
+
parseTarget: value.parseTarget,
|
|
3633
|
+
contextInput: value.contextInput,
|
|
3634
|
+
action: Object.freeze({
|
|
3635
|
+
state: "unavailable",
|
|
3636
|
+
reason: value.action.reason,
|
|
3637
|
+
reply: "unsupported",
|
|
3638
|
+
}),
|
|
3639
|
+
});
|
|
3640
|
+
}
|
|
3641
|
+
if (value.action.state !== "supported") {
|
|
3642
|
+
throw new Error(
|
|
3643
|
+
`provider plugin surface ${surfaceId} messaging action has an invalid state`,
|
|
3644
|
+
);
|
|
3645
|
+
}
|
|
3646
|
+
if (value.resolveRoute.operation !== "conversations.read") {
|
|
3647
|
+
throw new Error(
|
|
3648
|
+
`provider plugin surface ${surfaceId} actionable messaging requires exact conversations.read route resolution`,
|
|
3649
|
+
);
|
|
3650
|
+
}
|
|
3651
|
+
const action = value.action;
|
|
3652
|
+
requireExactKeys(
|
|
3653
|
+
action,
|
|
3654
|
+
[
|
|
3655
|
+
"state",
|
|
3656
|
+
"operation",
|
|
3657
|
+
"reply",
|
|
3658
|
+
"livePreflight",
|
|
3659
|
+
"compileTurnPart",
|
|
3660
|
+
"mapAcceptedResult",
|
|
3661
|
+
"proveExpectedOwnPrefix",
|
|
3662
|
+
"reconciliation",
|
|
3663
|
+
],
|
|
3664
|
+
`provider plugin surface ${surfaceId} messaging action`,
|
|
3665
|
+
);
|
|
3666
|
+
const actionOperation = operations.find((candidate) =>
|
|
3667
|
+
candidate.name === action.operation && candidate.contractVersion === 1);
|
|
3668
|
+
if (
|
|
3669
|
+
actionOperation === undefined
|
|
3670
|
+
|| actionOperation.state !== "observed"
|
|
3671
|
+
|| actionOperation.risk !== "R3"
|
|
3672
|
+
|| action.reply !== "supported" && action.reply !== "unsupported"
|
|
3673
|
+
|| typeof action.livePreflight !== "object"
|
|
3674
|
+
|| action.livePreflight === null
|
|
3675
|
+
|| typeof action.compileTurnPart !== "function"
|
|
3676
|
+
|| typeof action.mapAcceptedResult !== "function"
|
|
3677
|
+
|| typeof action.proveExpectedOwnPrefix !== "function"
|
|
3678
|
+
|| typeof action.reconciliation !== "function"
|
|
3679
|
+
) {
|
|
3680
|
+
throw new Error(
|
|
3681
|
+
`provider plugin surface ${surfaceId} messaging action must bind one observed R3 operation`,
|
|
3682
|
+
);
|
|
3683
|
+
}
|
|
3684
|
+
requireExactKeys(
|
|
3685
|
+
action.livePreflight,
|
|
3686
|
+
["operation", "input", "snapshot"],
|
|
3687
|
+
`provider plugin surface ${surfaceId} messaging action live preflight`,
|
|
3688
|
+
);
|
|
3689
|
+
const livePreflightOperation = operations.find((candidate) =>
|
|
3690
|
+
candidate.name === action.livePreflight.operation
|
|
3691
|
+
&& candidate.contractVersion === 1);
|
|
3692
|
+
if (
|
|
3693
|
+
livePreflightOperation === undefined
|
|
3694
|
+
|| livePreflightOperation.state !== "observed"
|
|
3695
|
+
|| livePreflightOperation.risk !== "R1"
|
|
3696
|
+
|| typeof action.livePreflight.input !== "function"
|
|
3697
|
+
|| typeof action.livePreflight.snapshot !== "function"
|
|
3698
|
+
) throw new Error(
|
|
3699
|
+
`provider plugin surface ${surfaceId} messaging action requires one observed R1 live preflight`,
|
|
3700
|
+
);
|
|
3701
|
+
return Object.freeze({
|
|
3702
|
+
schemaVersion: 1,
|
|
3703
|
+
contractId: value.contractId,
|
|
3704
|
+
network: value.network,
|
|
3705
|
+
contextLiveness: value.contextLiveness,
|
|
3706
|
+
listOperation: value.listOperation,
|
|
3707
|
+
contextOperation: value.contextOperation,
|
|
3708
|
+
enumerateRoutes: value.enumerateRoutes,
|
|
3709
|
+
resolveRoute: Object.freeze({
|
|
3710
|
+
operation: value.resolveRoute.operation,
|
|
3711
|
+
input: value.resolveRoute.input,
|
|
3712
|
+
candidates: value.resolveRoute.candidates,
|
|
3713
|
+
sourceConversationCoordinate:
|
|
3714
|
+
value.resolveRoute.sourceConversationCoordinate,
|
|
3715
|
+
}),
|
|
3716
|
+
parseTarget: value.parseTarget,
|
|
3717
|
+
contextInput: value.contextInput,
|
|
3718
|
+
action: Object.freeze({
|
|
3719
|
+
state: "supported",
|
|
3720
|
+
operation: action.operation,
|
|
3721
|
+
reply: action.reply,
|
|
3722
|
+
livePreflight: Object.freeze({
|
|
3723
|
+
operation: action.livePreflight.operation,
|
|
3724
|
+
input: action.livePreflight.input,
|
|
3725
|
+
snapshot: action.livePreflight.snapshot,
|
|
3726
|
+
}),
|
|
3727
|
+
compileTurnPart: action.compileTurnPart,
|
|
3728
|
+
mapAcceptedResult: action.mapAcceptedResult,
|
|
3729
|
+
proveExpectedOwnPrefix: action.proveExpectedOwnPrefix,
|
|
3730
|
+
reconciliation: action.reconciliation,
|
|
3731
|
+
}),
|
|
3732
|
+
});
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
function freezeBinding(
|
|
3736
|
+
binding: ProviderPluginBindingDefinitionV1,
|
|
3737
|
+
sourceKind: ProviderPluginV1["sourceKind"],
|
|
3738
|
+
): ProviderPluginBindingV1 {
|
|
3739
|
+
requireExactKeys(
|
|
3740
|
+
binding,
|
|
3741
|
+
[
|
|
3742
|
+
"transport",
|
|
3743
|
+
"surfaceId",
|
|
3744
|
+
"origin",
|
|
3745
|
+
...(binding.transport === "provider-api" ? ["runtimeOrigins"] : []),
|
|
3746
|
+
...(binding.transport === "local-cli" ? ["tool"] : []),
|
|
3747
|
+
"manifestOrigins",
|
|
3748
|
+
"protectedHostnameFamilies",
|
|
3749
|
+
"authKinds",
|
|
3750
|
+
"operations",
|
|
3751
|
+
"subject",
|
|
3752
|
+
"messaging",
|
|
3753
|
+
...(binding.transport === "linked-device"
|
|
3754
|
+
? ["linkedDeviceLifecycle"]
|
|
3755
|
+
: []),
|
|
3756
|
+
"runtime",
|
|
3757
|
+
],
|
|
3758
|
+
"provider plugin binding",
|
|
3759
|
+
);
|
|
3760
|
+
if (!providerPluginTransports.includes(binding.transport)) {
|
|
3761
|
+
throw new Error("provider plugin binding has an unsupported transport");
|
|
3762
|
+
}
|
|
3763
|
+
if (!isProviderPluginSurfaceId(binding.surfaceId)) {
|
|
3764
|
+
throw new Error(`provider plugin surface ID ${binding.surfaceId} must be strict lowercase kebab-case with at most 63 characters`);
|
|
3765
|
+
}
|
|
3766
|
+
let origin: URL;
|
|
3767
|
+
try {
|
|
3768
|
+
origin = new URL(binding.origin);
|
|
3769
|
+
} catch {
|
|
3770
|
+
throw new Error(`provider plugin surface ${binding.surfaceId} has an invalid origin`);
|
|
3771
|
+
}
|
|
3772
|
+
if (
|
|
3773
|
+
origin.protocol !== "https:"
|
|
3774
|
+
|| origin.username !== ""
|
|
3775
|
+
|| origin.password !== ""
|
|
3776
|
+
|| origin.pathname !== "/"
|
|
3777
|
+
|| origin.search !== ""
|
|
3778
|
+
|| origin.hash !== ""
|
|
3779
|
+
|| binding.origin !== origin.origin
|
|
3780
|
+
) {
|
|
3781
|
+
throw new Error(`provider plugin surface ${binding.surfaceId} must declare an exact credential-free HTTPS origin`);
|
|
3782
|
+
}
|
|
3783
|
+
let runtimeOrigins: `https://${string}`[] = [];
|
|
3784
|
+
if (binding.transport === "provider-api") {
|
|
3785
|
+
const declaredRuntimeOrigins = binding.runtimeOrigins ?? [binding.origin];
|
|
3786
|
+
requireBoundedNonEmptyArray(
|
|
3787
|
+
declaredRuntimeOrigins,
|
|
3788
|
+
`provider plugin surface ${binding.surfaceId} runtimeOrigins`,
|
|
3789
|
+
20,
|
|
3790
|
+
);
|
|
3791
|
+
runtimeOrigins = [...declaredRuntimeOrigins];
|
|
3792
|
+
for (const runtimeOrigin of runtimeOrigins) {
|
|
3793
|
+
let parsed: URL;
|
|
3794
|
+
try {
|
|
3795
|
+
parsed = new URL(runtimeOrigin);
|
|
3796
|
+
} catch {
|
|
3797
|
+
throw new Error(
|
|
3798
|
+
`provider plugin surface ${binding.surfaceId} has an invalid runtime origin`,
|
|
3799
|
+
);
|
|
3800
|
+
}
|
|
3801
|
+
if (
|
|
3802
|
+
parsed.protocol !== "https:"
|
|
3803
|
+
|| parsed.username !== ""
|
|
3804
|
+
|| parsed.password !== ""
|
|
3805
|
+
|| parsed.pathname !== "/"
|
|
3806
|
+
|| parsed.search !== ""
|
|
3807
|
+
|| parsed.hash !== ""
|
|
3808
|
+
|| runtimeOrigin !== parsed.origin
|
|
3809
|
+
) {
|
|
3810
|
+
throw new Error(
|
|
3811
|
+
`provider plugin surface ${binding.surfaceId} must declare exact credential-free HTTPS runtime origins`,
|
|
3812
|
+
);
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
if (new Set(runtimeOrigins).size !== runtimeOrigins.length) {
|
|
3816
|
+
throw new Error(
|
|
3817
|
+
`provider plugin surface ${binding.surfaceId} repeats a runtime origin`,
|
|
3818
|
+
);
|
|
3819
|
+
}
|
|
3820
|
+
if (!runtimeOrigins.includes(binding.origin)) {
|
|
3821
|
+
throw new Error(
|
|
3822
|
+
`provider plugin surface ${binding.surfaceId} runtime origins must include its primary origin`,
|
|
3823
|
+
);
|
|
3824
|
+
}
|
|
3825
|
+
runtimeOrigins.sort();
|
|
3826
|
+
}
|
|
3827
|
+
const declaredManifestOrigins = binding.manifestOrigins === undefined
|
|
3828
|
+
? [binding.origin]
|
|
3829
|
+
: binding.manifestOrigins;
|
|
3830
|
+
requireBoundedNonEmptyArray(
|
|
3831
|
+
declaredManifestOrigins,
|
|
3832
|
+
`provider plugin surface ${binding.surfaceId} manifestOrigins`,
|
|
3833
|
+
20,
|
|
3834
|
+
);
|
|
3835
|
+
const manifestOrigins = [...declaredManifestOrigins];
|
|
3836
|
+
for (const manifestOrigin of manifestOrigins) {
|
|
3837
|
+
let parsed: URL;
|
|
3838
|
+
try {
|
|
3839
|
+
parsed = new URL(manifestOrigin);
|
|
3840
|
+
} catch {
|
|
3841
|
+
throw new Error(
|
|
3842
|
+
`provider plugin surface ${binding.surfaceId} has an invalid manifest origin`,
|
|
3843
|
+
);
|
|
3844
|
+
}
|
|
3845
|
+
if (
|
|
3846
|
+
parsed.protocol !== "https:"
|
|
3847
|
+
|| parsed.username !== ""
|
|
3848
|
+
|| parsed.password !== ""
|
|
3849
|
+
|| parsed.pathname !== "/"
|
|
3850
|
+
|| parsed.search !== ""
|
|
3851
|
+
|| parsed.hash !== ""
|
|
3852
|
+
|| manifestOrigin !== parsed.origin
|
|
3853
|
+
) {
|
|
3854
|
+
throw new Error(
|
|
3855
|
+
`provider plugin surface ${binding.surfaceId} must declare exact credential-free HTTPS manifest origins`,
|
|
3856
|
+
);
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
if (new Set(manifestOrigins).size !== manifestOrigins.length) {
|
|
3860
|
+
throw new Error(
|
|
3861
|
+
`provider plugin surface ${binding.surfaceId} repeats a manifest origin`,
|
|
3862
|
+
);
|
|
3863
|
+
}
|
|
3864
|
+
const endpointHostnames = [
|
|
3865
|
+
...runtimeOrigins.map((runtimeOrigin) =>
|
|
3866
|
+
new URL(runtimeOrigin).hostname.toLowerCase()),
|
|
3867
|
+
...(binding.transport === "provider-api"
|
|
3868
|
+
? []
|
|
3869
|
+
: [origin.hostname.toLowerCase()]),
|
|
3870
|
+
...manifestOrigins.map((manifestOrigin) =>
|
|
3871
|
+
new URL(manifestOrigin).hostname.toLowerCase()),
|
|
3872
|
+
];
|
|
3873
|
+
const declaredProtectedHostnameFamilies =
|
|
3874
|
+
binding.protectedHostnameFamilies === undefined
|
|
3875
|
+
? [...new Set(endpointHostnames)]
|
|
3876
|
+
: binding.protectedHostnameFamilies;
|
|
3877
|
+
requireBoundedNonEmptyArray(
|
|
3878
|
+
declaredProtectedHostnameFamilies,
|
|
3879
|
+
`provider plugin surface ${binding.surfaceId} protectedHostnameFamilies`,
|
|
3880
|
+
20,
|
|
3881
|
+
);
|
|
3882
|
+
const protectedHostnameFamilies = [...declaredProtectedHostnameFamilies];
|
|
3883
|
+
for (const family of protectedHostnameFamilies) {
|
|
3884
|
+
if (
|
|
3885
|
+
typeof family !== "string"
|
|
3886
|
+
|| family !== family.toLowerCase()
|
|
3887
|
+
|| family.length > 253
|
|
3888
|
+
|| !/^[a-z0-9](?:[a-z0-9.-]{0,251}[a-z0-9])?$/u.test(family)
|
|
3889
|
+
|| family.includes("..")
|
|
3890
|
+
) {
|
|
3891
|
+
throw new Error(
|
|
3892
|
+
`provider plugin surface ${binding.surfaceId} has an invalid protected hostname family`,
|
|
3893
|
+
);
|
|
3894
|
+
}
|
|
3895
|
+
}
|
|
3896
|
+
if (
|
|
3897
|
+
new Set(protectedHostnameFamilies).size
|
|
3898
|
+
!== protectedHostnameFamilies.length
|
|
3899
|
+
) {
|
|
3900
|
+
throw new Error(
|
|
3901
|
+
`provider plugin surface ${binding.surfaceId} repeats a protected hostname family`,
|
|
3902
|
+
);
|
|
3903
|
+
}
|
|
3904
|
+
for (const hostname of endpointHostnames) {
|
|
3905
|
+
if (
|
|
3906
|
+
!protectedHostnameFamilies.some((family) =>
|
|
3907
|
+
hostname === family || hostname.endsWith(`.${family}`))
|
|
3908
|
+
) {
|
|
3909
|
+
throw new Error(
|
|
3910
|
+
`provider plugin surface ${binding.surfaceId} protected hostname families do not cover ${hostname}`,
|
|
3911
|
+
);
|
|
3912
|
+
}
|
|
3913
|
+
}
|
|
3914
|
+
requireBoundedNonEmptyArray(
|
|
3915
|
+
binding.authKinds,
|
|
3916
|
+
`provider plugin surface ${binding.surfaceId} authKinds`,
|
|
3917
|
+
binding.transport === "web-session-api" || binding.transport === "local-cli"
|
|
3918
|
+
? 5
|
|
3919
|
+
: 1,
|
|
3920
|
+
);
|
|
3921
|
+
const acceptedAuthKinds = [...binding.authKinds];
|
|
3922
|
+
for (const kind of acceptedAuthKinds) {
|
|
3923
|
+
if (!authKinds.has(kind)) {
|
|
3924
|
+
throw new Error(`provider plugin surface ${binding.surfaceId} accepts unsupported auth kind ${String(kind)}`);
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
if (new Set(acceptedAuthKinds).size !== acceptedAuthKinds.length) {
|
|
3928
|
+
throw new Error(`provider plugin surface ${binding.surfaceId} repeats an auth kind`);
|
|
3929
|
+
}
|
|
3930
|
+
acceptedAuthKinds.sort();
|
|
3931
|
+
if (
|
|
3932
|
+
binding.transport === "provider-api"
|
|
3933
|
+
&& (acceptedAuthKinds.length !== 1 || acceptedAuthKinds[0] !== "oauth-token-file")
|
|
3934
|
+
) {
|
|
3935
|
+
throw new Error(`provider plugin surface ${binding.surfaceId} provider-api auth must be oauth-token-file`);
|
|
3936
|
+
}
|
|
3937
|
+
if (
|
|
3938
|
+
binding.transport === "linked-device"
|
|
3939
|
+
&& (acceptedAuthKinds.length !== 1 || acceptedAuthKinds[0] !== "linked-device-store")
|
|
3940
|
+
) {
|
|
3941
|
+
throw new Error(`provider plugin surface ${binding.surfaceId} linked-device auth must be linked-device-store`);
|
|
3942
|
+
}
|
|
3943
|
+
if (
|
|
3944
|
+
binding.transport === "web-session-api"
|
|
3945
|
+
&& acceptedAuthKinds.some((kind) => kind === "oauth-token-file" || kind === "linked-device-store")
|
|
3946
|
+
) {
|
|
3947
|
+
throw new Error(`provider plugin surface ${binding.surfaceId} web-session-api auth must use browser-session credentials`);
|
|
3948
|
+
}
|
|
3949
|
+
const localCliTool = binding.transport === "local-cli"
|
|
3950
|
+
? parseLocalCliToolIdentityV1(binding.tool)
|
|
3951
|
+
: undefined;
|
|
3952
|
+
requireBoundedNonEmptyArray(
|
|
3953
|
+
binding.operations,
|
|
3954
|
+
`provider plugin surface ${binding.surfaceId} operations`,
|
|
3955
|
+
MAX_PROVIDER_PLUGIN_OPERATIONS_PER_BINDING,
|
|
3956
|
+
);
|
|
3957
|
+
const operations = [...binding.operations].map((operation) =>
|
|
3958
|
+
freezeOperation(operation, binding.transport, sourceKind));
|
|
3959
|
+
operations.sort((left, right) =>
|
|
3960
|
+
left.name.localeCompare(right.name) || left.contractVersion - right.contractVersion);
|
|
3961
|
+
const operationKeys = operations.map((operation) =>
|
|
3962
|
+
`${operation.name}@${operation.contractVersion}`);
|
|
3963
|
+
if (new Set(operationKeys).size !== operationKeys.length) {
|
|
3964
|
+
throw new Error(`provider plugin surface ${binding.surfaceId} repeats an exact operation contract`);
|
|
3965
|
+
}
|
|
3966
|
+
const messaging = freezeProviderPluginMessaging(
|
|
3967
|
+
binding.messaging,
|
|
3968
|
+
operations,
|
|
3969
|
+
binding.surfaceId,
|
|
3970
|
+
);
|
|
3971
|
+
if (
|
|
3972
|
+
messaging?.action.state === "supported"
|
|
3973
|
+
&& binding.transport === "provider-api"
|
|
3974
|
+
) {
|
|
3975
|
+
throw new Error(
|
|
3976
|
+
`provider plugin surface ${binding.surfaceId} messaging actions require a cleanup-qualified session or local CLI transport`,
|
|
3977
|
+
);
|
|
3978
|
+
}
|
|
3979
|
+
if (
|
|
3980
|
+
typeof binding.subject !== "object"
|
|
3981
|
+
|| binding.subject === null
|
|
3982
|
+
|| typeof binding.subject.format !== "string"
|
|
3983
|
+
|| binding.subject.format.length < 1
|
|
3984
|
+
|| typeof binding.subject.matches !== "function"
|
|
3985
|
+
) {
|
|
3986
|
+
throw new Error(`provider plugin surface ${binding.surfaceId} must declare a subject matcher`);
|
|
3987
|
+
}
|
|
3988
|
+
requireExactKeys(binding.subject, ["format", "matches"], "provider plugin subject");
|
|
3989
|
+
if (
|
|
3990
|
+
typeof binding.runtime !== "object"
|
|
3991
|
+
|| binding.runtime === null
|
|
3992
|
+
|| typeof binding.runtime.loadRuntime !== "function"
|
|
3993
|
+
) {
|
|
3994
|
+
throw new Error(`provider plugin surface ${binding.surfaceId} must declare a lazy runtime loader`);
|
|
3995
|
+
}
|
|
3996
|
+
const common = {
|
|
3997
|
+
surfaceId: binding.surfaceId,
|
|
3998
|
+
origin: binding.origin,
|
|
3999
|
+
manifestOrigins: Object.freeze(manifestOrigins),
|
|
4000
|
+
protectedHostnameFamilies: Object.freeze(
|
|
4001
|
+
protectedHostnameFamilies.sort(),
|
|
4002
|
+
),
|
|
4003
|
+
authKinds: Object.freeze(acceptedAuthKinds),
|
|
4004
|
+
...(messaging === undefined ? {} : { messaging }),
|
|
4005
|
+
};
|
|
4006
|
+
if (binding.transport === "provider-api") {
|
|
4007
|
+
requireExactKeys(binding.runtime, ["loadRuntime"], "provider plugin runtime hooks");
|
|
4008
|
+
const loadRuntime = binding.runtime.loadRuntime;
|
|
4009
|
+
const result: ProviderApiPluginBindingV1 = Object.freeze({
|
|
4010
|
+
...common,
|
|
4011
|
+
transport: "provider-api",
|
|
4012
|
+
runtimeOrigins: Object.freeze(runtimeOrigins),
|
|
4013
|
+
operations: Object.freeze(operations) as readonly ProviderApiPluginOperationV1[],
|
|
4014
|
+
subject: Object.freeze({ ...binding.subject }),
|
|
4015
|
+
loadRuntime,
|
|
4016
|
+
execute: async (context: ProviderActionContext) =>
|
|
4017
|
+
(await loadRuntime()).execute(context),
|
|
4018
|
+
...(messaging?.action.state === "supported"
|
|
4019
|
+
? {
|
|
4020
|
+
executeMessagingPart: async (
|
|
4021
|
+
operation: string,
|
|
4022
|
+
input: OperationInput,
|
|
4023
|
+
auth: GhostgetAuth,
|
|
4024
|
+
attempt: ProviderPluginMessagingActionAttemptV1,
|
|
4025
|
+
) => {
|
|
4026
|
+
const hook = (await loadRuntime()).executeMessagingPart;
|
|
4027
|
+
if (hook === undefined) {
|
|
4028
|
+
throw new Error(
|
|
4029
|
+
`provider plugin surface ${binding.surfaceId} declared messaging actions without a runtime hook`,
|
|
4030
|
+
);
|
|
4031
|
+
}
|
|
4032
|
+
return hook(operation, input, auth, attempt);
|
|
4033
|
+
},
|
|
4034
|
+
}
|
|
4035
|
+
: {}),
|
|
4036
|
+
});
|
|
4037
|
+
return result;
|
|
4038
|
+
}
|
|
4039
|
+
if (binding.transport === "local-cli") {
|
|
4040
|
+
requireExactKeys(binding.runtime, ["loadRuntime"], "local CLI plugin runtime hooks");
|
|
4041
|
+
const loadRuntime = binding.runtime.loadRuntime;
|
|
4042
|
+
const reconciles = operations.some(
|
|
4043
|
+
(operation) => operation.reconciliation !== undefined,
|
|
4044
|
+
);
|
|
4045
|
+
const reconcile: NonNullable<LocalCliPluginRuntimeV1["reconcile"]> = async (
|
|
4046
|
+
operationName,
|
|
4047
|
+
input,
|
|
4048
|
+
auth,
|
|
4049
|
+
context,
|
|
4050
|
+
options,
|
|
4051
|
+
) => {
|
|
4052
|
+
const selectedOperation = operations.find((operation) =>
|
|
4053
|
+
operation.name === operationName
|
|
4054
|
+
&& operation.reconciliation !== undefined);
|
|
4055
|
+
if (selectedOperation === undefined) {
|
|
4056
|
+
throw new Error(
|
|
4057
|
+
`provider plugin surface ${binding.surfaceId} has no reconciliation contract for ${operationName}`,
|
|
4058
|
+
);
|
|
4059
|
+
}
|
|
4060
|
+
const reconciliationKind = selectedOperation.reconciliation?.kind;
|
|
4061
|
+
const reconciliationContext =
|
|
4062
|
+
reconciliationKind === "provider-accepted-target-presence"
|
|
4063
|
+
|| reconciliationKind === "provider-bound-target-desired-state"
|
|
4064
|
+
? (() => {
|
|
4065
|
+
const parsed = parseProviderPluginReconciliationContextV1(context);
|
|
4066
|
+
if (parsed.kind !== reconciliationKind) {
|
|
4067
|
+
throw new Error(
|
|
4068
|
+
`provider plugin surface ${binding.surfaceId} reconciliation target context kind changed`,
|
|
4069
|
+
);
|
|
4070
|
+
}
|
|
4071
|
+
return parsed;
|
|
4072
|
+
})()
|
|
4073
|
+
: (() => {
|
|
4074
|
+
if (context !== undefined) {
|
|
4075
|
+
throw new Error(
|
|
4076
|
+
`provider plugin surface ${binding.surfaceId} boolean reconciliation does not accept target context`,
|
|
4077
|
+
);
|
|
4078
|
+
}
|
|
4079
|
+
return undefined;
|
|
4080
|
+
})();
|
|
4081
|
+
const hook = (await loadRuntime()).reconcile;
|
|
4082
|
+
if (hook === undefined) {
|
|
4083
|
+
throw new Error(
|
|
4084
|
+
`provider plugin surface ${binding.surfaceId} declared reconciliation without a runtime hook`,
|
|
4085
|
+
);
|
|
4086
|
+
}
|
|
4087
|
+
const value = await hook(
|
|
4088
|
+
operationName,
|
|
4089
|
+
input,
|
|
4090
|
+
auth,
|
|
4091
|
+
reconciliationContext,
|
|
4092
|
+
options,
|
|
4093
|
+
);
|
|
4094
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
4095
|
+
throw new Error("provider plugin reconciliation returned an invalid readback");
|
|
4096
|
+
}
|
|
4097
|
+
requireExactKeys(
|
|
4098
|
+
value,
|
|
4099
|
+
["actualState", "reason"],
|
|
4100
|
+
"provider plugin reconciliation readback",
|
|
4101
|
+
);
|
|
4102
|
+
if (
|
|
4103
|
+
typeof value.actualState !== "boolean"
|
|
4104
|
+
|| typeof value.reason !== "string"
|
|
4105
|
+
|| value.reason.length < 1
|
|
4106
|
+
|| value.reason.length > 200
|
|
4107
|
+
) {
|
|
4108
|
+
throw new Error("provider plugin reconciliation returned an invalid readback");
|
|
4109
|
+
}
|
|
4110
|
+
return Object.freeze({
|
|
4111
|
+
actualState: value.actualState,
|
|
4112
|
+
reason: value.reason,
|
|
4113
|
+
});
|
|
4114
|
+
};
|
|
4115
|
+
const result: LocalCliPluginBindingV1 = Object.freeze({
|
|
4116
|
+
...common,
|
|
4117
|
+
transport: "local-cli",
|
|
4118
|
+
tool: localCliTool!,
|
|
4119
|
+
operations: Object.freeze(operations) as readonly LocalCliPluginOperationV1[],
|
|
4120
|
+
loadRuntime,
|
|
4121
|
+
subject: Object.freeze({
|
|
4122
|
+
...binding.subject,
|
|
4123
|
+
probe: async (
|
|
4124
|
+
auth: GhostgetAuth,
|
|
4125
|
+
options?: ProviderPluginSubjectProbeOptionsV1,
|
|
4126
|
+
) => {
|
|
4127
|
+
const subject = await (await loadRuntime()).probe(auth, options);
|
|
4128
|
+
if (
|
|
4129
|
+
typeof subject !== "string"
|
|
4130
|
+
|| !binding.subject.matches(subject)
|
|
4131
|
+
) {
|
|
4132
|
+
throw new Error(
|
|
4133
|
+
`provider plugin surface ${binding.surfaceId} returned a subject outside ${binding.subject.format}`,
|
|
4134
|
+
);
|
|
4135
|
+
}
|
|
4136
|
+
return subject;
|
|
4137
|
+
},
|
|
4138
|
+
}),
|
|
4139
|
+
inspect: async (environment, options) => {
|
|
4140
|
+
let artifact: LocalCliToolArtifactIdentityV1;
|
|
4141
|
+
try {
|
|
4142
|
+
artifact = localCliToolArtifactForCurrentRuntime(localCliTool!);
|
|
4143
|
+
} catch {
|
|
4144
|
+
return Object.freeze({
|
|
4145
|
+
ready: false,
|
|
4146
|
+
platform: process.platform,
|
|
4147
|
+
arch: process.arch,
|
|
4148
|
+
version: null,
|
|
4149
|
+
executableSha256: null,
|
|
4150
|
+
reason: `unsupported-runtime:${process.platform}/${process.arch}`,
|
|
4151
|
+
});
|
|
4152
|
+
}
|
|
4153
|
+
const cleanupFilesystem = inspectLocalCliCleanupFilesystemReadiness();
|
|
4154
|
+
if (!cleanupFilesystem.ready) {
|
|
4155
|
+
return Object.freeze({
|
|
4156
|
+
ready: false,
|
|
4157
|
+
platform: process.platform,
|
|
4158
|
+
arch: process.arch,
|
|
4159
|
+
version: null,
|
|
4160
|
+
executableSha256: null,
|
|
4161
|
+
reason: cleanupFilesystem.reason,
|
|
4162
|
+
});
|
|
4163
|
+
}
|
|
4164
|
+
const [ready, platform, arch, version, executableSha256, reason] =
|
|
4165
|
+
snapshotExactEnumerableDataProperties(
|
|
4166
|
+
await (await loadRuntime()).inspect(environment, options),
|
|
4167
|
+
[
|
|
4168
|
+
"ready",
|
|
4169
|
+
"platform",
|
|
4170
|
+
"arch",
|
|
4171
|
+
"version",
|
|
4172
|
+
"executableSha256",
|
|
4173
|
+
"reason",
|
|
4174
|
+
],
|
|
4175
|
+
"local CLI runtime inspection status",
|
|
4176
|
+
);
|
|
4177
|
+
if (
|
|
4178
|
+
typeof ready !== "boolean"
|
|
4179
|
+
|| typeof platform !== "string"
|
|
4180
|
+
|| platform !== process.platform
|
|
4181
|
+
|| typeof arch !== "string"
|
|
4182
|
+
|| arch !== process.arch
|
|
4183
|
+
|| (version !== null && (
|
|
4184
|
+
typeof version !== "string"
|
|
4185
|
+
|| version.length < 1
|
|
4186
|
+
|| version.length > 128
|
|
4187
|
+
|| /[\u0000-\u001f\u007f-\u009f]/u.test(version)
|
|
4188
|
+
|| hasUnpairedSurrogate(version)
|
|
4189
|
+
))
|
|
4190
|
+
|| (executableSha256 !== null && (
|
|
4191
|
+
typeof executableSha256 !== "string"
|
|
4192
|
+
|| !/^[a-f0-9]{64}$/u.test(executableSha256)
|
|
4193
|
+
))
|
|
4194
|
+
|| (reason !== null && (
|
|
4195
|
+
typeof reason !== "string"
|
|
4196
|
+
|| reason.length < 1
|
|
4197
|
+
|| reason.length > 500
|
|
4198
|
+
|| /[\u0000-\u001f\u007f-\u009f]/u.test(reason)
|
|
4199
|
+
|| hasUnpairedSurrogate(reason)
|
|
4200
|
+
))
|
|
4201
|
+
|| (ready && (
|
|
4202
|
+
version !== localCliTool!.version
|
|
4203
|
+
|| executableSha256 !== artifact.executableSha256
|
|
4204
|
+
|| reason !== null
|
|
4205
|
+
))
|
|
4206
|
+
|| (!ready && reason === null)
|
|
4207
|
+
) {
|
|
4208
|
+
throw new Error("local CLI runtime inspection returned an invalid status");
|
|
4209
|
+
}
|
|
4210
|
+
return Object.freeze({
|
|
4211
|
+
ready,
|
|
4212
|
+
platform,
|
|
4213
|
+
arch,
|
|
4214
|
+
version,
|
|
4215
|
+
executableSha256,
|
|
4216
|
+
reason,
|
|
4217
|
+
});
|
|
4218
|
+
},
|
|
4219
|
+
execute: async (manifest, recipe, input, auth, options) =>
|
|
4220
|
+
(await loadRuntime()).execute(manifest, recipe, input, auth, options),
|
|
4221
|
+
...(messaging?.action.state === "supported"
|
|
4222
|
+
? {
|
|
4223
|
+
executeMessagingPart: async (
|
|
4224
|
+
operation: string,
|
|
4225
|
+
input: OperationInput,
|
|
4226
|
+
auth: GhostgetAuth,
|
|
4227
|
+
attempt: ProviderPluginMessagingActionAttemptV1,
|
|
4228
|
+
) => {
|
|
4229
|
+
const hook = (await loadRuntime()).executeMessagingPart;
|
|
4230
|
+
if (hook === undefined) {
|
|
4231
|
+
throw new Error(
|
|
4232
|
+
`provider plugin surface ${binding.surfaceId} declared messaging actions without a runtime hook`,
|
|
4233
|
+
);
|
|
4234
|
+
}
|
|
4235
|
+
return hook(operation, input, auth, attempt);
|
|
4236
|
+
},
|
|
4237
|
+
}
|
|
4238
|
+
: {}),
|
|
4239
|
+
...(reconciles ? { reconcile } : {}),
|
|
4240
|
+
});
|
|
4241
|
+
return result;
|
|
4242
|
+
}
|
|
4243
|
+
requireExactKeys(binding.runtime, ["loadRuntime"], "web-session plugin runtime hooks");
|
|
4244
|
+
const loadRuntime = binding.runtime.loadRuntime;
|
|
4245
|
+
const execute: WebSessionOperationExecutor = async (
|
|
4246
|
+
manifest,
|
|
4247
|
+
recipe,
|
|
4248
|
+
input,
|
|
4249
|
+
auth,
|
|
4250
|
+
options,
|
|
4251
|
+
) => (await loadRuntime()).execute(manifest, recipe, input, auth, options);
|
|
4252
|
+
const hasPublicOperations = operations.some(
|
|
4253
|
+
(operation) => operation.access === "public",
|
|
4254
|
+
);
|
|
4255
|
+
const executePublic: PublicWebSessionOperationExecutor = async (
|
|
4256
|
+
manifest,
|
|
4257
|
+
recipe,
|
|
4258
|
+
input,
|
|
4259
|
+
options,
|
|
4260
|
+
) => {
|
|
4261
|
+
const runtime = await loadRuntime();
|
|
4262
|
+
if (runtime.executePublic === undefined) {
|
|
4263
|
+
throw new Error(
|
|
4264
|
+
`provider plugin surface ${binding.surfaceId} is missing its reviewed public runtime hook`,
|
|
4265
|
+
);
|
|
4266
|
+
}
|
|
4267
|
+
return runtime.executePublic(manifest, recipe, input, options);
|
|
4268
|
+
};
|
|
4269
|
+
const reconciles = operations.some(
|
|
4270
|
+
(operation) => operation.reconciliation !== undefined,
|
|
4271
|
+
);
|
|
4272
|
+
const reconcile: NonNullable<WebSessionPluginRuntimeV1["reconcile"]> = async (
|
|
4273
|
+
operationName,
|
|
4274
|
+
input,
|
|
4275
|
+
auth,
|
|
4276
|
+
context,
|
|
4277
|
+
) => {
|
|
4278
|
+
const selectedOperation = operations.find((operation) =>
|
|
4279
|
+
operation.name === operationName
|
|
4280
|
+
&& operation.reconciliation !== undefined);
|
|
4281
|
+
if (selectedOperation === undefined) {
|
|
4282
|
+
throw new Error(
|
|
4283
|
+
`provider plugin surface ${binding.surfaceId} has no reconciliation contract for ${operationName}`,
|
|
4284
|
+
);
|
|
4285
|
+
}
|
|
4286
|
+
const reconciliationKind = selectedOperation.reconciliation?.kind;
|
|
4287
|
+
const reconciliationContext =
|
|
4288
|
+
reconciliationKind === "provider-accepted-target-presence"
|
|
4289
|
+
|| reconciliationKind === "provider-bound-target-desired-state"
|
|
4290
|
+
? (() => {
|
|
4291
|
+
const parsed = parseProviderPluginReconciliationContextV1(context);
|
|
4292
|
+
if (parsed.kind !== reconciliationKind) {
|
|
4293
|
+
throw new Error(
|
|
4294
|
+
`provider plugin surface ${binding.surfaceId} reconciliation target context kind changed`,
|
|
4295
|
+
);
|
|
4296
|
+
}
|
|
4297
|
+
return parsed;
|
|
4298
|
+
})()
|
|
4299
|
+
: (() => {
|
|
4300
|
+
if (context !== undefined) {
|
|
4301
|
+
throw new Error(
|
|
4302
|
+
`provider plugin surface ${binding.surfaceId} boolean reconciliation does not accept target context`,
|
|
4303
|
+
);
|
|
4304
|
+
}
|
|
4305
|
+
return undefined;
|
|
4306
|
+
})();
|
|
4307
|
+
const hook = (await loadRuntime()).reconcile;
|
|
4308
|
+
if (hook === undefined) {
|
|
4309
|
+
throw new Error(
|
|
4310
|
+
`provider plugin surface ${binding.surfaceId} declared reconciliation without a runtime hook`,
|
|
4311
|
+
);
|
|
4312
|
+
}
|
|
4313
|
+
const value = await hook(
|
|
4314
|
+
operationName,
|
|
4315
|
+
input,
|
|
4316
|
+
auth,
|
|
4317
|
+
reconciliationContext,
|
|
4318
|
+
);
|
|
4319
|
+
if (
|
|
4320
|
+
typeof value !== "object"
|
|
4321
|
+
|| value === null
|
|
4322
|
+
|| Array.isArray(value)
|
|
4323
|
+
) {
|
|
4324
|
+
throw new Error("provider plugin reconciliation returned an invalid readback");
|
|
4325
|
+
}
|
|
4326
|
+
requireExactKeys(
|
|
4327
|
+
value,
|
|
4328
|
+
["actualState", "reason"],
|
|
4329
|
+
"provider plugin reconciliation readback",
|
|
4330
|
+
);
|
|
4331
|
+
if (
|
|
4332
|
+
typeof value.actualState !== "boolean"
|
|
4333
|
+
|| typeof value.reason !== "string"
|
|
4334
|
+
|| value.reason.length < 1
|
|
4335
|
+
|| value.reason.length > 200
|
|
4336
|
+
) {
|
|
4337
|
+
throw new Error("provider plugin reconciliation returned an invalid readback");
|
|
4338
|
+
}
|
|
4339
|
+
return Object.freeze({
|
|
4340
|
+
actualState: value.actualState,
|
|
4341
|
+
reason: value.reason,
|
|
4342
|
+
});
|
|
4343
|
+
};
|
|
4344
|
+
let linkedDeviceLifecycle:
|
|
4345
|
+
| ProviderPluginLinkedDeviceLifecycleRuntimeV1
|
|
4346
|
+
| undefined;
|
|
4347
|
+
if (binding.transport === "linked-device") {
|
|
4348
|
+
const declaration = binding.linkedDeviceLifecycle;
|
|
4349
|
+
if (declaration !== undefined) {
|
|
4350
|
+
requireExactKeys(
|
|
4351
|
+
declaration,
|
|
4352
|
+
["inspect", "pair", "syncOnce"],
|
|
4353
|
+
"linked-device plugin lifecycle",
|
|
4354
|
+
);
|
|
4355
|
+
if (
|
|
4356
|
+
declaration.inspect !== true
|
|
4357
|
+
|| declaration.pair !== true
|
|
4358
|
+
|| declaration.syncOnce !== true
|
|
4359
|
+
) {
|
|
4360
|
+
throw new Error(
|
|
4361
|
+
`provider plugin surface ${binding.surfaceId} has an invalid linked-device lifecycle declaration`,
|
|
4362
|
+
);
|
|
4363
|
+
}
|
|
4364
|
+
const requireLifecycle = async ():
|
|
4365
|
+
Promise<ProviderPluginLinkedDeviceLifecycleRuntimeV1> => {
|
|
4366
|
+
const lifecycle = (await loadRuntime()).linkedDeviceLifecycle;
|
|
4367
|
+
if (lifecycle === undefined) {
|
|
4368
|
+
throw new Error(
|
|
4369
|
+
`provider plugin surface ${binding.surfaceId} declared linked-device lifecycle capabilities without runtime hooks`,
|
|
4370
|
+
);
|
|
4371
|
+
}
|
|
4372
|
+
return lifecycle;
|
|
4373
|
+
};
|
|
4374
|
+
linkedDeviceLifecycle = Object.freeze({
|
|
4375
|
+
inspect: async (environment) => {
|
|
4376
|
+
const value = await (await requireLifecycle()).inspect(environment);
|
|
4377
|
+
if (
|
|
4378
|
+
typeof value !== "object"
|
|
4379
|
+
|| value === null
|
|
4380
|
+
|| Array.isArray(value)
|
|
4381
|
+
) {
|
|
4382
|
+
throw new Error("linked-device lifecycle inspection returned invalid status");
|
|
4383
|
+
}
|
|
4384
|
+
const keys = [
|
|
4385
|
+
"ready",
|
|
4386
|
+
"implementation",
|
|
4387
|
+
"version",
|
|
4388
|
+
"integrity",
|
|
4389
|
+
...(value.setupCommand === undefined ? [] : ["setupCommand"]),
|
|
4390
|
+
];
|
|
4391
|
+
requireExactKeys(
|
|
4392
|
+
value,
|
|
4393
|
+
keys,
|
|
4394
|
+
"linked-device lifecycle inspection status",
|
|
4395
|
+
);
|
|
4396
|
+
if (
|
|
4397
|
+
typeof value.ready !== "boolean"
|
|
4398
|
+
|| typeof value.implementation !== "string"
|
|
4399
|
+
|| value.implementation.length < 1
|
|
4400
|
+
|| typeof value.version !== "string"
|
|
4401
|
+
|| value.version.length < 1
|
|
4402
|
+
|| typeof value.integrity !== "string"
|
|
4403
|
+
|| value.integrity.length < 1
|
|
4404
|
+
|| (
|
|
4405
|
+
value.setupCommand !== undefined
|
|
4406
|
+
&& (
|
|
4407
|
+
typeof value.setupCommand !== "string"
|
|
4408
|
+
|| value.setupCommand.length < 1
|
|
4409
|
+
)
|
|
4410
|
+
)
|
|
4411
|
+
) {
|
|
4412
|
+
throw new Error("linked-device lifecycle inspection returned invalid status");
|
|
4413
|
+
}
|
|
4414
|
+
return Object.freeze({ ...value });
|
|
4415
|
+
},
|
|
4416
|
+
pair: async (auth, options) => {
|
|
4417
|
+
const subject = await (await requireLifecycle()).pair(auth, options);
|
|
4418
|
+
if (
|
|
4419
|
+
typeof subject !== "string"
|
|
4420
|
+
|| !binding.subject.matches(subject)
|
|
4421
|
+
) {
|
|
4422
|
+
throw new Error("linked-device lifecycle pairing returned an invalid subject");
|
|
4423
|
+
}
|
|
4424
|
+
return subject;
|
|
4425
|
+
},
|
|
4426
|
+
syncOnce: async (auth, options) => {
|
|
4427
|
+
const value = await (await requireLifecycle()).syncOnce(auth, options);
|
|
4428
|
+
if (
|
|
4429
|
+
typeof value !== "object"
|
|
4430
|
+
|| value === null
|
|
4431
|
+
|| Array.isArray(value)
|
|
4432
|
+
) {
|
|
4433
|
+
throw new Error("linked-device lifecycle sync returned an invalid result");
|
|
4434
|
+
}
|
|
4435
|
+
requireExactKeys(
|
|
4436
|
+
value,
|
|
4437
|
+
["itemsStored", "projection", "emitsProtocolAcknowledgements"],
|
|
4438
|
+
"linked-device lifecycle sync result",
|
|
4439
|
+
);
|
|
4440
|
+
if (
|
|
4441
|
+
!Number.isSafeInteger(value.itemsStored)
|
|
4442
|
+
|| value.itemsStored < 0
|
|
4443
|
+
|| typeof value.projection !== "string"
|
|
4444
|
+
|| value.projection.length < 1
|
|
4445
|
+
|| value.emitsProtocolAcknowledgements !== true
|
|
4446
|
+
) {
|
|
4447
|
+
throw new Error("linked-device lifecycle sync returned an invalid result");
|
|
4448
|
+
}
|
|
4449
|
+
return Object.freeze({ ...value });
|
|
4450
|
+
},
|
|
4451
|
+
});
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4454
|
+
const result: WebSessionApiPluginBindingV1 | LinkedDevicePluginBindingV1 =
|
|
4455
|
+
Object.freeze({
|
|
4456
|
+
...common,
|
|
4457
|
+
transport: binding.transport,
|
|
4458
|
+
operations: Object.freeze(operations) as readonly WebSessionPluginOperationV1[],
|
|
4459
|
+
loadRuntime,
|
|
4460
|
+
subject: Object.freeze({
|
|
4461
|
+
...binding.subject,
|
|
4462
|
+
probe: async (
|
|
4463
|
+
auth: GhostgetAuth,
|
|
4464
|
+
options?: ProviderPluginSubjectProbeOptionsV1,
|
|
4465
|
+
) => {
|
|
4466
|
+
const subject = await (await loadRuntime()).probe(auth, options);
|
|
4467
|
+
if (
|
|
4468
|
+
typeof subject !== "string"
|
|
4469
|
+
|| !binding.subject.matches(subject)
|
|
4470
|
+
) {
|
|
4471
|
+
throw new Error(
|
|
4472
|
+
`provider plugin surface ${binding.surfaceId} returned a subject outside ${binding.subject.format}`,
|
|
4473
|
+
);
|
|
4474
|
+
}
|
|
4475
|
+
return subject;
|
|
4476
|
+
},
|
|
4477
|
+
}),
|
|
4478
|
+
execute,
|
|
4479
|
+
...(messaging?.action.state === "supported"
|
|
4480
|
+
? {
|
|
4481
|
+
executeMessagingPart: async (
|
|
4482
|
+
operation: string,
|
|
4483
|
+
input: OperationInput,
|
|
4484
|
+
auth: GhostgetAuth,
|
|
4485
|
+
attempt: ProviderPluginMessagingActionAttemptV1,
|
|
4486
|
+
) => {
|
|
4487
|
+
const hook = (await loadRuntime()).executeMessagingPart;
|
|
4488
|
+
if (hook === undefined) {
|
|
4489
|
+
throw new Error(
|
|
4490
|
+
`provider plugin surface ${binding.surfaceId} declared messaging actions without a runtime hook`,
|
|
4491
|
+
);
|
|
4492
|
+
}
|
|
4493
|
+
return hook(operation, input, auth, attempt);
|
|
4494
|
+
},
|
|
4495
|
+
}
|
|
4496
|
+
: {}),
|
|
4497
|
+
...(hasPublicOperations ? { executePublic } : {}),
|
|
4498
|
+
...(reconciles ? { reconcile } : {}),
|
|
4499
|
+
...(linkedDeviceLifecycle === undefined ? {} : { linkedDeviceLifecycle }),
|
|
4500
|
+
});
|
|
4501
|
+
return result;
|
|
4502
|
+
}
|
|
4503
|
+
|
|
4504
|
+
/**
|
|
4505
|
+
* Validate and deeply freeze one trusted source plugin definition.
|
|
4506
|
+
*
|
|
4507
|
+
* V1 intentionally accepts only statically imported metadata. Runtime hooks
|
|
4508
|
+
* must remain behind a lazy loader; executable plugin code is not sandboxed.
|
|
4509
|
+
*/
|
|
4510
|
+
export function defineProviderPlugin(
|
|
4511
|
+
plugin: ProviderPluginDefinitionV1,
|
|
4512
|
+
): ProviderPluginV1 {
|
|
4513
|
+
requireExactKeys(
|
|
4514
|
+
plugin,
|
|
4515
|
+
[
|
|
4516
|
+
"apiVersion",
|
|
4517
|
+
"id",
|
|
4518
|
+
"version",
|
|
4519
|
+
"displayName",
|
|
4520
|
+
"sourceKind",
|
|
4521
|
+
"implementationSources",
|
|
4522
|
+
"bindings",
|
|
4523
|
+
],
|
|
4524
|
+
"provider plugin",
|
|
4525
|
+
);
|
|
4526
|
+
if (plugin.apiVersion !== PROVIDER_PLUGIN_API_VERSION) {
|
|
4527
|
+
throw new Error(`provider plugin ${String(plugin.id)} uses unsupported API version ${String(plugin.apiVersion)}`);
|
|
4528
|
+
}
|
|
4529
|
+
if (!isProviderPluginId(plugin.id)) {
|
|
4530
|
+
throw new Error("provider plugin ID must be strict lowercase kebab-case with at most 63 characters");
|
|
4531
|
+
}
|
|
4532
|
+
if (!pluginVersionPattern.test(plugin.version)) {
|
|
4533
|
+
throw new Error(`provider plugin ${plugin.id} must declare a semantic version`);
|
|
4534
|
+
}
|
|
4535
|
+
if (typeof plugin.displayName !== "string" || plugin.displayName.length < 1 || plugin.displayName.length > 100) {
|
|
4536
|
+
throw new Error(`provider plugin ${plugin.id} must declare a display name`);
|
|
4537
|
+
}
|
|
4538
|
+
if (plugin.sourceKind !== "built-in" && plugin.sourceKind !== "source") {
|
|
4539
|
+
throw new Error(`provider plugin ${plugin.id} has an unsupported source kind`);
|
|
4540
|
+
}
|
|
4541
|
+
requireBoundedNonEmptyArray(
|
|
4542
|
+
plugin.implementationSources,
|
|
4543
|
+
`provider plugin ${plugin.id} implementationSources`,
|
|
4544
|
+
MAX_PROVIDER_PLUGIN_IMPLEMENTATION_SOURCES,
|
|
4545
|
+
);
|
|
4546
|
+
const sources = [...plugin.implementationSources].map((source) => {
|
|
4547
|
+
requireExactKeys(source, ["label", "url"], `provider plugin ${plugin.id} implementation source`);
|
|
4548
|
+
if (!sourceLabelPattern.test(source.label) || source.label.includes("..") || source.label.startsWith("/")) {
|
|
4549
|
+
throw new Error(`provider plugin ${plugin.id} has an unsafe implementation source label`);
|
|
4550
|
+
}
|
|
4551
|
+
const localFileProtocol = ["file", ":"].join("");
|
|
4552
|
+
if (!(source.url instanceof URL) || source.url.protocol !== localFileProtocol) {
|
|
4553
|
+
throw new Error(`provider plugin ${plugin.id} implementation sources must be file URLs`);
|
|
4554
|
+
}
|
|
4555
|
+
let stats: ReturnType<typeof lstatSync>;
|
|
4556
|
+
let realPath: string;
|
|
4557
|
+
try {
|
|
4558
|
+
const path = fileURLToPath(source.url);
|
|
4559
|
+
stats = lstatSync(path);
|
|
4560
|
+
realPath = realpathSync(path);
|
|
4561
|
+
} catch {
|
|
4562
|
+
throw new Error(`provider plugin ${plugin.id} implementation source ${source.label} is unreadable`);
|
|
4563
|
+
}
|
|
4564
|
+
const relativePath = relative(providerPluginSourceRoot, realPath);
|
|
4565
|
+
if (
|
|
4566
|
+
stats.isSymbolicLink()
|
|
4567
|
+
|| !stats.isFile()
|
|
4568
|
+
|| relativePath === ""
|
|
4569
|
+
|| relativePath.startsWith("..")
|
|
4570
|
+
|| isAbsolute(relativePath)
|
|
4571
|
+
) {
|
|
4572
|
+
throw new Error(
|
|
4573
|
+
`provider plugin ${plugin.id} implementation source ${source.label} must be a regular file under the Ghostget source root`,
|
|
4574
|
+
);
|
|
4575
|
+
}
|
|
4576
|
+
return Object.freeze({ label: source.label, path: realPath });
|
|
4577
|
+
});
|
|
4578
|
+
sources.sort((left, right) => left.label.localeCompare(right.label));
|
|
4579
|
+
if (new Set(sources.map((source) => source.label)).size !== sources.length) {
|
|
4580
|
+
throw new Error(`provider plugin ${plugin.id} repeats an implementation source label`);
|
|
4581
|
+
}
|
|
4582
|
+
if (!sources.some((source) => source.label === "plugin.ts")) {
|
|
4583
|
+
throw new Error(`provider plugin ${plugin.id} must bind its plugin.ts implementation source`);
|
|
4584
|
+
}
|
|
4585
|
+
requireBoundedNonEmptyArray(
|
|
4586
|
+
plugin.bindings,
|
|
4587
|
+
`provider plugin ${plugin.id} bindings`,
|
|
4588
|
+
MAX_PROVIDER_PLUGIN_BINDINGS,
|
|
4589
|
+
);
|
|
4590
|
+
let operationCount = 0;
|
|
4591
|
+
for (const binding of plugin.bindings) {
|
|
4592
|
+
requireBoundedNonEmptyArray(
|
|
4593
|
+
binding.operations,
|
|
4594
|
+
`provider plugin surface ${String(binding.surfaceId)} operations`,
|
|
4595
|
+
MAX_PROVIDER_PLUGIN_OPERATIONS_PER_BINDING,
|
|
4596
|
+
);
|
|
4597
|
+
operationCount += binding.operations.length;
|
|
4598
|
+
if (operationCount > MAX_PROVIDER_PLUGIN_OPERATIONS) {
|
|
4599
|
+
throw new Error(
|
|
4600
|
+
`provider plugin ${plugin.id} may declare at most ${MAX_PROVIDER_PLUGIN_OPERATIONS} operations`,
|
|
4601
|
+
);
|
|
4602
|
+
}
|
|
4603
|
+
}
|
|
4604
|
+
const bindings = [...plugin.bindings].map((binding) =>
|
|
4605
|
+
freezeBinding(binding, plugin.sourceKind));
|
|
4606
|
+
for (const binding of bindings) {
|
|
4607
|
+
if (!providerPluginLazyRuntimeLoaders.has(binding.loadRuntime)) {
|
|
4608
|
+
throw new Error(
|
|
4609
|
+
`provider plugin ${plugin.id} runtime must use the branded lazy runtime helper`,
|
|
4610
|
+
);
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4613
|
+
bindings.sort((left, right) =>
|
|
4614
|
+
left.transport.localeCompare(right.transport) || left.surfaceId.localeCompare(right.surfaceId));
|
|
4615
|
+
const result = Object.freeze({
|
|
4616
|
+
...plugin,
|
|
4617
|
+
implementationSources: Object.freeze(sources),
|
|
4618
|
+
bindings: Object.freeze(bindings),
|
|
4619
|
+
});
|
|
4620
|
+
const evaluationIdentity = captureProviderPluginEvaluationIdentity(
|
|
4621
|
+
plugin.id,
|
|
4622
|
+
sources,
|
|
4623
|
+
);
|
|
4624
|
+
providerPluginEvaluationSourceDigests.set(
|
|
4625
|
+
result,
|
|
4626
|
+
evaluationIdentity.sourceDigests,
|
|
4627
|
+
);
|
|
4628
|
+
providerPluginEvaluationInstalledPackageDigests.set(
|
|
4629
|
+
result,
|
|
4630
|
+
evaluationIdentity.installedPackageDigests,
|
|
4631
|
+
);
|
|
4632
|
+
validatedProviderPlugins.add(result);
|
|
4633
|
+
return result;
|
|
4634
|
+
}
|
|
4635
|
+
|
|
4636
|
+
function freezePortableManifestValue<T>(value: T): T {
|
|
4637
|
+
if (typeof value !== "object" || value === null || Object.isFrozen(value)) {
|
|
4638
|
+
return value;
|
|
4639
|
+
}
|
|
4640
|
+
if (Array.isArray(value)) {
|
|
4641
|
+
for (const entry of value) freezePortableManifestValue(entry);
|
|
4642
|
+
return Object.freeze(value);
|
|
4643
|
+
}
|
|
4644
|
+
for (const entry of Object.values(value as Record<string, unknown>)) {
|
|
4645
|
+
freezePortableManifestValue(entry);
|
|
4646
|
+
}
|
|
4647
|
+
return Object.freeze(value);
|
|
4648
|
+
}
|
|
4649
|
+
|
|
4650
|
+
/**
|
|
4651
|
+
* Project one already verified, content-addressed portable package into the
|
|
4652
|
+
* same logical registry shape as trusted source plugins.
|
|
4653
|
+
*
|
|
4654
|
+
* This does not trust package code in-process. Every runtime hook supplied by
|
|
4655
|
+
* the caller must be a kernel-owned wrapper around the portable child host.
|
|
4656
|
+
*/
|
|
4657
|
+
export function definePortableProviderPluginProjection(
|
|
4658
|
+
plugin: PortableProviderPluginProjectionDefinitionV1,
|
|
4659
|
+
): ProviderPluginV1 {
|
|
4660
|
+
const [
|
|
4661
|
+
apiVersion,
|
|
4662
|
+
id,
|
|
4663
|
+
version,
|
|
4664
|
+
displayName,
|
|
4665
|
+
sourceKind,
|
|
4666
|
+
packageValue,
|
|
4667
|
+
rawBindings,
|
|
4668
|
+
] = snapshotExactEnumerableDataProperties(
|
|
4669
|
+
plugin,
|
|
4670
|
+
[
|
|
4671
|
+
"apiVersion",
|
|
4672
|
+
"id",
|
|
4673
|
+
"version",
|
|
4674
|
+
"displayName",
|
|
4675
|
+
"sourceKind",
|
|
4676
|
+
"package",
|
|
4677
|
+
"bindings",
|
|
4678
|
+
],
|
|
4679
|
+
"portable provider plugin projection",
|
|
4680
|
+
);
|
|
4681
|
+
if (
|
|
4682
|
+
apiVersion !== PROVIDER_PLUGIN_API_VERSION
|
|
4683
|
+
|| sourceKind !== "portable"
|
|
4684
|
+
|| !isProviderPluginId(id)
|
|
4685
|
+
|| typeof version !== "string"
|
|
4686
|
+
|| !pluginVersionPattern.test(version)
|
|
4687
|
+
|| typeof displayName !== "string"
|
|
4688
|
+
|| displayName.length < 1
|
|
4689
|
+
|| displayName.length > 160
|
|
4690
|
+
|| !isVerifiedPortableProviderPluginPackage(packageValue)
|
|
4691
|
+
|| packageValue.manifest.id !== id
|
|
4692
|
+
|| packageValue.manifest.version !== version
|
|
4693
|
+
|| packageValue.manifest.displayName !== displayName
|
|
4694
|
+
|| packageValue.manifest.hostApiVersion !== 1
|
|
4695
|
+
) {
|
|
4696
|
+
throw new Error("portable provider plugin projection identity is invalid");
|
|
4697
|
+
}
|
|
4698
|
+
const portablePackage = Object.freeze({
|
|
4699
|
+
hostApiVersion: packageValue.manifest.hostApiVersion,
|
|
4700
|
+
bundleSha256: packageValue.bundleSha256,
|
|
4701
|
+
manifestSha256: packageValue.manifestSha256,
|
|
4702
|
+
capabilities: packageValue.manifest.capabilities,
|
|
4703
|
+
});
|
|
4704
|
+
const bindings = snapshotBoundedDenseArray(
|
|
4705
|
+
rawBindings,
|
|
4706
|
+
`portable provider plugin ${id} bindings`,
|
|
4707
|
+
MAX_PROVIDER_PLUGIN_BINDINGS,
|
|
4708
|
+
);
|
|
4709
|
+
let portableOperationCount = 0;
|
|
4710
|
+
const adapterIds = new Set<string>();
|
|
4711
|
+
const projected = bindings.map((rawEntry, index) => {
|
|
4712
|
+
const [
|
|
4713
|
+
adapterIdValue,
|
|
4714
|
+
manifestValue,
|
|
4715
|
+
portableBindingValue,
|
|
4716
|
+
bindingValue,
|
|
4717
|
+
] = snapshotExactEnumerableDataProperties(
|
|
4718
|
+
rawEntry,
|
|
4719
|
+
["adapterId", "manifest", "portableBinding", "binding"],
|
|
4720
|
+
`portable provider plugin ${id} binding projection ${index}`,
|
|
4721
|
+
);
|
|
4722
|
+
if (
|
|
4723
|
+
typeof adapterIdValue !== "string"
|
|
4724
|
+
|| !/^[a-z][a-z0-9-]{0,47}$/u.test(adapterIdValue)
|
|
4725
|
+
) {
|
|
4726
|
+
throw new Error(
|
|
4727
|
+
`portable provider plugin ${id} binding projection ${index} adapterId must be lowercase kebab-case with at most 48 characters`,
|
|
4728
|
+
);
|
|
4729
|
+
}
|
|
4730
|
+
if (adapterIds.has(adapterIdValue)) {
|
|
4731
|
+
throw new Error(
|
|
4732
|
+
`portable provider plugin ${id} repeats adapter ID ${adapterIdValue}`,
|
|
4733
|
+
);
|
|
4734
|
+
}
|
|
4735
|
+
if (
|
|
4736
|
+
typeof manifestValue !== "object"
|
|
4737
|
+
|| manifestValue === null
|
|
4738
|
+
|| typeof portableBindingValue !== "object"
|
|
4739
|
+
|| portableBindingValue === null
|
|
4740
|
+
|| typeof bindingValue !== "object"
|
|
4741
|
+
|| bindingValue === null
|
|
4742
|
+
) {
|
|
4743
|
+
throw new Error(
|
|
4744
|
+
`portable provider plugin ${id} has an invalid binding projection at index ${index}`,
|
|
4745
|
+
);
|
|
4746
|
+
}
|
|
4747
|
+
const adapterId = adapterIdValue;
|
|
4748
|
+
const manifest = manifestValue as GhostgetManifest;
|
|
4749
|
+
const portableBinding =
|
|
4750
|
+
portableBindingValue as PortablePackageBindingV1;
|
|
4751
|
+
const bindingDefinition =
|
|
4752
|
+
bindingValue as ProviderPluginBindingDefinitionV1;
|
|
4753
|
+
if (bindingDefinition.transport === "local-cli") {
|
|
4754
|
+
throw new Error(
|
|
4755
|
+
`portable provider plugin ${id} cannot project a source-only local-cli binding`,
|
|
4756
|
+
);
|
|
4757
|
+
}
|
|
4758
|
+
adapterIds.add(adapterId);
|
|
4759
|
+
assertKernelPortableProviderPluginBindingProjection(
|
|
4760
|
+
bindingDefinition,
|
|
4761
|
+
packageValue,
|
|
4762
|
+
portableBinding,
|
|
4763
|
+
adapterId,
|
|
4764
|
+
manifest,
|
|
4765
|
+
);
|
|
4766
|
+
const binding = freezeBinding(bindingDefinition, "portable");
|
|
4767
|
+
if (binding.operations.length > MAX_PROVIDER_PLUGIN_OPERATIONS_PER_BINDING) {
|
|
4768
|
+
throw new Error(
|
|
4769
|
+
`provider plugin surface ${binding.surfaceId} operations may contain at most ${MAX_PROVIDER_PLUGIN_OPERATIONS_PER_BINDING} items`,
|
|
4770
|
+
);
|
|
4771
|
+
}
|
|
4772
|
+
portableOperationCount += binding.operations.length;
|
|
4773
|
+
if (portableOperationCount > MAX_PROVIDER_PLUGIN_OPERATIONS) {
|
|
4774
|
+
throw new Error(
|
|
4775
|
+
`portable provider plugin ${id} may declare at most ${MAX_PROVIDER_PLUGIN_OPERATIONS} operations`,
|
|
4776
|
+
);
|
|
4777
|
+
}
|
|
4778
|
+
const operationNames = Object.keys(manifest.operations).sort();
|
|
4779
|
+
const bindingOperationNames = binding.operations
|
|
4780
|
+
.map((operation) => operation.name)
|
|
4781
|
+
.sort();
|
|
4782
|
+
const expectedSchema = binding.transport === "provider-api" ? 3 : 4;
|
|
4783
|
+
if (
|
|
4784
|
+
portableBinding.adapterId !== adapterId
|
|
4785
|
+
|| portableBinding.transport !== binding.transport
|
|
4786
|
+
|| portableBinding.surfaceId !== binding.surfaceId
|
|
4787
|
+
|| portableBinding.origin !== binding.origin
|
|
4788
|
+
|| portableBinding.subject.format !== binding.subject.format
|
|
4789
|
+
|| portableBinding.authKinds.length !== binding.authKinds.length
|
|
4790
|
+
|| portableBinding.authKinds.some(
|
|
4791
|
+
(kind, index) => kind !== binding.authKinds[index],
|
|
4792
|
+
)
|
|
4793
|
+
|| manifest.id !== adapterId
|
|
4794
|
+
|| manifest.schemaVersion !== expectedSchema
|
|
4795
|
+
|| manifest.surfaceId !== binding.surfaceId
|
|
4796
|
+
|| manifest.origins.length !== binding.manifestOrigins.length
|
|
4797
|
+
|| manifest.origins.some(
|
|
4798
|
+
(origin, index) => origin !== binding.manifestOrigins[index],
|
|
4799
|
+
)
|
|
4800
|
+
|| operationNames.length !== bindingOperationNames.length
|
|
4801
|
+
|| operationNames.some(
|
|
4802
|
+
(operation, index) => operation !== bindingOperationNames[index],
|
|
4803
|
+
)
|
|
4804
|
+
) {
|
|
4805
|
+
throw new Error(
|
|
4806
|
+
`portable provider plugin ${id} adapter ${adapterId} does not match its binding`,
|
|
4807
|
+
);
|
|
4808
|
+
}
|
|
4809
|
+
const operationIdentities = new Map<
|
|
4810
|
+
string,
|
|
4811
|
+
PortableOperationIdentityV1
|
|
4812
|
+
>();
|
|
4813
|
+
for (const operation of binding.operations) {
|
|
4814
|
+
if (
|
|
4815
|
+
operation.contractVersions.length !== 1
|
|
4816
|
+
|| operation.contractVersions[0] !== operation.contractVersion
|
|
4817
|
+
) {
|
|
4818
|
+
throw new Error(
|
|
4819
|
+
`portable provider plugin ${id} operation ${operation.name} must own one current contract version`,
|
|
4820
|
+
);
|
|
4821
|
+
}
|
|
4822
|
+
const portableOperation = portableBinding.operations.find(
|
|
4823
|
+
(candidate) =>
|
|
4824
|
+
candidate.name === operation.name
|
|
4825
|
+
&& candidate.contractVersion === operation.contractVersion,
|
|
4826
|
+
);
|
|
4827
|
+
if (
|
|
4828
|
+
portableOperation === undefined
|
|
4829
|
+
|| portableOperation.risk !== operation.risk
|
|
4830
|
+
|| portableOperation.state !== operation.state
|
|
4831
|
+
|| portableOperation.dispatch !== operation.dispatch
|
|
4832
|
+
|| portableOperation.sideEffect !== operation.sideEffect
|
|
4833
|
+
|| portableOperation.idempotency !== operation.idempotency
|
|
4834
|
+
|| portableOperation.dedupeWindowMs !== operation.dedupeWindowMs
|
|
4835
|
+
|| portableOperation.implementation !== operation.implementation
|
|
4836
|
+
|| JSON.stringify(portableOperation.input)
|
|
4837
|
+
!== JSON.stringify(operation.input)
|
|
4838
|
+
) {
|
|
4839
|
+
throw new Error(
|
|
4840
|
+
`portable provider plugin ${id} operation ${operation.name} metadata does not match its runtime projection`,
|
|
4841
|
+
);
|
|
4842
|
+
}
|
|
4843
|
+
const identity = createPortableOperationIdentityV1({
|
|
4844
|
+
package: {
|
|
4845
|
+
id,
|
|
4846
|
+
version,
|
|
4847
|
+
...portablePackage,
|
|
4848
|
+
},
|
|
4849
|
+
binding: portableBinding,
|
|
4850
|
+
operation: portableOperation,
|
|
4851
|
+
});
|
|
4852
|
+
operationIdentities.set(
|
|
4853
|
+
`${operation.name}@${operation.contractVersion}`,
|
|
4854
|
+
identity,
|
|
4855
|
+
);
|
|
4856
|
+
}
|
|
4857
|
+
if (
|
|
4858
|
+
operationIdentities.size !== portableBinding.operations.length
|
|
4859
|
+
) {
|
|
4860
|
+
throw new Error(
|
|
4861
|
+
`portable provider plugin ${id} binding operation metadata is incomplete`,
|
|
4862
|
+
);
|
|
4863
|
+
}
|
|
4864
|
+
const projectedManifest = freezePortableManifestValue(
|
|
4865
|
+
structuredClone(manifest),
|
|
4866
|
+
);
|
|
4867
|
+
portableProviderPluginAdapters.set(binding, Object.freeze({
|
|
4868
|
+
adapterId,
|
|
4869
|
+
manifest: projectedManifest,
|
|
4870
|
+
}));
|
|
4871
|
+
portableProviderPluginOperationIdentities.set(
|
|
4872
|
+
binding,
|
|
4873
|
+
Object.freeze(operationIdentities),
|
|
4874
|
+
);
|
|
4875
|
+
const subjectProbe = portableBinding.subject.probe;
|
|
4876
|
+
if (subjectProbe !== null) {
|
|
4877
|
+
const identity = operationIdentities.get(
|
|
4878
|
+
`${subjectProbe.operation}@${subjectProbe.contractVersion}`,
|
|
4879
|
+
);
|
|
4880
|
+
if (identity === undefined) {
|
|
4881
|
+
throw new Error(
|
|
4882
|
+
`portable provider plugin ${id} subject probe identity is missing`,
|
|
4883
|
+
);
|
|
4884
|
+
}
|
|
4885
|
+
portableProviderPluginSubjectProbeIdentities.set(binding, identity);
|
|
4886
|
+
}
|
|
4887
|
+
return binding;
|
|
4888
|
+
});
|
|
4889
|
+
projected.sort((left, right) =>
|
|
4890
|
+
left.transport.localeCompare(right.transport)
|
|
4891
|
+
|| left.surfaceId.localeCompare(right.surfaceId));
|
|
4892
|
+
const result: ProviderPluginV1 = Object.freeze({
|
|
4893
|
+
apiVersion,
|
|
4894
|
+
id,
|
|
4895
|
+
version,
|
|
4896
|
+
displayName,
|
|
4897
|
+
sourceKind: "portable",
|
|
4898
|
+
implementationSources: Object.freeze([]),
|
|
4899
|
+
bindings: Object.freeze(projected),
|
|
4900
|
+
});
|
|
4901
|
+
validatedProviderPlugins.add(result);
|
|
4902
|
+
portableProviderPluginArtifacts.set(
|
|
4903
|
+
result,
|
|
4904
|
+
packageValue.bundleSha256,
|
|
4905
|
+
);
|
|
4906
|
+
return result;
|
|
4907
|
+
}
|