@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,1455 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
constants,
|
|
5
|
+
existsSync,
|
|
6
|
+
fchmodSync,
|
|
7
|
+
fstatSync,
|
|
8
|
+
fsyncSync,
|
|
9
|
+
lstatSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
openSync,
|
|
12
|
+
opendirSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
readdirSync,
|
|
15
|
+
renameSync,
|
|
16
|
+
rmSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
} from "node:fs";
|
|
19
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
20
|
+
|
|
21
|
+
import { canonicalJson } from "./canonical-json";
|
|
22
|
+
import {
|
|
23
|
+
derivePortableProviderPluginMinimumRisk,
|
|
24
|
+
LEGACY_PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME,
|
|
25
|
+
parsePortableProviderPluginManifest,
|
|
26
|
+
PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME,
|
|
27
|
+
WRENCH_PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME,
|
|
28
|
+
renderPortableProviderPluginManifest,
|
|
29
|
+
verifyPortableProviderPluginPackageDirectory,
|
|
30
|
+
type PortableProviderPluginFileV1,
|
|
31
|
+
type PortableProviderApiPluginOperationV1,
|
|
32
|
+
type PortableProviderPluginBindingV1,
|
|
33
|
+
type PortableProviderPluginManifestV1,
|
|
34
|
+
type PortableWebSessionPluginOperationV1,
|
|
35
|
+
type VerifiedPortableProviderPluginPackage,
|
|
36
|
+
} from "./provider-plugin-package";
|
|
37
|
+
import {
|
|
38
|
+
runPortableProviderPluginHost,
|
|
39
|
+
type PortableProviderPluginCapabilityContext,
|
|
40
|
+
type PortableProviderPluginCapabilityHost,
|
|
41
|
+
type PortableProviderPluginHostResult,
|
|
42
|
+
} from "./provider-plugin-host";
|
|
43
|
+
import {
|
|
44
|
+
projectPortableProviderPluginPackage,
|
|
45
|
+
} from "./provider-plugin-portable-catalog";
|
|
46
|
+
import {
|
|
47
|
+
createProviderPluginRegistry,
|
|
48
|
+
extendProviderPluginRegistryWithPortablePlugins,
|
|
49
|
+
} from "./provider-plugin-registry";
|
|
50
|
+
import {
|
|
51
|
+
recoverPortableProviderPluginInvocationLeaseTombstones,
|
|
52
|
+
type PortableProviderPluginInvocationLeaseRepairReport,
|
|
53
|
+
} from "./provider-plugin-invocation-lease";
|
|
54
|
+
import {
|
|
55
|
+
assertPortableProviderPluginTrustApprovalMatches,
|
|
56
|
+
disablePortableProviderPluginPackage,
|
|
57
|
+
installPortableProviderPluginPackage,
|
|
58
|
+
listPortableProviderPluginInstallations,
|
|
59
|
+
loadPortableProviderPluginInstallation,
|
|
60
|
+
parsePortableProviderPluginTrustApproval,
|
|
61
|
+
removePortableProviderPluginPackage,
|
|
62
|
+
type InstalledPortableProviderPlugin,
|
|
63
|
+
type PortableProviderPluginAssertActivatable,
|
|
64
|
+
type PortableProviderPluginAssertQuiescent,
|
|
65
|
+
} from "./provider-plugin-store";
|
|
66
|
+
import type {
|
|
67
|
+
PortablePluginCapabilityRequest,
|
|
68
|
+
PortablePluginCapabilityResult,
|
|
69
|
+
PortablePluginInvocationAuth,
|
|
70
|
+
PortablePluginInvocationFile,
|
|
71
|
+
PortablePluginJsonObject,
|
|
72
|
+
PortablePluginJsonValue,
|
|
73
|
+
PortablePluginRoute,
|
|
74
|
+
} from "./provider-plugin-protocol";
|
|
75
|
+
import {
|
|
76
|
+
PORTABLE_PROVIDER_PLUGIN_PROTOCOL_VERSION,
|
|
77
|
+
normalizePortablePluginJsonObject,
|
|
78
|
+
normalizePortablePluginJsonValue,
|
|
79
|
+
parsePortableProviderPluginMessage,
|
|
80
|
+
} from "./provider-plugin-protocol";
|
|
81
|
+
import {
|
|
82
|
+
readRegularFile,
|
|
83
|
+
removePrivateDirectoryTree,
|
|
84
|
+
ghostgetStateHome,
|
|
85
|
+
} from "./storage";
|
|
86
|
+
|
|
87
|
+
const MAX_AUTHORING_MANIFEST_BYTES = 256 * 1024;
|
|
88
|
+
const MAX_AUTHORING_FILE_BYTES = 8 * 1024 * 1024;
|
|
89
|
+
const MAX_FIXTURE_BYTES = 1024 * 1024;
|
|
90
|
+
const MAX_FIXTURE_CAPABILITY_STEPS = 128;
|
|
91
|
+
const MAX_AUTHORING_FILES = 256;
|
|
92
|
+
const MAX_AUTHORING_PARENT_ENTRIES = 10_000;
|
|
93
|
+
const MAX_ABANDONED_AUTHORING_STAGES = 64;
|
|
94
|
+
const authoringStageUuidPattern =
|
|
95
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
96
|
+
|
|
97
|
+
type Environment = Readonly<Record<string, string | undefined>>;
|
|
98
|
+
|
|
99
|
+
export type PortableProviderPluginSummary = {
|
|
100
|
+
readonly id: string;
|
|
101
|
+
readonly version: string;
|
|
102
|
+
readonly displayName: string;
|
|
103
|
+
readonly bundleSha256: string;
|
|
104
|
+
readonly manifestSha256: string;
|
|
105
|
+
readonly sourceKind: "portable";
|
|
106
|
+
readonly execution: "trusted-child-process";
|
|
107
|
+
readonly sandboxed: false;
|
|
108
|
+
readonly activation: "uninstalled" | "enabled" | "disabled";
|
|
109
|
+
readonly bindings: number;
|
|
110
|
+
readonly operations: number;
|
|
111
|
+
readonly capabilities: PortableProviderPluginManifestV1["capabilities"];
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export type PortableProviderPluginFixtureV1 = {
|
|
115
|
+
readonly schemaVersion: 1;
|
|
116
|
+
readonly route: PortablePluginRoute;
|
|
117
|
+
readonly input: PortablePluginJsonObject;
|
|
118
|
+
readonly auth: Omit<PortablePluginInvocationAuth, "handle">;
|
|
119
|
+
readonly files: readonly PortablePluginInvocationFile[];
|
|
120
|
+
readonly capabilityTranscript: readonly {
|
|
121
|
+
readonly request: PortablePluginCapabilityRequest;
|
|
122
|
+
readonly result: PortablePluginCapabilityResult;
|
|
123
|
+
}[];
|
|
124
|
+
readonly expected: {
|
|
125
|
+
readonly output: PortablePluginJsonValue;
|
|
126
|
+
readonly finalUrl: string | null;
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
function sha256(bytes: Uint8Array | string): string {
|
|
131
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
135
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
const prototype: unknown = Object.getPrototypeOf(value) as unknown;
|
|
139
|
+
return prototype === Object.prototype || prototype === null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function exactKeys(
|
|
143
|
+
value: Readonly<Record<string, unknown>>,
|
|
144
|
+
expected: readonly string[],
|
|
145
|
+
label: string,
|
|
146
|
+
): void {
|
|
147
|
+
const actual = Object.keys(value).sort();
|
|
148
|
+
const wanted = [...expected].sort();
|
|
149
|
+
if (
|
|
150
|
+
actual.length !== wanted.length
|
|
151
|
+
|| actual.some((key, index) => key !== wanted[index])
|
|
152
|
+
) {
|
|
153
|
+
throw new Error(`${label} must contain exactly: ${wanted.join(", ")}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function pluginStoreRoot(environment: Environment): string {
|
|
158
|
+
return join(ghostgetStateHome(environment), "provider-plugins");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function portableProviderPluginStoreRoot(
|
|
162
|
+
environment: Environment = process.env,
|
|
163
|
+
): string {
|
|
164
|
+
return pluginStoreRoot(environment);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function packageSummary(
|
|
168
|
+
packageValue: VerifiedPortableProviderPluginPackage,
|
|
169
|
+
activation: PortableProviderPluginSummary["activation"],
|
|
170
|
+
): PortableProviderPluginSummary {
|
|
171
|
+
const operations = packageValue.manifest.bindings.reduce(
|
|
172
|
+
(count, binding) => count + binding.operations.length,
|
|
173
|
+
0,
|
|
174
|
+
);
|
|
175
|
+
return Object.freeze({
|
|
176
|
+
id: packageValue.manifest.id,
|
|
177
|
+
version: packageValue.manifest.version,
|
|
178
|
+
displayName: packageValue.manifest.displayName,
|
|
179
|
+
bundleSha256: packageValue.bundleSha256,
|
|
180
|
+
manifestSha256: packageValue.manifestSha256,
|
|
181
|
+
sourceKind: "portable",
|
|
182
|
+
execution: "trusted-child-process",
|
|
183
|
+
sandboxed: false,
|
|
184
|
+
activation,
|
|
185
|
+
bindings: packageValue.manifest.bindings.length,
|
|
186
|
+
operations,
|
|
187
|
+
capabilities: packageValue.manifest.capabilities,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function summary(
|
|
192
|
+
installed: InstalledPortableProviderPlugin,
|
|
193
|
+
): PortableProviderPluginSummary {
|
|
194
|
+
return packageSummary(installed.package, installed.active.status);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function listPortableProviderPlugins(
|
|
198
|
+
environment: Environment = process.env,
|
|
199
|
+
): readonly PortableProviderPluginSummary[] {
|
|
200
|
+
return Object.freeze(
|
|
201
|
+
listPortableProviderPluginInstallations(pluginStoreRoot(environment))
|
|
202
|
+
.map(summary),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function showPortableProviderPlugin(
|
|
207
|
+
id: string,
|
|
208
|
+
environment: Environment = process.env,
|
|
209
|
+
): {
|
|
210
|
+
readonly summary: PortableProviderPluginSummary;
|
|
211
|
+
readonly manifest: PortableProviderPluginManifestV1;
|
|
212
|
+
readonly trust: InstalledPortableProviderPlugin["trust"];
|
|
213
|
+
readonly active: InstalledPortableProviderPlugin["active"];
|
|
214
|
+
} | null {
|
|
215
|
+
const installed = loadPortableProviderPluginInstallation(
|
|
216
|
+
pluginStoreRoot(environment),
|
|
217
|
+
id,
|
|
218
|
+
);
|
|
219
|
+
if (installed === null) return null;
|
|
220
|
+
return Object.freeze({
|
|
221
|
+
summary: summary(installed),
|
|
222
|
+
manifest: installed.package.manifest,
|
|
223
|
+
trust: installed.trust,
|
|
224
|
+
active: installed.active,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function checkPortableProviderPlugin(
|
|
229
|
+
pathValue: string,
|
|
230
|
+
): PortableProviderPluginSummary & {
|
|
231
|
+
readonly path: string;
|
|
232
|
+
readonly payloadBytes: number;
|
|
233
|
+
} {
|
|
234
|
+
const packageValue = verifyPortableProviderPluginPackageDirectory(pathValue);
|
|
235
|
+
// Mirror activation's package-local checks before any package code runs.
|
|
236
|
+
const projected = projectPortableProviderPluginPackage(packageValue);
|
|
237
|
+
extendProviderPluginRegistryWithPortablePlugins(
|
|
238
|
+
createProviderPluginRegistry([]),
|
|
239
|
+
[projected],
|
|
240
|
+
);
|
|
241
|
+
return Object.freeze({
|
|
242
|
+
...packageSummary(packageValue, "uninstalled"),
|
|
243
|
+
path: packageValue.root,
|
|
244
|
+
payloadBytes: packageValue.payloadBytes,
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function installPortableProviderPlugin(
|
|
249
|
+
pathValue: string,
|
|
250
|
+
options: {
|
|
251
|
+
readonly trustExecutableCode: boolean;
|
|
252
|
+
readonly expectedCurrentBundleSha256: string | null;
|
|
253
|
+
readonly assertActivatable: PortableProviderPluginAssertActivatable;
|
|
254
|
+
readonly assertCurrentQuiescent: PortableProviderPluginAssertQuiescent;
|
|
255
|
+
readonly environment?: Environment;
|
|
256
|
+
readonly now?: Date;
|
|
257
|
+
},
|
|
258
|
+
): PortableProviderPluginSummary {
|
|
259
|
+
if (!options.trustExecutableCode) {
|
|
260
|
+
throw new Error(
|
|
261
|
+
"portable plugin installation requires --trust-code because a child process is isolation, not a hostile-code sandbox",
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
const packageValue = verifyPortableProviderPluginPackageDirectory(pathValue);
|
|
265
|
+
const installed = installPortableProviderPluginPackage(pathValue, {
|
|
266
|
+
storeRoot: pluginStoreRoot(options.environment ?? process.env),
|
|
267
|
+
approval: {
|
|
268
|
+
decision: "trust-executable-code",
|
|
269
|
+
pluginId: packageValue.manifest.id,
|
|
270
|
+
pluginVersion: packageValue.manifest.version,
|
|
271
|
+
bundleSha256: packageValue.bundleSha256,
|
|
272
|
+
},
|
|
273
|
+
expectedCurrentBundleSha256: options.expectedCurrentBundleSha256,
|
|
274
|
+
assertActivatable: options.assertActivatable,
|
|
275
|
+
assertCurrentQuiescent: options.assertCurrentQuiescent,
|
|
276
|
+
...(options.now === undefined ? {} : { now: options.now }),
|
|
277
|
+
});
|
|
278
|
+
return summary(installed);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function disablePortableProviderPlugin(
|
|
282
|
+
id: string,
|
|
283
|
+
options: {
|
|
284
|
+
readonly expectedBundleSha256: string;
|
|
285
|
+
readonly assertQuiescent: PortableProviderPluginAssertQuiescent;
|
|
286
|
+
readonly environment?: Environment;
|
|
287
|
+
readonly now?: Date;
|
|
288
|
+
},
|
|
289
|
+
): PortableProviderPluginSummary {
|
|
290
|
+
return summary(disablePortableProviderPluginPackage(
|
|
291
|
+
pluginStoreRoot(options.environment ?? process.env),
|
|
292
|
+
id,
|
|
293
|
+
{
|
|
294
|
+
expectedBundleSha256: options.expectedBundleSha256,
|
|
295
|
+
assertQuiescent: options.assertQuiescent,
|
|
296
|
+
...(options.now === undefined ? {} : { now: options.now }),
|
|
297
|
+
},
|
|
298
|
+
));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function removePortableProviderPlugin(
|
|
302
|
+
id: string,
|
|
303
|
+
options: {
|
|
304
|
+
readonly expectedBundleSha256: string;
|
|
305
|
+
readonly assertQuiescent: PortableProviderPluginAssertQuiescent;
|
|
306
|
+
readonly environment?: Environment;
|
|
307
|
+
readonly now?: Date;
|
|
308
|
+
},
|
|
309
|
+
): PortableProviderPluginSummary & {
|
|
310
|
+
readonly activation: "uninstalled";
|
|
311
|
+
readonly retainedAuditArtifact: true;
|
|
312
|
+
} {
|
|
313
|
+
const removed = removePortableProviderPluginPackage(
|
|
314
|
+
pluginStoreRoot(options.environment ?? process.env),
|
|
315
|
+
id,
|
|
316
|
+
{
|
|
317
|
+
expectedBundleSha256: options.expectedBundleSha256,
|
|
318
|
+
assertQuiescent: options.assertQuiescent,
|
|
319
|
+
...(options.now === undefined ? {} : { now: options.now }),
|
|
320
|
+
},
|
|
321
|
+
);
|
|
322
|
+
return Object.freeze({
|
|
323
|
+
...packageSummary(removed.package, "uninstalled"),
|
|
324
|
+
activation: "uninstalled",
|
|
325
|
+
retainedAuditArtifact: true,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function safeAuthoringPath(
|
|
330
|
+
pathValue: string,
|
|
331
|
+
label: string,
|
|
332
|
+
): string {
|
|
333
|
+
const path = resolve(pathValue);
|
|
334
|
+
const parent = dirname(path);
|
|
335
|
+
const stats = lstatSync(parent);
|
|
336
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) {
|
|
337
|
+
throw new Error(`${label} parent must be one real directory`);
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
lstatSync(path);
|
|
341
|
+
throw new Error(`${label} already exists`);
|
|
342
|
+
} catch (error) {
|
|
343
|
+
if (
|
|
344
|
+
error instanceof Error
|
|
345
|
+
&& error.message === `${label} already exists`
|
|
346
|
+
) throw error;
|
|
347
|
+
if (
|
|
348
|
+
typeof error !== "object"
|
|
349
|
+
|| error === null
|
|
350
|
+
|| !("code" in error)
|
|
351
|
+
|| error.code !== "ENOENT"
|
|
352
|
+
) {
|
|
353
|
+
throw new Error(`${label} could not be inspected`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return path;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
type PortableProviderPluginAuthoringStageKind = "init" | "pack";
|
|
360
|
+
|
|
361
|
+
function recoverAbandonedAuthoringStages(
|
|
362
|
+
output: string,
|
|
363
|
+
kind: PortableProviderPluginAuthoringStageKind,
|
|
364
|
+
): void {
|
|
365
|
+
const parent = dirname(output);
|
|
366
|
+
const outputName = basename(output);
|
|
367
|
+
const prefixes = [
|
|
368
|
+
`.${outputName}.wrench-plugin-${kind}-`,
|
|
369
|
+
`.${outputName}.oh-plugin-${kind}-`,
|
|
370
|
+
] as const;
|
|
371
|
+
const stages: {
|
|
372
|
+
readonly path: string;
|
|
373
|
+
readonly identity: { readonly device: string; readonly inode: string };
|
|
374
|
+
}[] = [];
|
|
375
|
+
const directory = opendirSync(parent);
|
|
376
|
+
try {
|
|
377
|
+
for (let entriesRead = 0;; entriesRead += 1) {
|
|
378
|
+
const entry = directory.readSync();
|
|
379
|
+
if (entry === null) break;
|
|
380
|
+
if (entriesRead >= MAX_AUTHORING_PARENT_ENTRIES) {
|
|
381
|
+
throw new Error(
|
|
382
|
+
`portable plugin ${kind} recovery exceeds its ${MAX_AUTHORING_PARENT_ENTRIES} parent-entry bound`,
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
const prefix = prefixes.find((candidate) => entry.name.startsWith(candidate));
|
|
386
|
+
if (
|
|
387
|
+
prefix === undefined
|
|
388
|
+
|| !authoringStageUuidPattern.test(entry.name.slice(prefix.length))
|
|
389
|
+
) continue;
|
|
390
|
+
if (stages.length >= MAX_ABANDONED_AUTHORING_STAGES) {
|
|
391
|
+
throw new Error(
|
|
392
|
+
`portable plugin ${kind} recovery found more than ${MAX_ABANDONED_AUTHORING_STAGES} exact abandoned stages`,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
const path = join(parent, entry.name);
|
|
396
|
+
const stats = lstatSync(path, { bigint: true });
|
|
397
|
+
const currentUid = typeof process.getuid === "function"
|
|
398
|
+
? BigInt(process.getuid())
|
|
399
|
+
: stats.uid;
|
|
400
|
+
if (
|
|
401
|
+
entry.isSymbolicLink()
|
|
402
|
+
|| !entry.isDirectory()
|
|
403
|
+
|| !stats.isDirectory()
|
|
404
|
+
|| stats.isSymbolicLink()
|
|
405
|
+
|| stats.uid !== currentUid
|
|
406
|
+
|| (stats.mode & 0o777n) !== 0o700n
|
|
407
|
+
) {
|
|
408
|
+
throw new Error(
|
|
409
|
+
`portable plugin ${kind} abandoned stage is not a private current-user directory: ${path}`,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
stages.push({
|
|
413
|
+
path,
|
|
414
|
+
identity: {
|
|
415
|
+
device: stats.dev.toString(),
|
|
416
|
+
inode: stats.ino.toString(),
|
|
417
|
+
},
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
} finally {
|
|
421
|
+
directory.closeSync();
|
|
422
|
+
}
|
|
423
|
+
for (const stage of stages) {
|
|
424
|
+
if (!removePrivateDirectoryTree(stage.path, stage.identity)) {
|
|
425
|
+
throw new Error(`portable plugin ${kind} abandoned stage disappeared during recovery`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function pauseAfterAuthoringStageForTest(
|
|
431
|
+
kind: PortableProviderPluginAuthoringStageKind,
|
|
432
|
+
): void {
|
|
433
|
+
if (
|
|
434
|
+
process.env.NODE_ENV !== "test"
|
|
435
|
+
|| (process.env.GHOSTGET_TEST_PLUGIN_AUTHORING_STAGE_FAULT ?? process.env.WRENCH_TEST_PLUGIN_AUTHORING_STAGE_FAULT) !== kind
|
|
436
|
+
) return;
|
|
437
|
+
const signal = new Int32Array(new SharedArrayBuffer(4));
|
|
438
|
+
if (Atomics.wait(signal, 0, 0, 60_000) === "timed-out") {
|
|
439
|
+
throw new Error(`portable plugin ${kind} stage test pause expired`);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function writePrivateAuthoringFile(path: string, bytes: Uint8Array | string): void {
|
|
444
|
+
const descriptor = openSync(
|
|
445
|
+
path,
|
|
446
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL,
|
|
447
|
+
0o600,
|
|
448
|
+
);
|
|
449
|
+
try {
|
|
450
|
+
fchmodSync(descriptor, 0o600);
|
|
451
|
+
writeFileSync(descriptor, bytes);
|
|
452
|
+
fsyncSync(descriptor);
|
|
453
|
+
} finally {
|
|
454
|
+
closeSync(descriptor);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function runtimeTemplate(): string {
|
|
459
|
+
return `import { createInterface } from "node:readline";
|
|
460
|
+
|
|
461
|
+
let invocation = null;
|
|
462
|
+
let running = null;
|
|
463
|
+
const pending = new Map();
|
|
464
|
+
const send = (value) => process.stdout.write(\`\${JSON.stringify(value)}\\n\`);
|
|
465
|
+
|
|
466
|
+
async function invoke(context) {
|
|
467
|
+
// Replace this inert reservation only after capturing authorized provider
|
|
468
|
+
// evidence and updating ghostget-plugin.json to state "observed".
|
|
469
|
+
throw Object.assign(new Error("operation remains capture-required"), {
|
|
470
|
+
code: "CAPTURE_REQUIRED",
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const capability = (request) => new Promise((resolve, reject) => {
|
|
475
|
+
const requestId = crypto.randomUUID();
|
|
476
|
+
pending.set(requestId, { resolve, reject });
|
|
477
|
+
send({
|
|
478
|
+
protocolVersion: 1,
|
|
479
|
+
kind: "plugin.capability.request",
|
|
480
|
+
invocationId: invocation.invocationId,
|
|
481
|
+
requestId,
|
|
482
|
+
request,
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
async function runInvocation(message) {
|
|
487
|
+
invocation = message;
|
|
488
|
+
try {
|
|
489
|
+
const output = await invoke({
|
|
490
|
+
route: message.route,
|
|
491
|
+
input: message.input,
|
|
492
|
+
auth: message.auth,
|
|
493
|
+
files: message.files,
|
|
494
|
+
capability,
|
|
495
|
+
});
|
|
496
|
+
send({
|
|
497
|
+
protocolVersion: 1,
|
|
498
|
+
kind: "plugin.result",
|
|
499
|
+
invocationId: message.invocationId,
|
|
500
|
+
output,
|
|
501
|
+
finalUrl: null,
|
|
502
|
+
});
|
|
503
|
+
} catch (error) {
|
|
504
|
+
send({
|
|
505
|
+
protocolVersion: 1,
|
|
506
|
+
kind: "plugin.error",
|
|
507
|
+
stage: "invocation",
|
|
508
|
+
invocationId: message.invocationId,
|
|
509
|
+
code: typeof error?.code === "string" ? error.code : "PLUGIN_FAILED",
|
|
510
|
+
message: "plugin invocation failed",
|
|
511
|
+
});
|
|
512
|
+
} finally {
|
|
513
|
+
invocation = null;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
for await (const line of createInterface({ input: process.stdin, crlfDelay: Infinity })) {
|
|
518
|
+
const message = JSON.parse(line);
|
|
519
|
+
if (message.kind === "host.hello") {
|
|
520
|
+
send({
|
|
521
|
+
protocolVersion: 1,
|
|
522
|
+
kind: "plugin.ready",
|
|
523
|
+
plugin: message.plugin,
|
|
524
|
+
});
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
if (message.kind === "host.capability.result") {
|
|
528
|
+
pending.get(message.requestId)?.resolve(message.result);
|
|
529
|
+
pending.delete(message.requestId);
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
if (message.kind === "host.capability.error") {
|
|
533
|
+
pending.get(message.requestId)?.reject(
|
|
534
|
+
Object.assign(new Error(message.error.message), {
|
|
535
|
+
code: message.error.code,
|
|
536
|
+
}),
|
|
537
|
+
);
|
|
538
|
+
pending.delete(message.requestId);
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
if (message.kind !== "host.invoke") continue;
|
|
542
|
+
if (running !== null) {
|
|
543
|
+
send({
|
|
544
|
+
protocolVersion: 1,
|
|
545
|
+
kind: "plugin.error",
|
|
546
|
+
stage: "invocation",
|
|
547
|
+
invocationId: message.invocationId,
|
|
548
|
+
code: "INVOCATION_IN_PROGRESS",
|
|
549
|
+
message: "plugin invocation failed",
|
|
550
|
+
});
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
running = runInvocation(message).finally(() => {
|
|
554
|
+
running = null;
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
await running;
|
|
558
|
+
`;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function guideTemplate(
|
|
562
|
+
id: string,
|
|
563
|
+
surfaceId: string,
|
|
564
|
+
operation: string,
|
|
565
|
+
): string {
|
|
566
|
+
return `# Contents
|
|
567
|
+
|
|
568
|
+
- \`ghostget-plugin.json\` – strict static identity, capability ceiling, adapter, and operation descriptor.
|
|
569
|
+
- \`dist/plugin.mjs\` – self-contained child-process runtime; it may use only the versioned Ghostget message protocol.
|
|
570
|
+
- \`fixtures/\` – secret-free deterministic invocation fixtures.
|
|
571
|
+
|
|
572
|
+
# Guidelines
|
|
573
|
+
|
|
574
|
+
- Keep ${id}/${surfaceId}/${operation} capture-required until authorized evidence proves its exact request, response, account, and reconciliation semantics.
|
|
575
|
+
- Parse every foreign value from unknown and reject extra fields.
|
|
576
|
+
- Ask Ghostget for declared capabilities; never read GHOSTGET_STATE_HOME, browser profiles, token files, or arbitrary filesystem paths.
|
|
577
|
+
- Give each observed operation fixture bounded \`files\` metadata and an exact ordered \`capabilityTranscript\`. Each step contains one bounded \`request\` and \`result\`, including explicit failure statuses and malformed response shapes; plugin tests never use live credentials or the network.
|
|
578
|
+
- A child process isolates crashes and dependencies but is not a hostile-code sandbox. Testing and installation require explicit executable-code trust.
|
|
579
|
+
- Run \`ghostget plugin check .\`, \`ghostget plugin test . --trust-code\`, and \`ghostget plugin pack . --output ../${id}.ghostgetplugin\` after each change.
|
|
580
|
+
`;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
export function initPortableProviderPlugin(options: {
|
|
584
|
+
readonly id: string;
|
|
585
|
+
readonly displayName: string;
|
|
586
|
+
readonly surfaceId: string;
|
|
587
|
+
readonly origin: string;
|
|
588
|
+
readonly operation: string;
|
|
589
|
+
readonly transport?:
|
|
590
|
+
PortableProviderPluginManifestV1["bindings"][number]["transport"];
|
|
591
|
+
readonly requiredScopeSets?: readonly (readonly string[])[];
|
|
592
|
+
readonly coverage?: readonly string[];
|
|
593
|
+
readonly output: string;
|
|
594
|
+
}): {
|
|
595
|
+
readonly path: string;
|
|
596
|
+
readonly manifest: PortableProviderPluginManifestV1;
|
|
597
|
+
readonly bundleSha256: string;
|
|
598
|
+
} {
|
|
599
|
+
const output = safeAuthoringPath(options.output, "portable plugin output");
|
|
600
|
+
recoverAbandonedAuthoringStages(output, "init");
|
|
601
|
+
const stage = join(
|
|
602
|
+
dirname(output),
|
|
603
|
+
`.${basename(output)}.wrench-plugin-init-${crypto.randomUUID()}`,
|
|
604
|
+
);
|
|
605
|
+
mkdirSync(stage, { mode: 0o700 });
|
|
606
|
+
try {
|
|
607
|
+
pauseAfterAuthoringStageForTest("init");
|
|
608
|
+
let parsedOrigin: URL;
|
|
609
|
+
try {
|
|
610
|
+
parsedOrigin = new URL(options.origin);
|
|
611
|
+
} catch {
|
|
612
|
+
throw new Error("portable plugin init origin must be exact canonical HTTPS");
|
|
613
|
+
}
|
|
614
|
+
if (
|
|
615
|
+
parsedOrigin.protocol !== "https:"
|
|
616
|
+
|| parsedOrigin.username !== ""
|
|
617
|
+
|| parsedOrigin.password !== ""
|
|
618
|
+
|| parsedOrigin.pathname !== "/"
|
|
619
|
+
|| parsedOrigin.search !== ""
|
|
620
|
+
|| parsedOrigin.hash !== ""
|
|
621
|
+
|| parsedOrigin.origin !== options.origin
|
|
622
|
+
) {
|
|
623
|
+
throw new Error("portable plugin init origin must be exact canonical HTTPS");
|
|
624
|
+
}
|
|
625
|
+
const origin = parsedOrigin.origin as `https://${string}`;
|
|
626
|
+
const transport = options.transport ?? "web-session-api";
|
|
627
|
+
if (
|
|
628
|
+
transport === "provider-api"
|
|
629
|
+
&& (
|
|
630
|
+
options.requiredScopeSets === undefined
|
|
631
|
+
|| options.coverage === undefined
|
|
632
|
+
)
|
|
633
|
+
) {
|
|
634
|
+
throw new Error(
|
|
635
|
+
"provider-api portable plugin init requires explicit requiredScopeSets and coverage",
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
if (
|
|
639
|
+
transport !== "provider-api"
|
|
640
|
+
&& (
|
|
641
|
+
options.requiredScopeSets !== undefined
|
|
642
|
+
|| options.coverage !== undefined
|
|
643
|
+
)
|
|
644
|
+
) {
|
|
645
|
+
throw new Error(
|
|
646
|
+
"only provider-api portable plugins may declare scope sets and coverage",
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
const authKinds = transport === "provider-api"
|
|
650
|
+
? ["oauth-token-file"] as const
|
|
651
|
+
: transport === "linked-device"
|
|
652
|
+
? ["linked-device-store"] as const
|
|
653
|
+
: [
|
|
654
|
+
"browser-profile",
|
|
655
|
+
"cookie-source",
|
|
656
|
+
"cookies-file",
|
|
657
|
+
] as const;
|
|
658
|
+
const sessionMaterial: PortableProviderPluginManifestV1["capabilities"]["sessionMaterial"] = transport === "provider-api"
|
|
659
|
+
? ["oauth-access-token"]
|
|
660
|
+
: transport === "web-session-api"
|
|
661
|
+
? ["cookie-jar"]
|
|
662
|
+
: [];
|
|
663
|
+
mkdirSync(join(stage, "dist"), { mode: 0o700 });
|
|
664
|
+
mkdirSync(join(stage, "fixtures"), { mode: 0o700 });
|
|
665
|
+
const runtime = Buffer.from(runtimeTemplate(), "utf8");
|
|
666
|
+
const guide = Buffer.from(
|
|
667
|
+
guideTemplate(options.id, options.surfaceId, options.operation),
|
|
668
|
+
"utf8",
|
|
669
|
+
);
|
|
670
|
+
const fixtureName =
|
|
671
|
+
`fixtures/${options.surfaceId}.${options.operation}.v1.json`;
|
|
672
|
+
const fixture = Buffer.from(`${JSON.stringify({
|
|
673
|
+
schemaVersion: 1,
|
|
674
|
+
route: {
|
|
675
|
+
transport,
|
|
676
|
+
surfaceId: options.surfaceId,
|
|
677
|
+
operation: options.operation,
|
|
678
|
+
contractVersion: 1,
|
|
679
|
+
},
|
|
680
|
+
input: {},
|
|
681
|
+
auth: {
|
|
682
|
+
kind: authKinds[0],
|
|
683
|
+
},
|
|
684
|
+
files: [],
|
|
685
|
+
capabilityTranscript: [],
|
|
686
|
+
expected: {
|
|
687
|
+
output: null,
|
|
688
|
+
finalUrl: null,
|
|
689
|
+
},
|
|
690
|
+
}, null, 2)}\n`, "utf8");
|
|
691
|
+
const risk = derivePortableProviderPluginMinimumRisk(options.operation);
|
|
692
|
+
const dispatch = risk === "R2" || risk === "R3" ? "single" : "none";
|
|
693
|
+
const baseOperation: PortableWebSessionPluginOperationV1 = {
|
|
694
|
+
name: options.operation,
|
|
695
|
+
contractVersion: 1,
|
|
696
|
+
timeoutMs: 30_000,
|
|
697
|
+
maxOutputBytes: 256 * 1024,
|
|
698
|
+
state: "capture-required",
|
|
699
|
+
risk,
|
|
700
|
+
dispatch,
|
|
701
|
+
sideEffect: dispatch === "none"
|
|
702
|
+
? "none"
|
|
703
|
+
: "capture-required remote side effect",
|
|
704
|
+
idempotency: dispatch === "none"
|
|
705
|
+
? "none"
|
|
706
|
+
: "local-at-most-once",
|
|
707
|
+
dedupeWindowMs: dispatch === "none" ? 0 : 60_000,
|
|
708
|
+
input: {
|
|
709
|
+
properties: {},
|
|
710
|
+
required: [],
|
|
711
|
+
},
|
|
712
|
+
implementation:
|
|
713
|
+
"Network-inert reservation awaiting authorized provider evidence.",
|
|
714
|
+
};
|
|
715
|
+
const providerOperation: PortableProviderApiPluginOperationV1 | null =
|
|
716
|
+
transport === "provider-api"
|
|
717
|
+
? {
|
|
718
|
+
...baseOperation,
|
|
719
|
+
requiredScopeSets: options.requiredScopeSets
|
|
720
|
+
?? (() => {
|
|
721
|
+
throw new Error(
|
|
722
|
+
"provider-api portable plugin init requires scope sets",
|
|
723
|
+
);
|
|
724
|
+
})(),
|
|
725
|
+
coverage: options.coverage
|
|
726
|
+
?? (() => {
|
|
727
|
+
throw new Error(
|
|
728
|
+
"provider-api portable plugin init requires coverage",
|
|
729
|
+
);
|
|
730
|
+
})(),
|
|
731
|
+
}
|
|
732
|
+
: null;
|
|
733
|
+
const binding: PortableProviderPluginBindingV1 =
|
|
734
|
+
transport === "provider-api"
|
|
735
|
+
? (() => {
|
|
736
|
+
if (providerOperation === null) {
|
|
737
|
+
throw new Error(
|
|
738
|
+
"provider-api portable plugin operation disappeared",
|
|
739
|
+
);
|
|
740
|
+
}
|
|
741
|
+
return {
|
|
742
|
+
transport,
|
|
743
|
+
adapterId: options.id,
|
|
744
|
+
surfaceId: options.surfaceId,
|
|
745
|
+
origin,
|
|
746
|
+
authKinds: ["oauth-token-file"] as const,
|
|
747
|
+
subject: {
|
|
748
|
+
format: "bounded provider account identifier",
|
|
749
|
+
kind: "opaque-token" as const,
|
|
750
|
+
probe: null,
|
|
751
|
+
},
|
|
752
|
+
operations: [providerOperation],
|
|
753
|
+
};
|
|
754
|
+
})()
|
|
755
|
+
: transport === "linked-device"
|
|
756
|
+
? {
|
|
757
|
+
transport,
|
|
758
|
+
adapterId: options.id,
|
|
759
|
+
surfaceId: options.surfaceId,
|
|
760
|
+
origin,
|
|
761
|
+
authKinds: ["linked-device-store"],
|
|
762
|
+
subject: {
|
|
763
|
+
format: "bounded linked-device account identifier",
|
|
764
|
+
kind: "opaque-token",
|
|
765
|
+
probe: null,
|
|
766
|
+
},
|
|
767
|
+
operations: [baseOperation],
|
|
768
|
+
}
|
|
769
|
+
: {
|
|
770
|
+
transport,
|
|
771
|
+
adapterId: options.id,
|
|
772
|
+
surfaceId: options.surfaceId,
|
|
773
|
+
origin,
|
|
774
|
+
authKinds: [
|
|
775
|
+
"browser-profile",
|
|
776
|
+
"cookie-source",
|
|
777
|
+
"cookies-file",
|
|
778
|
+
],
|
|
779
|
+
subject: {
|
|
780
|
+
format: "bounded signed-in account identifier",
|
|
781
|
+
kind: "opaque-token",
|
|
782
|
+
probe: null,
|
|
783
|
+
},
|
|
784
|
+
operations: [baseOperation],
|
|
785
|
+
};
|
|
786
|
+
const manifest: PortableProviderPluginManifestV1 = {
|
|
787
|
+
schemaVersion: 1,
|
|
788
|
+
hostApiVersion: 1,
|
|
789
|
+
id: options.id,
|
|
790
|
+
version: "0.1.0",
|
|
791
|
+
displayName: options.displayName,
|
|
792
|
+
runtime: {
|
|
793
|
+
kind: "bun-js",
|
|
794
|
+
entrypoint: "dist/plugin.mjs",
|
|
795
|
+
},
|
|
796
|
+
provenance: { kind: "local" },
|
|
797
|
+
capabilities: {
|
|
798
|
+
networkOrigins: [origin],
|
|
799
|
+
planFiles: "none",
|
|
800
|
+
state: "none",
|
|
801
|
+
sessionMaterial,
|
|
802
|
+
},
|
|
803
|
+
bindings: [binding],
|
|
804
|
+
files: [
|
|
805
|
+
fileRecord("AGENTS.md", "data", guide),
|
|
806
|
+
fileRecord("dist/plugin.mjs", "runtime", runtime),
|
|
807
|
+
fileRecord(fixtureName, "data", fixture),
|
|
808
|
+
],
|
|
809
|
+
};
|
|
810
|
+
writePrivateAuthoringFile(join(stage, "AGENTS.md"), guide);
|
|
811
|
+
writePrivateAuthoringFile(join(stage, "dist", "plugin.mjs"), runtime);
|
|
812
|
+
writePrivateAuthoringFile(join(stage, ...fixtureName.split("/")), fixture);
|
|
813
|
+
writePrivateAuthoringFile(
|
|
814
|
+
join(stage, "ghostget-plugin.json"),
|
|
815
|
+
renderPortableProviderPluginManifest(manifest),
|
|
816
|
+
);
|
|
817
|
+
const verified = verifyPortableProviderPluginPackageDirectory(stage);
|
|
818
|
+
renameSync(stage, output);
|
|
819
|
+
return Object.freeze({
|
|
820
|
+
path: output,
|
|
821
|
+
manifest: verified.manifest,
|
|
822
|
+
bundleSha256: verified.bundleSha256,
|
|
823
|
+
});
|
|
824
|
+
} catch (error) {
|
|
825
|
+
rmSync(stage, { recursive: true, force: true });
|
|
826
|
+
throw error;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function fileRecord(
|
|
831
|
+
path: string,
|
|
832
|
+
kind: PortableProviderPluginFileV1["kind"],
|
|
833
|
+
bytes: Uint8Array,
|
|
834
|
+
): PortableProviderPluginFileV1 {
|
|
835
|
+
return Object.freeze({
|
|
836
|
+
path,
|
|
837
|
+
kind,
|
|
838
|
+
bytes: bytes.byteLength,
|
|
839
|
+
sha256: sha256(bytes),
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function sourceManifestName(source: string): string {
|
|
844
|
+
const names = [
|
|
845
|
+
PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME,
|
|
846
|
+
WRENCH_PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME,
|
|
847
|
+
LEGACY_PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME,
|
|
848
|
+
].filter((name) => existsSync(join(source, name)));
|
|
849
|
+
if (names.length === 0) {
|
|
850
|
+
throw new Error(
|
|
851
|
+
`portable plugin source is missing ${PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME}`,
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
if (names.length > 1) {
|
|
855
|
+
throw new Error(
|
|
856
|
+
`portable plugin source must contain exactly one of ${names.join(" or ")}`,
|
|
857
|
+
);
|
|
858
|
+
}
|
|
859
|
+
const name = names[0];
|
|
860
|
+
if (name === undefined) throw new Error("portable plugin manifest selection failed");
|
|
861
|
+
return name;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function readSourceManifest(source: string): {
|
|
865
|
+
readonly name: string;
|
|
866
|
+
readonly value: PortableProviderPluginManifestV1;
|
|
867
|
+
} {
|
|
868
|
+
const name = sourceManifestName(source);
|
|
869
|
+
const text = readRegularFile(
|
|
870
|
+
join(source, name),
|
|
871
|
+
MAX_AUTHORING_MANIFEST_BYTES,
|
|
872
|
+
"portable plugin authoring manifest",
|
|
873
|
+
);
|
|
874
|
+
let value: unknown;
|
|
875
|
+
try {
|
|
876
|
+
value = JSON.parse(text) as unknown;
|
|
877
|
+
} catch {
|
|
878
|
+
throw new Error("portable plugin authoring manifest must contain JSON");
|
|
879
|
+
}
|
|
880
|
+
const parsed = parsePortableProviderPluginManifest(value);
|
|
881
|
+
if (!parsed.ok) {
|
|
882
|
+
throw new Error(
|
|
883
|
+
`portable plugin authoring manifest is invalid: ${parsed.issues.join("; ")}`,
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
return { name, value: parsed.value };
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function readDeclaredSourceFile(
|
|
890
|
+
source: string,
|
|
891
|
+
file: PortableProviderPluginFileV1,
|
|
892
|
+
): Buffer {
|
|
893
|
+
const path = join(source, ...file.path.split("/"));
|
|
894
|
+
const noFollow = "O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0;
|
|
895
|
+
const descriptor = openSync(path, constants.O_RDONLY | noFollow);
|
|
896
|
+
try {
|
|
897
|
+
const stats = fstatSync(descriptor);
|
|
898
|
+
if (
|
|
899
|
+
!stats.isFile()
|
|
900
|
+
|| stats.size < 1
|
|
901
|
+
|| stats.size > MAX_AUTHORING_FILE_BYTES
|
|
902
|
+
) {
|
|
903
|
+
throw new Error(`portable plugin source file ${file.path} is invalid`);
|
|
904
|
+
}
|
|
905
|
+
return readFileSync(descriptor);
|
|
906
|
+
} finally {
|
|
907
|
+
// Node's descriptor is closed by readFileSync only when it opened the
|
|
908
|
+
// path, not when it receives the caller's descriptor.
|
|
909
|
+
closeSync(descriptor);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function authoringFiles(source: string): readonly string[] {
|
|
914
|
+
const files: string[] = [];
|
|
915
|
+
const visit = (relativeDirectory: string): void => {
|
|
916
|
+
const directory = relativeDirectory === ""
|
|
917
|
+
? source
|
|
918
|
+
: join(source, ...relativeDirectory.split("/"));
|
|
919
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
920
|
+
const relativePath = relativeDirectory === ""
|
|
921
|
+
? entry.name
|
|
922
|
+
: `${relativeDirectory}/${entry.name}`;
|
|
923
|
+
if (entry.isSymbolicLink()) {
|
|
924
|
+
throw new Error(`portable plugin source contains symlink ${relativePath}`);
|
|
925
|
+
}
|
|
926
|
+
if (entry.isDirectory()) {
|
|
927
|
+
visit(relativePath);
|
|
928
|
+
} else if (entry.isFile()) {
|
|
929
|
+
files.push(relativePath);
|
|
930
|
+
if (files.length > MAX_AUTHORING_FILES + 1) {
|
|
931
|
+
throw new Error("portable plugin source contains too many files");
|
|
932
|
+
}
|
|
933
|
+
} else {
|
|
934
|
+
throw new Error(
|
|
935
|
+
`portable plugin source contains unsupported entry ${relativePath}`,
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
visit("");
|
|
941
|
+
return Object.freeze(files.sort());
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
export function packPortableProviderPlugin(
|
|
945
|
+
sourceValue: string,
|
|
946
|
+
outputValue: string,
|
|
947
|
+
): {
|
|
948
|
+
readonly path: string;
|
|
949
|
+
readonly bundleSha256: string;
|
|
950
|
+
readonly manifestSha256: string;
|
|
951
|
+
} {
|
|
952
|
+
const source = resolve(sourceValue);
|
|
953
|
+
const sourceStats = lstatSync(source);
|
|
954
|
+
if (!sourceStats.isDirectory() || sourceStats.isSymbolicLink()) {
|
|
955
|
+
throw new Error("portable plugin source must be one real directory");
|
|
956
|
+
}
|
|
957
|
+
const output = safeAuthoringPath(outputValue, "portable plugin package output");
|
|
958
|
+
recoverAbandonedAuthoringStages(output, "pack");
|
|
959
|
+
const sourceManifest = readSourceManifest(source);
|
|
960
|
+
const manifest = sourceManifest.value;
|
|
961
|
+
const actualFiles = authoringFiles(source)
|
|
962
|
+
.filter((path) => path !== sourceManifest.name);
|
|
963
|
+
const declaredFiles = manifest.files.map((file) => file.path);
|
|
964
|
+
if (
|
|
965
|
+
actualFiles.length !== declaredFiles.length
|
|
966
|
+
|| actualFiles.some((path, index) => path !== declaredFiles[index])
|
|
967
|
+
) {
|
|
968
|
+
throw new Error(
|
|
969
|
+
"portable plugin source files must exactly match its static manifest before packing",
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
const payload = new Map<string, Buffer>();
|
|
973
|
+
const refreshedFiles = manifest.files.map((file) => {
|
|
974
|
+
const bytes = readDeclaredSourceFile(source, file);
|
|
975
|
+
payload.set(file.path, bytes);
|
|
976
|
+
return fileRecord(file.path, file.kind, bytes);
|
|
977
|
+
});
|
|
978
|
+
const refreshed: PortableProviderPluginManifestV1 = {
|
|
979
|
+
...manifest,
|
|
980
|
+
files: Object.freeze(refreshedFiles),
|
|
981
|
+
};
|
|
982
|
+
const stage = join(
|
|
983
|
+
dirname(output),
|
|
984
|
+
`.${basename(output)}.wrench-plugin-pack-${crypto.randomUUID()}`,
|
|
985
|
+
);
|
|
986
|
+
mkdirSync(stage, { mode: 0o700 });
|
|
987
|
+
try {
|
|
988
|
+
pauseAfterAuthoringStageForTest("pack");
|
|
989
|
+
for (const file of refreshed.files) {
|
|
990
|
+
const destination = join(stage, ...file.path.split("/"));
|
|
991
|
+
mkdirSync(dirname(destination), { recursive: true, mode: 0o700 });
|
|
992
|
+
const bytes = payload.get(file.path);
|
|
993
|
+
if (bytes === undefined) {
|
|
994
|
+
throw new Error("portable plugin pack omitted a declared file");
|
|
995
|
+
}
|
|
996
|
+
writePrivateAuthoringFile(destination, bytes);
|
|
997
|
+
}
|
|
998
|
+
writePrivateAuthoringFile(
|
|
999
|
+
join(stage, "ghostget-plugin.json"),
|
|
1000
|
+
renderPortableProviderPluginManifest(refreshed),
|
|
1001
|
+
);
|
|
1002
|
+
const verified = verifyPortableProviderPluginPackageDirectory(stage);
|
|
1003
|
+
renameSync(stage, output);
|
|
1004
|
+
return Object.freeze({
|
|
1005
|
+
path: output,
|
|
1006
|
+
bundleSha256: verified.bundleSha256,
|
|
1007
|
+
manifestSha256: verified.manifestSha256,
|
|
1008
|
+
});
|
|
1009
|
+
} catch (error) {
|
|
1010
|
+
rmSync(stage, { recursive: true, force: true });
|
|
1011
|
+
throw error;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function parseFixtureCapabilityRequest(
|
|
1016
|
+
value: unknown,
|
|
1017
|
+
index: number,
|
|
1018
|
+
): PortablePluginCapabilityRequest {
|
|
1019
|
+
const parsed = parsePortableProviderPluginMessage({
|
|
1020
|
+
protocolVersion: PORTABLE_PROVIDER_PLUGIN_PROTOCOL_VERSION,
|
|
1021
|
+
kind: "plugin.capability.request",
|
|
1022
|
+
invocationId: "fixture-invocation",
|
|
1023
|
+
requestId: `fixture-step-${index + 1}`,
|
|
1024
|
+
request: value,
|
|
1025
|
+
});
|
|
1026
|
+
if (!parsed.ok) {
|
|
1027
|
+
throw new Error(
|
|
1028
|
+
`portable plugin fixture capabilityTranscript[${index}].request is invalid: ${parsed.issues.join("; ")}`,
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
if (parsed.value.kind !== "plugin.capability.request") {
|
|
1032
|
+
throw new Error("portable plugin fixture capability request parser drifted");
|
|
1033
|
+
}
|
|
1034
|
+
if (parsed.value.request.kind === "log.write") {
|
|
1035
|
+
throw new Error(
|
|
1036
|
+
`portable plugin fixture capabilityTranscript[${index}] must not include log.write because the host acknowledges bounded logs directly`,
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
return parsed.value.request;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function parseFixtureCapabilityResult(
|
|
1043
|
+
value: unknown,
|
|
1044
|
+
request: PortablePluginCapabilityRequest,
|
|
1045
|
+
index: number,
|
|
1046
|
+
): PortablePluginCapabilityResult {
|
|
1047
|
+
const parsed = parsePortableProviderPluginMessage({
|
|
1048
|
+
protocolVersion: PORTABLE_PROVIDER_PLUGIN_PROTOCOL_VERSION,
|
|
1049
|
+
kind: "host.capability.result",
|
|
1050
|
+
invocationId: "fixture-invocation",
|
|
1051
|
+
requestId: `fixture-step-${index + 1}`,
|
|
1052
|
+
result: value,
|
|
1053
|
+
});
|
|
1054
|
+
if (!parsed.ok) {
|
|
1055
|
+
throw new Error(
|
|
1056
|
+
`portable plugin fixture capabilityTranscript[${index}].result is invalid: ${parsed.issues.join("; ")}`,
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
if (parsed.value.kind !== "host.capability.result") {
|
|
1060
|
+
throw new Error("portable plugin fixture capability result parser drifted");
|
|
1061
|
+
}
|
|
1062
|
+
if (parsed.value.result.kind !== request.kind) {
|
|
1063
|
+
throw new Error(
|
|
1064
|
+
`portable plugin fixture capabilityTranscript[${index}] result kind must match ${request.kind}`,
|
|
1065
|
+
);
|
|
1066
|
+
}
|
|
1067
|
+
return parsed.value.result;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function parseFixtureCapabilityTranscript(
|
|
1071
|
+
value: unknown,
|
|
1072
|
+
): PortableProviderPluginFixtureV1["capabilityTranscript"] {
|
|
1073
|
+
if (
|
|
1074
|
+
!Array.isArray(value)
|
|
1075
|
+
|| value.length > MAX_FIXTURE_CAPABILITY_STEPS
|
|
1076
|
+
) {
|
|
1077
|
+
throw new Error(
|
|
1078
|
+
`portable plugin fixture capabilityTranscript must contain at most ${MAX_FIXTURE_CAPABILITY_STEPS} steps`,
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
return Object.freeze(value.map((candidate, index) => {
|
|
1082
|
+
if (!isRecord(candidate)) {
|
|
1083
|
+
throw new Error(
|
|
1084
|
+
`portable plugin fixture capabilityTranscript[${index}] must be an object`,
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
exactKeys(
|
|
1088
|
+
candidate,
|
|
1089
|
+
["request", "result"],
|
|
1090
|
+
`portable plugin fixture capabilityTranscript[${index}]`,
|
|
1091
|
+
);
|
|
1092
|
+
const request = parseFixtureCapabilityRequest(candidate.request, index);
|
|
1093
|
+
return Object.freeze({
|
|
1094
|
+
request,
|
|
1095
|
+
result: parseFixtureCapabilityResult(candidate.result, request, index),
|
|
1096
|
+
});
|
|
1097
|
+
}));
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function parseFixtureFiles(
|
|
1101
|
+
value: unknown,
|
|
1102
|
+
route: PortablePluginRoute,
|
|
1103
|
+
input: PortablePluginJsonObject,
|
|
1104
|
+
auth: Omit<PortablePluginInvocationAuth, "handle">,
|
|
1105
|
+
): readonly PortablePluginInvocationFile[] {
|
|
1106
|
+
const parsed = parsePortableProviderPluginMessage({
|
|
1107
|
+
protocolVersion: PORTABLE_PROVIDER_PLUGIN_PROTOCOL_VERSION,
|
|
1108
|
+
kind: "host.invoke",
|
|
1109
|
+
invocationId: "fixture-invocation",
|
|
1110
|
+
route,
|
|
1111
|
+
input,
|
|
1112
|
+
auth: { ...auth, handle: "fixture-auth" },
|
|
1113
|
+
files: value === undefined ? [] : value,
|
|
1114
|
+
timeoutMs: 5_000,
|
|
1115
|
+
});
|
|
1116
|
+
if (!parsed.ok) {
|
|
1117
|
+
throw new Error(
|
|
1118
|
+
`portable plugin fixture files are invalid: ${parsed.issues.join("; ")}`,
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
if (parsed.value.kind !== "host.invoke") {
|
|
1122
|
+
throw new Error("portable plugin fixture file parser drifted");
|
|
1123
|
+
}
|
|
1124
|
+
return parsed.value.files;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
function parseFixture(
|
|
1128
|
+
value: unknown,
|
|
1129
|
+
): PortableProviderPluginFixtureV1 {
|
|
1130
|
+
if (!isRecord(value)) throw new Error("portable plugin fixture must be an object");
|
|
1131
|
+
exactKeys(
|
|
1132
|
+
value,
|
|
1133
|
+
[
|
|
1134
|
+
"schemaVersion",
|
|
1135
|
+
"route",
|
|
1136
|
+
"input",
|
|
1137
|
+
"auth",
|
|
1138
|
+
...(value.files === undefined ? [] : ["files"]),
|
|
1139
|
+
...(value.capabilityTranscript === undefined
|
|
1140
|
+
? []
|
|
1141
|
+
: ["capabilityTranscript"]),
|
|
1142
|
+
"expected",
|
|
1143
|
+
],
|
|
1144
|
+
"portable plugin fixture",
|
|
1145
|
+
);
|
|
1146
|
+
if (value.schemaVersion !== 1) {
|
|
1147
|
+
throw new Error("portable plugin fixture schemaVersion is unsupported");
|
|
1148
|
+
}
|
|
1149
|
+
if (!isRecord(value.route)) throw new Error("portable plugin fixture route is invalid");
|
|
1150
|
+
exactKeys(
|
|
1151
|
+
value.route,
|
|
1152
|
+
["transport", "surfaceId", "operation", "contractVersion"],
|
|
1153
|
+
"portable plugin fixture route",
|
|
1154
|
+
);
|
|
1155
|
+
if (
|
|
1156
|
+
value.route.transport !== "provider-api"
|
|
1157
|
+
&& value.route.transport !== "web-session-api"
|
|
1158
|
+
&& value.route.transport !== "linked-device"
|
|
1159
|
+
) {
|
|
1160
|
+
throw new Error("portable plugin fixture transport is invalid");
|
|
1161
|
+
}
|
|
1162
|
+
if (
|
|
1163
|
+
typeof value.route.surfaceId !== "string"
|
|
1164
|
+
|| typeof value.route.operation !== "string"
|
|
1165
|
+
|| typeof value.route.contractVersion !== "number"
|
|
1166
|
+
|| !Number.isSafeInteger(value.route.contractVersion)
|
|
1167
|
+
) {
|
|
1168
|
+
throw new Error("portable plugin fixture route is invalid");
|
|
1169
|
+
}
|
|
1170
|
+
if (!isRecord(value.auth)) throw new Error("portable plugin fixture auth is invalid");
|
|
1171
|
+
const authKeys = [
|
|
1172
|
+
"kind",
|
|
1173
|
+
...(value.auth.subject === undefined ? [] : ["subject"]),
|
|
1174
|
+
];
|
|
1175
|
+
exactKeys(value.auth, authKeys, "portable plugin fixture auth");
|
|
1176
|
+
if (
|
|
1177
|
+
value.auth.kind !== "cookie-source"
|
|
1178
|
+
&& value.auth.kind !== "cookies-file"
|
|
1179
|
+
&& value.auth.kind !== "browser-profile"
|
|
1180
|
+
&& value.auth.kind !== "oauth-token-file"
|
|
1181
|
+
&& value.auth.kind !== "linked-device-store"
|
|
1182
|
+
) {
|
|
1183
|
+
throw new Error("portable plugin fixture auth kind is invalid");
|
|
1184
|
+
}
|
|
1185
|
+
if (
|
|
1186
|
+
value.auth.subject !== undefined
|
|
1187
|
+
&& typeof value.auth.subject !== "string"
|
|
1188
|
+
) {
|
|
1189
|
+
throw new Error("portable plugin fixture auth subject is invalid");
|
|
1190
|
+
}
|
|
1191
|
+
if (!isRecord(value.expected)) {
|
|
1192
|
+
throw new Error("portable plugin fixture expected result is invalid");
|
|
1193
|
+
}
|
|
1194
|
+
exactKeys(
|
|
1195
|
+
value.expected,
|
|
1196
|
+
["output", "finalUrl"],
|
|
1197
|
+
"portable plugin fixture expected result",
|
|
1198
|
+
);
|
|
1199
|
+
if (
|
|
1200
|
+
value.expected.finalUrl !== null
|
|
1201
|
+
&& typeof value.expected.finalUrl !== "string"
|
|
1202
|
+
) {
|
|
1203
|
+
throw new Error("portable plugin fixture finalUrl is invalid");
|
|
1204
|
+
}
|
|
1205
|
+
const route: PortablePluginRoute = Object.freeze({
|
|
1206
|
+
transport: value.route.transport,
|
|
1207
|
+
surfaceId: value.route.surfaceId,
|
|
1208
|
+
operation: value.route.operation,
|
|
1209
|
+
contractVersion: value.route.contractVersion,
|
|
1210
|
+
});
|
|
1211
|
+
const input = normalizePortablePluginJsonObject(
|
|
1212
|
+
value.input,
|
|
1213
|
+
"portable plugin fixture input",
|
|
1214
|
+
);
|
|
1215
|
+
const auth: Omit<PortablePluginInvocationAuth, "handle"> = Object.freeze({
|
|
1216
|
+
kind: value.auth.kind,
|
|
1217
|
+
...(value.auth.subject === undefined
|
|
1218
|
+
? {}
|
|
1219
|
+
: { subject: value.auth.subject }),
|
|
1220
|
+
});
|
|
1221
|
+
return Object.freeze({
|
|
1222
|
+
schemaVersion: 1,
|
|
1223
|
+
route,
|
|
1224
|
+
input,
|
|
1225
|
+
auth,
|
|
1226
|
+
files: parseFixtureFiles(value.files, route, input, auth),
|
|
1227
|
+
capabilityTranscript: parseFixtureCapabilityTranscript(
|
|
1228
|
+
value.capabilityTranscript === undefined
|
|
1229
|
+
? []
|
|
1230
|
+
: value.capabilityTranscript,
|
|
1231
|
+
),
|
|
1232
|
+
expected: Object.freeze({
|
|
1233
|
+
output: normalizePortablePluginJsonValue(
|
|
1234
|
+
value.expected.output,
|
|
1235
|
+
"portable plugin fixture expected output",
|
|
1236
|
+
),
|
|
1237
|
+
finalUrl: value.expected.finalUrl,
|
|
1238
|
+
}),
|
|
1239
|
+
});
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function fixtureCapabilityHost(
|
|
1243
|
+
fixture: PortableProviderPluginFixtureV1,
|
|
1244
|
+
): {
|
|
1245
|
+
readonly host: PortableProviderPluginCapabilityHost;
|
|
1246
|
+
readonly assertSatisfied: () => void;
|
|
1247
|
+
} {
|
|
1248
|
+
const label = `${fixture.route.surfaceId}/${fixture.route.operation}`;
|
|
1249
|
+
let next = 0;
|
|
1250
|
+
let mismatch: string | null = null;
|
|
1251
|
+
const fail = (message: string): never => {
|
|
1252
|
+
mismatch ??= message;
|
|
1253
|
+
throw new Error(message);
|
|
1254
|
+
};
|
|
1255
|
+
return Object.freeze({
|
|
1256
|
+
host: Object.freeze({
|
|
1257
|
+
handle: (
|
|
1258
|
+
request: PortablePluginCapabilityRequest,
|
|
1259
|
+
context: PortableProviderPluginCapabilityContext,
|
|
1260
|
+
): Promise<PortablePluginCapabilityResult> => {
|
|
1261
|
+
if (context.signal.aborted) {
|
|
1262
|
+
throw new Error("portable plugin fixture capability was cancelled");
|
|
1263
|
+
}
|
|
1264
|
+
if (mismatch !== null) throw new Error(mismatch);
|
|
1265
|
+
const step = fixture.capabilityTranscript[next];
|
|
1266
|
+
if (step === undefined) {
|
|
1267
|
+
return fail(
|
|
1268
|
+
`portable plugin fixture ${label} emitted unexpected ${request.kind} capability after its transcript ended`,
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
if (canonicalJson(step.request) !== canonicalJson(request)) {
|
|
1272
|
+
return fail(
|
|
1273
|
+
`portable plugin fixture ${label} capability step ${next + 1} expected ${step.request.kind} with different fields than the emitted ${request.kind} request`,
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
1276
|
+
next += 1;
|
|
1277
|
+
return Promise.resolve(step.result);
|
|
1278
|
+
},
|
|
1279
|
+
}),
|
|
1280
|
+
assertSatisfied: () => {
|
|
1281
|
+
if (mismatch !== null) throw new Error(mismatch);
|
|
1282
|
+
const step = fixture.capabilityTranscript[next];
|
|
1283
|
+
if (step !== undefined) {
|
|
1284
|
+
throw new Error(
|
|
1285
|
+
`portable plugin fixture ${label} returned before capability step ${next + 1} (${step.request.kind})`,
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
},
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function fixturePath(
|
|
1293
|
+
packageValue: VerifiedPortableProviderPluginPackage,
|
|
1294
|
+
route: PortablePluginRoute,
|
|
1295
|
+
): string {
|
|
1296
|
+
return join(
|
|
1297
|
+
packageValue.root,
|
|
1298
|
+
"fixtures",
|
|
1299
|
+
`${route.surfaceId}.${route.operation}.v${route.contractVersion}.json`,
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
export async function testPortableProviderPlugin(
|
|
1304
|
+
pathValue: string,
|
|
1305
|
+
options: {
|
|
1306
|
+
readonly trustExecutableCode: boolean;
|
|
1307
|
+
},
|
|
1308
|
+
): Promise<{
|
|
1309
|
+
readonly ok: true;
|
|
1310
|
+
readonly pluginId: string;
|
|
1311
|
+
readonly bundleSha256: string;
|
|
1312
|
+
readonly inert: number;
|
|
1313
|
+
readonly executed: number;
|
|
1314
|
+
readonly fixtures: readonly {
|
|
1315
|
+
readonly route: PortablePluginRoute;
|
|
1316
|
+
readonly result: PortableProviderPluginHostResult;
|
|
1317
|
+
}[];
|
|
1318
|
+
}> {
|
|
1319
|
+
if (!options.trustExecutableCode) {
|
|
1320
|
+
throw new Error(
|
|
1321
|
+
"portable plugin test requires --trust-code because fixtures execute package code in a child process, not a hostile-code sandbox",
|
|
1322
|
+
);
|
|
1323
|
+
}
|
|
1324
|
+
const packageValue = verifyPortableProviderPluginPackageDirectory(pathValue);
|
|
1325
|
+
const approval = parsePortableProviderPluginTrustApproval({
|
|
1326
|
+
decision: "trust-executable-code",
|
|
1327
|
+
pluginId: packageValue.manifest.id,
|
|
1328
|
+
pluginVersion: packageValue.manifest.version,
|
|
1329
|
+
bundleSha256: packageValue.bundleSha256,
|
|
1330
|
+
});
|
|
1331
|
+
assertPortableProviderPluginTrustApprovalMatches(approval, packageValue);
|
|
1332
|
+
let inert = 0;
|
|
1333
|
+
let executed = 0;
|
|
1334
|
+
const fixtures: {
|
|
1335
|
+
readonly route: PortablePluginRoute;
|
|
1336
|
+
readonly result: PortableProviderPluginHostResult;
|
|
1337
|
+
}[] = [];
|
|
1338
|
+
for (const binding of packageValue.manifest.bindings) {
|
|
1339
|
+
for (const operation of binding.operations) {
|
|
1340
|
+
const route: PortablePluginRoute = {
|
|
1341
|
+
transport: binding.transport,
|
|
1342
|
+
surfaceId: binding.surfaceId,
|
|
1343
|
+
operation: operation.name,
|
|
1344
|
+
contractVersion: operation.contractVersion,
|
|
1345
|
+
};
|
|
1346
|
+
if (operation.state === "capture-required") {
|
|
1347
|
+
inert += 1;
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
const text = readRegularFile(
|
|
1351
|
+
fixturePath(packageValue, route),
|
|
1352
|
+
MAX_FIXTURE_BYTES,
|
|
1353
|
+
"portable plugin fixture",
|
|
1354
|
+
);
|
|
1355
|
+
let value: unknown;
|
|
1356
|
+
try {
|
|
1357
|
+
value = JSON.parse(text) as unknown;
|
|
1358
|
+
} catch {
|
|
1359
|
+
throw new Error("portable plugin fixture must contain JSON");
|
|
1360
|
+
}
|
|
1361
|
+
const fixture = parseFixture(value);
|
|
1362
|
+
if (canonicalJson(fixture.route) !== canonicalJson(route)) {
|
|
1363
|
+
throw new Error("portable plugin fixture route does not match its operation");
|
|
1364
|
+
}
|
|
1365
|
+
const transcript = fixtureCapabilityHost(fixture);
|
|
1366
|
+
let result: PortableProviderPluginHostResult;
|
|
1367
|
+
try {
|
|
1368
|
+
result = await runPortableProviderPluginHost({
|
|
1369
|
+
package: packageValue,
|
|
1370
|
+
route,
|
|
1371
|
+
input: fixture.input,
|
|
1372
|
+
auth: {
|
|
1373
|
+
...fixture.auth,
|
|
1374
|
+
handle: "fixture-auth",
|
|
1375
|
+
},
|
|
1376
|
+
files: fixture.files,
|
|
1377
|
+
timeoutMs: 5_000,
|
|
1378
|
+
hostVersion: "1.0.0",
|
|
1379
|
+
plannedDispatchIds: operation.dispatch === "single"
|
|
1380
|
+
? [operation.name]
|
|
1381
|
+
: [],
|
|
1382
|
+
// The exact fixture transcript is the only installed capability host.
|
|
1383
|
+
// It returns no ambient credentials and performs no network or file IO.
|
|
1384
|
+
capabilityHost: transcript.host,
|
|
1385
|
+
});
|
|
1386
|
+
} finally {
|
|
1387
|
+
transcript.assertSatisfied();
|
|
1388
|
+
}
|
|
1389
|
+
if (
|
|
1390
|
+
canonicalJson(result.output) !== canonicalJson(fixture.expected.output)
|
|
1391
|
+
|| result.finalUrl !== fixture.expected.finalUrl
|
|
1392
|
+
) {
|
|
1393
|
+
throw new Error(
|
|
1394
|
+
`portable plugin fixture ${route.surfaceId}/${route.operation} returned an unexpected result`,
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
executed += 1;
|
|
1398
|
+
fixtures.push(Object.freeze({ route, result }));
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
return Object.freeze({
|
|
1402
|
+
ok: true,
|
|
1403
|
+
pluginId: packageValue.manifest.id,
|
|
1404
|
+
bundleSha256: packageValue.bundleSha256,
|
|
1405
|
+
inert,
|
|
1406
|
+
executed,
|
|
1407
|
+
fixtures: Object.freeze(fixtures),
|
|
1408
|
+
});
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
export function doctorPortableProviderPlugins(
|
|
1412
|
+
environment: Environment = process.env,
|
|
1413
|
+
): {
|
|
1414
|
+
readonly ok: boolean;
|
|
1415
|
+
readonly installed: number;
|
|
1416
|
+
readonly issues: readonly string[];
|
|
1417
|
+
readonly plugins: readonly PortableProviderPluginSummary[];
|
|
1418
|
+
readonly invocationLeases:
|
|
1419
|
+
PortableProviderPluginInvocationLeaseRepairReport | null;
|
|
1420
|
+
} {
|
|
1421
|
+
try {
|
|
1422
|
+
const plugins = listPortableProviderPlugins(environment);
|
|
1423
|
+
const invocationLeases =
|
|
1424
|
+
recoverPortableProviderPluginInvocationLeaseTombstones(environment);
|
|
1425
|
+
const issues = [
|
|
1426
|
+
...(invocationLeases.invalid === 0
|
|
1427
|
+
? []
|
|
1428
|
+
: [
|
|
1429
|
+
`${invocationLeases.invalid} portable invocation lease(s) are invalid`,
|
|
1430
|
+
]),
|
|
1431
|
+
...(invocationLeases.unknown === 0
|
|
1432
|
+
? []
|
|
1433
|
+
: [
|
|
1434
|
+
`${invocationLeases.unknown} portable invocation lease owner(s) are unverifiable`,
|
|
1435
|
+
]),
|
|
1436
|
+
];
|
|
1437
|
+
return Object.freeze({
|
|
1438
|
+
ok: issues.length === 0,
|
|
1439
|
+
installed: plugins.length,
|
|
1440
|
+
issues: Object.freeze(issues),
|
|
1441
|
+
plugins,
|
|
1442
|
+
invocationLeases,
|
|
1443
|
+
});
|
|
1444
|
+
} catch {
|
|
1445
|
+
return Object.freeze({
|
|
1446
|
+
ok: false,
|
|
1447
|
+
installed: 0,
|
|
1448
|
+
issues: Object.freeze([
|
|
1449
|
+
"portable plugin store is invalid; no portable plugin was loaded",
|
|
1450
|
+
]),
|
|
1451
|
+
plugins: Object.freeze([]),
|
|
1452
|
+
invocationLeases: null,
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
}
|