@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,2057 @@
|
|
|
1
|
+
import { acquireCookieRecords, type CookieSelection } from "@hraness/kb/clip/acquire";
|
|
2
|
+
import { filterCookies, renderCookieHeader } from "@hraness/kb/clip/cookies";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import {
|
|
5
|
+
chmodSync,
|
|
6
|
+
closeSync,
|
|
7
|
+
constants,
|
|
8
|
+
fstatSync,
|
|
9
|
+
fsyncSync,
|
|
10
|
+
mkdtempSync,
|
|
11
|
+
openSync,
|
|
12
|
+
readSync,
|
|
13
|
+
rmSync,
|
|
14
|
+
unlinkSync,
|
|
15
|
+
writeSync,
|
|
16
|
+
} from "node:fs";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
|
|
20
|
+
import type { GhostgetAuth } from "./auth";
|
|
21
|
+
import type { BrowserFileResolver } from "./browser";
|
|
22
|
+
import {
|
|
23
|
+
type FileInputValue,
|
|
24
|
+
type GhostgetManifest,
|
|
25
|
+
type OperationInput,
|
|
26
|
+
canonicalJson,
|
|
27
|
+
} from "./model";
|
|
28
|
+
import { OperationDeadline } from "./operation-deadline";
|
|
29
|
+
import {
|
|
30
|
+
createPinnedHttpsFetchScope,
|
|
31
|
+
type PinnedHttpsFetch,
|
|
32
|
+
type PinnedHttpsFetchScope,
|
|
33
|
+
} from "./pinned-https";
|
|
34
|
+
import {
|
|
35
|
+
type ProviderPluginBindingDefinitionV1,
|
|
36
|
+
type ProviderPluginOperationDefinitionV1,
|
|
37
|
+
type ProviderApiPluginOperationDefinitionV1,
|
|
38
|
+
type WebSessionPluginOperationDefinitionV1,
|
|
39
|
+
type ProviderPluginSubjectDefinitionV1,
|
|
40
|
+
type ProviderPluginSubjectProbeOptionsV1,
|
|
41
|
+
} from "./provider-plugin";
|
|
42
|
+
import {
|
|
43
|
+
runPortableProviderPluginHost,
|
|
44
|
+
type PortableProviderPluginCapabilityHost,
|
|
45
|
+
type PortableProviderPluginHostResult,
|
|
46
|
+
} from "./provider-plugin-host";
|
|
47
|
+
import {
|
|
48
|
+
registerPortableProviderPluginCleanupBarrier,
|
|
49
|
+
settlePortableProviderPluginCleanup,
|
|
50
|
+
trackPortableProviderPluginHostCompletion,
|
|
51
|
+
type PortableProviderPluginCleanupBarrier,
|
|
52
|
+
} from "./provider-plugin-cleanup-barrier";
|
|
53
|
+
import {
|
|
54
|
+
acquirePortableProviderPluginInvocationLease,
|
|
55
|
+
createPortableProviderPluginInvocationLeaseContainmentController,
|
|
56
|
+
releasePortableProviderPluginInvocationLease,
|
|
57
|
+
} from "./provider-plugin-invocation-lease";
|
|
58
|
+
import type {
|
|
59
|
+
PortableProviderPluginBindingV1,
|
|
60
|
+
PortableProviderPluginOperationV1,
|
|
61
|
+
VerifiedPortableProviderPluginPackage,
|
|
62
|
+
} from "./provider-plugin-package";
|
|
63
|
+
import {
|
|
64
|
+
createPortableOperationIdentityV1,
|
|
65
|
+
} from "./provider-plugin-portable-identity";
|
|
66
|
+
import type {
|
|
67
|
+
PortablePluginCapabilityRequest,
|
|
68
|
+
PortablePluginCapabilityResult,
|
|
69
|
+
PortablePluginHttpBody,
|
|
70
|
+
PortablePluginInvocationFile,
|
|
71
|
+
PortablePluginJsonObject,
|
|
72
|
+
PortablePluginJsonValue,
|
|
73
|
+
PortablePluginVersionedStateResult,
|
|
74
|
+
} from "./provider-plugin-protocol";
|
|
75
|
+
import {
|
|
76
|
+
normalizePortablePluginJsonObject,
|
|
77
|
+
normalizePortablePluginJsonValue,
|
|
78
|
+
} from "./provider-plugin-protocol";
|
|
79
|
+
import {
|
|
80
|
+
loadOAuthToken,
|
|
81
|
+
} from "./provider-http";
|
|
82
|
+
import { summarizePlanFile } from "./plan-assets";
|
|
83
|
+
import {
|
|
84
|
+
createPrivateJsonIfAbsent,
|
|
85
|
+
ghostgetStateHome,
|
|
86
|
+
readPrivateStateFileIfPresent,
|
|
87
|
+
removePrivateStateFile,
|
|
88
|
+
removePrivateStateFileIfUnchanged,
|
|
89
|
+
writePrivateJson,
|
|
90
|
+
writePrivateJsonIfUnchanged,
|
|
91
|
+
} from "./storage";
|
|
92
|
+
import type {
|
|
93
|
+
ProviderActionContext,
|
|
94
|
+
} from "./provider";
|
|
95
|
+
import type {
|
|
96
|
+
WebSessionDispatchEvent,
|
|
97
|
+
WebSessionExecution,
|
|
98
|
+
WebSessionOperationDeadline,
|
|
99
|
+
WebSessionOperationExecutor,
|
|
100
|
+
} from "./web-session-execution";
|
|
101
|
+
|
|
102
|
+
type Environment = Readonly<Record<string, string | undefined>>;
|
|
103
|
+
|
|
104
|
+
const PORTABLE_HOST_VERSION = "io-portable-host-v1";
|
|
105
|
+
const WEB_SESSION_OPERATION_LABEL = "authenticated web operation deadline";
|
|
106
|
+
const MAX_PORTABLE_STATE_BYTES = 512 * 1024;
|
|
107
|
+
const MAX_PORTABLE_RESPONSE_HEADERS = 64;
|
|
108
|
+
const RESPONSE_HEADER_ALLOWLIST = new Set([
|
|
109
|
+
"cache-control",
|
|
110
|
+
"content-language",
|
|
111
|
+
"content-length",
|
|
112
|
+
"content-type",
|
|
113
|
+
"etag",
|
|
114
|
+
"expires",
|
|
115
|
+
"last-modified",
|
|
116
|
+
"location",
|
|
117
|
+
"retry-after",
|
|
118
|
+
"vary",
|
|
119
|
+
"x-ratelimit-limit",
|
|
120
|
+
"x-ratelimit-remaining",
|
|
121
|
+
"x-ratelimit-reset",
|
|
122
|
+
]);
|
|
123
|
+
const FORBIDDEN_REQUEST_HEADERS = new Set([
|
|
124
|
+
"authorization",
|
|
125
|
+
"connection",
|
|
126
|
+
"content-length",
|
|
127
|
+
"cookie",
|
|
128
|
+
"host",
|
|
129
|
+
"proxy-authorization",
|
|
130
|
+
"proxy-connection",
|
|
131
|
+
"set-cookie",
|
|
132
|
+
"te",
|
|
133
|
+
"trailer",
|
|
134
|
+
"transfer-encoding",
|
|
135
|
+
"upgrade",
|
|
136
|
+
"x-api-key",
|
|
137
|
+
]);
|
|
138
|
+
|
|
139
|
+
export type PortableProviderRuntimeDependencies = {
|
|
140
|
+
readonly runHost: typeof runPortableProviderPluginHost;
|
|
141
|
+
readonly createFetchScope: typeof createPinnedHttpsFetchScope;
|
|
142
|
+
readonly acquireCookies: typeof acquireCookieRecords;
|
|
143
|
+
readonly loadToken: typeof loadOAuthToken;
|
|
144
|
+
readonly closeBoundFile: (descriptor: number) => void;
|
|
145
|
+
/** Internal deterministic seam for pre-transfer descriptor cleanup. */
|
|
146
|
+
readonly closePlanFileBindingDescriptor: (descriptor: number) => void;
|
|
147
|
+
/** Internal deterministic seam for the private snapshot staging directory. */
|
|
148
|
+
readonly removePlanFileBindingDirectory: (
|
|
149
|
+
path: string,
|
|
150
|
+
force: boolean,
|
|
151
|
+
) => void;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const defaultDependencies: PortableProviderRuntimeDependencies = {
|
|
155
|
+
runHost: runPortableProviderPluginHost,
|
|
156
|
+
createFetchScope: createPinnedHttpsFetchScope,
|
|
157
|
+
acquireCookies: acquireCookieRecords,
|
|
158
|
+
loadToken: loadOAuthToken,
|
|
159
|
+
closeBoundFile: closeSync,
|
|
160
|
+
closePlanFileBindingDescriptor: closeSync,
|
|
161
|
+
removePlanFileBindingDirectory: (path, force) =>
|
|
162
|
+
rmSync(path, { recursive: true, force }),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const resolvedPortableProviderRuntimeDependencies = new WeakSet<object>();
|
|
166
|
+
const portableRuntimeDependencyKeys = Object.freeze([
|
|
167
|
+
"runHost",
|
|
168
|
+
"createFetchScope",
|
|
169
|
+
"acquireCookies",
|
|
170
|
+
"loadToken",
|
|
171
|
+
"closeBoundFile",
|
|
172
|
+
"closePlanFileBindingDescriptor",
|
|
173
|
+
"removePlanFileBindingDirectory",
|
|
174
|
+
] as const);
|
|
175
|
+
|
|
176
|
+
export function resolvePortableProviderRuntimeDependencies(
|
|
177
|
+
overrides: Partial<PortableProviderRuntimeDependencies>,
|
|
178
|
+
): PortableProviderRuntimeDependencies {
|
|
179
|
+
if (
|
|
180
|
+
typeof overrides !== "object"
|
|
181
|
+
|| overrides === null
|
|
182
|
+
|| Array.isArray(overrides)
|
|
183
|
+
) {
|
|
184
|
+
throw new Error(
|
|
185
|
+
"portable provider runtime dependency overrides must be a plain data object",
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
const actualKeys = Reflect.ownKeys(overrides);
|
|
189
|
+
const allowedKeys = new Set<PropertyKey>(portableRuntimeDependencyKeys);
|
|
190
|
+
if (actualKeys.some((key) => !allowedKeys.has(key))) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
"portable provider runtime dependency overrides contain unsupported keys",
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
if (
|
|
196
|
+
actualKeys.length > 0
|
|
197
|
+
&& process.env.NODE_ENV !== "test"
|
|
198
|
+
) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
"portable provider runtime dependency overrides are available only in tests",
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
const snapshot: Partial<PortableProviderRuntimeDependencies> = {};
|
|
204
|
+
for (const key of actualKeys) {
|
|
205
|
+
if (typeof key !== "string") {
|
|
206
|
+
throw new Error(
|
|
207
|
+
"portable provider runtime dependency overrides contain unsupported keys",
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
const descriptor = Object.getOwnPropertyDescriptor(overrides, key);
|
|
211
|
+
if (
|
|
212
|
+
descriptor === undefined
|
|
213
|
+
|| !descriptor.enumerable
|
|
214
|
+
|| !("value" in descriptor)
|
|
215
|
+
|| typeof descriptor.value !== "function"
|
|
216
|
+
) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`portable provider runtime dependency override ${key} must be an enumerable function data property`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
Object.defineProperty(snapshot, key, {
|
|
222
|
+
value: descriptor.value,
|
|
223
|
+
enumerable: true,
|
|
224
|
+
configurable: false,
|
|
225
|
+
writable: false,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
const dependencies = Object.freeze({
|
|
229
|
+
...defaultDependencies,
|
|
230
|
+
...snapshot,
|
|
231
|
+
});
|
|
232
|
+
resolvedPortableProviderRuntimeDependencies.add(dependencies);
|
|
233
|
+
return dependencies;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function isResolvedPortableProviderRuntimeDependencies(
|
|
237
|
+
value: unknown,
|
|
238
|
+
): value is PortableProviderRuntimeDependencies {
|
|
239
|
+
return (
|
|
240
|
+
typeof value === "object"
|
|
241
|
+
&& value !== null
|
|
242
|
+
&& resolvedPortableProviderRuntimeDependencies.has(value)
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export type KernelPortableProviderPluginBindingProjection = {
|
|
247
|
+
readonly adapterId: string;
|
|
248
|
+
readonly manifest: GhostgetManifest;
|
|
249
|
+
readonly portableBinding: PortableProviderPluginBindingV1;
|
|
250
|
+
readonly binding: ProviderPluginBindingDefinitionV1;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
type BoundInvocationFile = {
|
|
254
|
+
readonly descriptor: number;
|
|
255
|
+
readonly dispose: () => void;
|
|
256
|
+
readonly protocol: PortablePluginInvocationFile;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
class UnverifiedPlanFileBindingCleanupError extends Error {
|
|
260
|
+
constructor(resource: string, cause: unknown) {
|
|
261
|
+
super(
|
|
262
|
+
`portable plugin plan-file ${resource} cleanup could not be verified`,
|
|
263
|
+
{ cause },
|
|
264
|
+
);
|
|
265
|
+
this.name = "UnverifiedPlanFileBindingCleanupError";
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function disposePortableRuntimeResources(
|
|
270
|
+
cleanupBarrier: PortableProviderPluginCleanupBarrier,
|
|
271
|
+
fetchScope: PinnedHttpsFetchScope | undefined,
|
|
272
|
+
files: readonly BoundInvocationFile[],
|
|
273
|
+
): void {
|
|
274
|
+
let cleanupError: unknown = null;
|
|
275
|
+
try {
|
|
276
|
+
fetchScope?.close();
|
|
277
|
+
} catch (error) {
|
|
278
|
+
cleanupError = error;
|
|
279
|
+
}
|
|
280
|
+
for (const file of files) {
|
|
281
|
+
try {
|
|
282
|
+
file.dispose();
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (cleanupError === null) cleanupError = error;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (cleanupError === null) {
|
|
288
|
+
cleanupBarrier.verified();
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
cleanupBarrier.unsafe(cleanupError);
|
|
292
|
+
throw new Error(
|
|
293
|
+
"portable provider plugin runtime resource cleanup failed",
|
|
294
|
+
{ cause: cleanupError },
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
type DispatchBoundary = {
|
|
299
|
+
readonly verify: () => Promise<void>;
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
type PortableCapabilityActivity = {
|
|
303
|
+
readonly begin: () => () => void;
|
|
304
|
+
readonly whenIdle: () => Promise<void>;
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const PORTABLE_CAPABILITY_ABORT_JOIN_MS = 500;
|
|
308
|
+
|
|
309
|
+
async function settlesWithin<T>(
|
|
310
|
+
operation: Promise<T>,
|
|
311
|
+
maximumMs: number,
|
|
312
|
+
): Promise<PromiseSettledResult<T> | null> {
|
|
313
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
314
|
+
try {
|
|
315
|
+
return await Promise.race([
|
|
316
|
+
operation.then(
|
|
317
|
+
(value): PromiseFulfilledResult<T> => ({
|
|
318
|
+
status: "fulfilled",
|
|
319
|
+
value,
|
|
320
|
+
}),
|
|
321
|
+
(reason: unknown): PromiseRejectedResult => ({
|
|
322
|
+
status: "rejected",
|
|
323
|
+
reason,
|
|
324
|
+
}),
|
|
325
|
+
),
|
|
326
|
+
new Promise<null>((resolve) => {
|
|
327
|
+
timer = setTimeout(() => resolve(null), maximumMs);
|
|
328
|
+
}),
|
|
329
|
+
]);
|
|
330
|
+
} finally {
|
|
331
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function createPortableCapabilityActivity(): PortableCapabilityActivity {
|
|
336
|
+
let active = 0;
|
|
337
|
+
const waiters = new Set<() => void>();
|
|
338
|
+
return Object.freeze({
|
|
339
|
+
begin: () => {
|
|
340
|
+
active += 1;
|
|
341
|
+
let ended = false;
|
|
342
|
+
return () => {
|
|
343
|
+
if (ended) return;
|
|
344
|
+
ended = true;
|
|
345
|
+
active -= 1;
|
|
346
|
+
if (active !== 0) return;
|
|
347
|
+
for (const resolve of waiters) resolve();
|
|
348
|
+
waiters.clear();
|
|
349
|
+
};
|
|
350
|
+
},
|
|
351
|
+
whenIdle: () => active === 0
|
|
352
|
+
? Promise.resolve()
|
|
353
|
+
: new Promise<void>((resolve) => {
|
|
354
|
+
waiters.add(resolve);
|
|
355
|
+
}),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
type CapabilityMaterial =
|
|
360
|
+
| { readonly kind: "oauth-access-token"; readonly value: string }
|
|
361
|
+
| { readonly kind: "cookie-jar"; readonly auth: GhostgetAuth };
|
|
362
|
+
|
|
363
|
+
function sha256(value: string): string {
|
|
364
|
+
return createHash("sha256").update(value).digest("hex");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function portableSubject(
|
|
368
|
+
value: PortableProviderPluginBindingV1["subject"],
|
|
369
|
+
): ProviderPluginSubjectDefinitionV1 {
|
|
370
|
+
const matches = (candidate: string): boolean => {
|
|
371
|
+
if (
|
|
372
|
+
typeof candidate !== "string"
|
|
373
|
+
|| candidate.length < 1
|
|
374
|
+
|| Buffer.byteLength(candidate, "utf8") > 256
|
|
375
|
+
|| /[\0-\x20\x7f]/u.test(candidate)
|
|
376
|
+
) return false;
|
|
377
|
+
if (value.kind === "opaque-token") {
|
|
378
|
+
return /^[A-Za-z0-9][A-Za-z0-9._:@/+~-]{0,255}$/u.test(candidate);
|
|
379
|
+
}
|
|
380
|
+
if (value.kind === "decimal") return /^(?:0|[1-9][0-9]{0,127})$/u.test(candidate);
|
|
381
|
+
if (value.kind === "did") {
|
|
382
|
+
return /^did:[a-z0-9]+:[A-Za-z0-9._:%-]{1,220}$/u.test(candidate);
|
|
383
|
+
}
|
|
384
|
+
if (value.kind === "uuid") {
|
|
385
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(candidate);
|
|
386
|
+
}
|
|
387
|
+
return /^\+[1-9][0-9]{6,14}$/u.test(candidate);
|
|
388
|
+
};
|
|
389
|
+
return Object.freeze({ format: value.format, matches });
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function plannedDispatches(
|
|
393
|
+
operation: PortableProviderPluginOperationV1,
|
|
394
|
+
): ProviderPluginOperationDefinitionV1["planDispatches"] {
|
|
395
|
+
if (operation.dispatch === "none") return () => Object.freeze([]);
|
|
396
|
+
const schedule = Object.freeze([Object.freeze({
|
|
397
|
+
id: operation.name,
|
|
398
|
+
description: operation.sideEffect,
|
|
399
|
+
})]);
|
|
400
|
+
return () => schedule;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function portableOperation(
|
|
404
|
+
operation: PortableProviderPluginOperationV1,
|
|
405
|
+
): ProviderPluginOperationDefinitionV1 {
|
|
406
|
+
const common = {
|
|
407
|
+
name: operation.name,
|
|
408
|
+
contractVersion: operation.contractVersion,
|
|
409
|
+
risk: operation.risk,
|
|
410
|
+
input: operation.input,
|
|
411
|
+
sideEffect: operation.sideEffect,
|
|
412
|
+
idempotency: operation.idempotency,
|
|
413
|
+
dedupeWindowMs: operation.dedupeWindowMs,
|
|
414
|
+
state: operation.state,
|
|
415
|
+
dispatch: operation.dispatch,
|
|
416
|
+
implementation: operation.implementation,
|
|
417
|
+
planDispatches: plannedDispatches(operation),
|
|
418
|
+
validateInput: () => Object.freeze([]),
|
|
419
|
+
...(
|
|
420
|
+
operation.name === "messaging.list"
|
|
421
|
+
|| operation.name === "messaging.read"
|
|
422
|
+
? {
|
|
423
|
+
omni: Object.freeze({
|
|
424
|
+
state: "unsupported" as const,
|
|
425
|
+
reason: "portable provider plugins do not yet attest a versioned output materializer",
|
|
426
|
+
}),
|
|
427
|
+
}
|
|
428
|
+
: {}
|
|
429
|
+
),
|
|
430
|
+
} as const;
|
|
431
|
+
return (
|
|
432
|
+
operation.requiredScopeSets !== undefined
|
|
433
|
+
&& operation.coverage !== undefined
|
|
434
|
+
)
|
|
435
|
+
? {
|
|
436
|
+
...common,
|
|
437
|
+
requiredScopeSets: operation.requiredScopeSets,
|
|
438
|
+
coverage: operation.coverage,
|
|
439
|
+
}
|
|
440
|
+
: common;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function portableProviderOperation(
|
|
444
|
+
operation: Extract<
|
|
445
|
+
PortableProviderPluginOperationV1,
|
|
446
|
+
{ readonly requiredScopeSets: readonly (readonly string[])[] }
|
|
447
|
+
>,
|
|
448
|
+
): ProviderApiPluginOperationDefinitionV1 {
|
|
449
|
+
return portableOperation(operation) as ProviderApiPluginOperationDefinitionV1;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function portableSessionOperation(
|
|
453
|
+
operation: Exclude<
|
|
454
|
+
PortableProviderPluginOperationV1,
|
|
455
|
+
{ readonly requiredScopeSets: readonly (readonly string[])[] }
|
|
456
|
+
>,
|
|
457
|
+
): WebSessionPluginOperationDefinitionV1 {
|
|
458
|
+
return portableOperation(operation) as WebSessionPluginOperationDefinitionV1;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function freezePortableProjectionValue<T>(value: T): T {
|
|
462
|
+
if (typeof value !== "object" || value === null || Object.isFrozen(value)) {
|
|
463
|
+
return value;
|
|
464
|
+
}
|
|
465
|
+
for (const entry of Object.values(value as Record<string, unknown>)) {
|
|
466
|
+
freezePortableProjectionValue(entry);
|
|
467
|
+
}
|
|
468
|
+
return Object.freeze(value);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function operationManifest(
|
|
472
|
+
binding: PortableProviderPluginBindingV1,
|
|
473
|
+
operation: PortableProviderPluginOperationV1,
|
|
474
|
+
): GhostgetManifest["operations"][string] {
|
|
475
|
+
const common = {
|
|
476
|
+
description: operation.implementation,
|
|
477
|
+
risk: operation.risk,
|
|
478
|
+
sideEffect: operation.sideEffect,
|
|
479
|
+
idempotency: operation.idempotency,
|
|
480
|
+
dedupeWindowMs: operation.dedupeWindowMs,
|
|
481
|
+
input: operation.input,
|
|
482
|
+
};
|
|
483
|
+
return binding.transport === "provider-api"
|
|
484
|
+
? {
|
|
485
|
+
...common,
|
|
486
|
+
provider: {
|
|
487
|
+
provider: binding.surfaceId,
|
|
488
|
+
action: operation.name,
|
|
489
|
+
contractVersion: operation.contractVersion,
|
|
490
|
+
timeoutMs: operation.timeoutMs,
|
|
491
|
+
maxOutputBytes: operation.maxOutputBytes,
|
|
492
|
+
},
|
|
493
|
+
}
|
|
494
|
+
: {
|
|
495
|
+
...common,
|
|
496
|
+
webSession: {
|
|
497
|
+
site: binding.surfaceId,
|
|
498
|
+
action: operation.name,
|
|
499
|
+
contractVersion: operation.contractVersion,
|
|
500
|
+
timeoutMs: operation.timeoutMs,
|
|
501
|
+
maxOutputBytes: operation.maxOutputBytes,
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function virtualManifest(
|
|
507
|
+
packageValue: VerifiedPortableProviderPluginPackage,
|
|
508
|
+
binding: PortableProviderPluginBindingV1,
|
|
509
|
+
): GhostgetManifest {
|
|
510
|
+
const operations = Object.fromEntries(
|
|
511
|
+
binding.operations.map((operation) => [
|
|
512
|
+
operation.name,
|
|
513
|
+
operationManifest(binding, operation),
|
|
514
|
+
]),
|
|
515
|
+
);
|
|
516
|
+
return freezePortableProjectionValue({
|
|
517
|
+
schemaVersion: binding.transport === "provider-api" ? 3 : 4,
|
|
518
|
+
id: binding.adapterId,
|
|
519
|
+
version: packageValue.manifest.version,
|
|
520
|
+
displayName: packageValue.manifest.displayName,
|
|
521
|
+
surfaceId: binding.surfaceId,
|
|
522
|
+
origins: Object.freeze([binding.origin]),
|
|
523
|
+
browserDomains: Object.freeze([new URL(binding.origin).hostname]),
|
|
524
|
+
operations,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function cookieSelection(auth: GhostgetAuth, timeoutMs: number): CookieSelection {
|
|
529
|
+
if (auth.kind === "cookie-source") {
|
|
530
|
+
return {
|
|
531
|
+
cookieSources: [auth.source],
|
|
532
|
+
cookiesFile: undefined,
|
|
533
|
+
cookieProfile: auth.profile,
|
|
534
|
+
timeoutMs,
|
|
535
|
+
requireExplicitCookieScope: true,
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
if (auth.kind === "cookies-file") {
|
|
539
|
+
return {
|
|
540
|
+
cookieSources: [],
|
|
541
|
+
cookiesFile: auth.path,
|
|
542
|
+
cookieProfile: undefined,
|
|
543
|
+
timeoutMs,
|
|
544
|
+
requireExplicitCookieScope: true,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
if (auth.kind === "browser-profile" && auth.cookieSource !== undefined) {
|
|
548
|
+
return {
|
|
549
|
+
cookieSources: [auth.cookieSource],
|
|
550
|
+
cookiesFile: undefined,
|
|
551
|
+
cookieProfile: auth.cookieProfile,
|
|
552
|
+
timeoutMs,
|
|
553
|
+
requireExplicitCookieScope: true,
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
throw new Error("portable cookie material requires an explicit cookie source");
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function bodyBytes(body: PortablePluginHttpBody): ArrayBuffer | undefined {
|
|
560
|
+
if (body.kind === "none") return undefined;
|
|
561
|
+
const source = body.kind === "utf8"
|
|
562
|
+
? new TextEncoder().encode(body.text)
|
|
563
|
+
: Buffer.from(body.data, "base64");
|
|
564
|
+
const owned = new Uint8Array(source.byteLength);
|
|
565
|
+
owned.set(source);
|
|
566
|
+
return owned.buffer;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
async function boundedResponseBytes(
|
|
570
|
+
response: Response,
|
|
571
|
+
maximumBytes: number,
|
|
572
|
+
cleanupBarrier: PortableProviderPluginCleanupBarrier,
|
|
573
|
+
operationDeadline?: WebSessionOperationDeadline,
|
|
574
|
+
): Promise<Uint8Array> {
|
|
575
|
+
if (response.body === null) {
|
|
576
|
+
operationDeadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
|
|
577
|
+
return new Uint8Array();
|
|
578
|
+
}
|
|
579
|
+
const reader = response.body.getReader();
|
|
580
|
+
const chunks: Uint8Array[] = [];
|
|
581
|
+
let total = 0;
|
|
582
|
+
let pendingRead: ReturnType<typeof reader.read> | null = null;
|
|
583
|
+
let failure: unknown;
|
|
584
|
+
let failed = false;
|
|
585
|
+
try {
|
|
586
|
+
operationDeadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
|
|
587
|
+
for (;;) {
|
|
588
|
+
pendingRead = reader.read();
|
|
589
|
+
const next = operationDeadline === undefined
|
|
590
|
+
? await pendingRead
|
|
591
|
+
: await operationDeadline.run(
|
|
592
|
+
() => pendingRead as ReturnType<typeof reader.read>,
|
|
593
|
+
WEB_SESSION_OPERATION_LABEL,
|
|
594
|
+
);
|
|
595
|
+
pendingRead = null;
|
|
596
|
+
if (next.done) break;
|
|
597
|
+
const chunk: unknown = next.value;
|
|
598
|
+
if (!(chunk instanceof Uint8Array)) {
|
|
599
|
+
throw new Error("portable plugin HTTP response returned non-byte data");
|
|
600
|
+
}
|
|
601
|
+
total += chunk.byteLength;
|
|
602
|
+
if (total > maximumBytes) {
|
|
603
|
+
throw new Error("portable plugin HTTP response exceeded its declared bound");
|
|
604
|
+
}
|
|
605
|
+
chunks.push(chunk);
|
|
606
|
+
}
|
|
607
|
+
} catch (error) {
|
|
608
|
+
failed = true;
|
|
609
|
+
failure = error;
|
|
610
|
+
const cancellation = Promise.resolve().then(() =>
|
|
611
|
+
reader.cancel("portable plugin response processing stopped")
|
|
612
|
+
);
|
|
613
|
+
const quiescence = Promise.allSettled([
|
|
614
|
+
...(pendingRead === null ? [] : [pendingRead]),
|
|
615
|
+
cancellation,
|
|
616
|
+
]).then((results) => {
|
|
617
|
+
const cancellationResult = results.at(-1);
|
|
618
|
+
if (cancellationResult?.status === "rejected") {
|
|
619
|
+
throw cancellationResult.reason;
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
const settled = await settlesWithin(
|
|
623
|
+
quiescence,
|
|
624
|
+
PORTABLE_CAPABILITY_ABORT_JOIN_MS,
|
|
625
|
+
);
|
|
626
|
+
if (settled === null || settled.status === "rejected") {
|
|
627
|
+
const cleanupError = new Error(
|
|
628
|
+
"portable plugin HTTP response cleanup did not settle after cancellation",
|
|
629
|
+
settled?.status === "rejected"
|
|
630
|
+
? { cause: settled.reason }
|
|
631
|
+
: undefined,
|
|
632
|
+
);
|
|
633
|
+
cleanupBarrier.unsafe(cleanupError);
|
|
634
|
+
failure = new AggregateError(
|
|
635
|
+
[error, cleanupError],
|
|
636
|
+
"portable plugin HTTP response processing and cleanup failed",
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
} finally {
|
|
640
|
+
try {
|
|
641
|
+
reader.releaseLock();
|
|
642
|
+
} catch (error) {
|
|
643
|
+
const cleanupError = new Error(
|
|
644
|
+
"portable plugin HTTP response reader lock could not be released",
|
|
645
|
+
{ cause: error },
|
|
646
|
+
);
|
|
647
|
+
cleanupBarrier.unsafe(cleanupError);
|
|
648
|
+
failure = failed
|
|
649
|
+
? new AggregateError(
|
|
650
|
+
[failure, cleanupError],
|
|
651
|
+
"portable plugin HTTP response processing and cleanup failed",
|
|
652
|
+
)
|
|
653
|
+
: cleanupError;
|
|
654
|
+
failed = true;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
if (failed) throw failure;
|
|
658
|
+
operationDeadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
|
|
659
|
+
const output = new Uint8Array(total);
|
|
660
|
+
let offset = 0;
|
|
661
|
+
for (const chunk of chunks) {
|
|
662
|
+
output.set(chunk, offset);
|
|
663
|
+
offset += chunk.byteLength;
|
|
664
|
+
}
|
|
665
|
+
return output;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function responseHeaders(response: Response): readonly {
|
|
669
|
+
readonly name: string;
|
|
670
|
+
readonly value: string;
|
|
671
|
+
}[] {
|
|
672
|
+
const values: { readonly name: string; readonly value: string }[] = [];
|
|
673
|
+
response.headers.forEach((value, name) => {
|
|
674
|
+
const normalized = name.toLowerCase();
|
|
675
|
+
if (
|
|
676
|
+
values.length < MAX_PORTABLE_RESPONSE_HEADERS
|
|
677
|
+
&& RESPONSE_HEADER_ALLOWLIST.has(normalized)
|
|
678
|
+
&& !/[\0\r\n]/u.test(value)
|
|
679
|
+
&& Buffer.byteLength(value, "utf8") <= 8_192
|
|
680
|
+
) {
|
|
681
|
+
values.push(Object.freeze({ name: normalized, value }));
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
return Object.freeze(values);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function responseBody(bytes: Uint8Array): {
|
|
688
|
+
readonly kind: "utf8";
|
|
689
|
+
readonly text: string;
|
|
690
|
+
} | {
|
|
691
|
+
readonly kind: "base64";
|
|
692
|
+
readonly data: string;
|
|
693
|
+
} {
|
|
694
|
+
try {
|
|
695
|
+
return Object.freeze({
|
|
696
|
+
kind: "utf8" as const,
|
|
697
|
+
text: new TextDecoder("utf-8", { fatal: true }).decode(bytes),
|
|
698
|
+
});
|
|
699
|
+
} catch {
|
|
700
|
+
return Object.freeze({
|
|
701
|
+
kind: "base64" as const,
|
|
702
|
+
data: Buffer.from(bytes).toString("base64"),
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function statePath(
|
|
708
|
+
packageValue: VerifiedPortableProviderPluginPackage,
|
|
709
|
+
binding: PortableProviderPluginBindingV1,
|
|
710
|
+
auth: GhostgetAuth,
|
|
711
|
+
key: string,
|
|
712
|
+
environment: Environment,
|
|
713
|
+
): string {
|
|
714
|
+
return join(
|
|
715
|
+
ghostgetStateHome(environment),
|
|
716
|
+
"provider-plugin-state",
|
|
717
|
+
packageValue.manifest.id,
|
|
718
|
+
packageValue.bundleSha256,
|
|
719
|
+
binding.adapterId,
|
|
720
|
+
sha256(canonicalJson(auth)),
|
|
721
|
+
`${sha256(key)}.json`,
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function parseStoredState(
|
|
726
|
+
content: string,
|
|
727
|
+
key: string,
|
|
728
|
+
includeVersion: boolean,
|
|
729
|
+
): PortablePluginCapabilityResult {
|
|
730
|
+
let value: unknown;
|
|
731
|
+
try {
|
|
732
|
+
value = JSON.parse(content) as unknown;
|
|
733
|
+
} catch {
|
|
734
|
+
throw new Error("portable plugin namespaced state is malformed");
|
|
735
|
+
}
|
|
736
|
+
if (
|
|
737
|
+
typeof value !== "object"
|
|
738
|
+
|| value === null
|
|
739
|
+
|| Array.isArray(value)
|
|
740
|
+
) {
|
|
741
|
+
throw new Error("portable plugin namespaced state is malformed");
|
|
742
|
+
}
|
|
743
|
+
const record = value as Record<string, unknown>;
|
|
744
|
+
const legacy = record.schemaVersion === 1
|
|
745
|
+
&& Object.keys(record).sort().join(",") === "key,schemaVersion,value";
|
|
746
|
+
const current = record.schemaVersion === 2
|
|
747
|
+
&& Object.keys(record).sort().join(",")
|
|
748
|
+
=== "key,revision,schemaVersion,value"
|
|
749
|
+
&& typeof record.revision === "string"
|
|
750
|
+
&& /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu
|
|
751
|
+
.test(record.revision);
|
|
752
|
+
if ((!legacy && !current) || record.key !== key) {
|
|
753
|
+
throw new Error("portable plugin namespaced state identity is malformed");
|
|
754
|
+
}
|
|
755
|
+
const result = {
|
|
756
|
+
kind: "state.read",
|
|
757
|
+
found: true,
|
|
758
|
+
value: normalizePortablePluginJsonValue(
|
|
759
|
+
record.value,
|
|
760
|
+
"portable plugin namespaced state value",
|
|
761
|
+
),
|
|
762
|
+
} as const;
|
|
763
|
+
if (!includeVersion) return result;
|
|
764
|
+
const versioned: PortablePluginVersionedStateResult = {
|
|
765
|
+
...result,
|
|
766
|
+
version: sha256(content),
|
|
767
|
+
};
|
|
768
|
+
return versioned;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function portableStateRecord(
|
|
772
|
+
key: string,
|
|
773
|
+
value: PortablePluginJsonValue,
|
|
774
|
+
): Readonly<{
|
|
775
|
+
readonly schemaVersion: 2;
|
|
776
|
+
readonly key: string;
|
|
777
|
+
readonly revision: string;
|
|
778
|
+
readonly value: PortablePluginJsonValue;
|
|
779
|
+
}> {
|
|
780
|
+
return Object.freeze({
|
|
781
|
+
schemaVersion: 2 as const,
|
|
782
|
+
key,
|
|
783
|
+
revision: randomUUID(),
|
|
784
|
+
value: normalizePortablePluginJsonValue(
|
|
785
|
+
value,
|
|
786
|
+
"portable plugin namespaced state value",
|
|
787
|
+
),
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function requestHeaders(
|
|
792
|
+
request: Extract<PortablePluginCapabilityRequest, { readonly kind: "http.request" }>,
|
|
793
|
+
materials: ReadonlyMap<string, CapabilityMaterial>,
|
|
794
|
+
): Headers {
|
|
795
|
+
const headers = new Headers();
|
|
796
|
+
for (const header of request.headers) {
|
|
797
|
+
const name = header.name.toLowerCase();
|
|
798
|
+
if (FORBIDDEN_REQUEST_HEADERS.has(name)) {
|
|
799
|
+
throw new Error("portable plugin requested a forbidden HTTP header");
|
|
800
|
+
}
|
|
801
|
+
headers.append(name, header.value);
|
|
802
|
+
}
|
|
803
|
+
for (const credential of request.credentials) {
|
|
804
|
+
const material = materials.get(credential.handle);
|
|
805
|
+
if (material === undefined) {
|
|
806
|
+
throw new Error("portable plugin requested an unknown material handle");
|
|
807
|
+
}
|
|
808
|
+
if (
|
|
809
|
+
material.kind === "oauth-access-token"
|
|
810
|
+
&& credential.sink.kind === "header"
|
|
811
|
+
&& credential.sink.name.toLowerCase() === "authorization"
|
|
812
|
+
) {
|
|
813
|
+
if (headers.has("authorization")) {
|
|
814
|
+
throw new Error("portable plugin repeated the authorization sink");
|
|
815
|
+
}
|
|
816
|
+
headers.set("authorization", `Bearer ${material.value}`);
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
if (
|
|
820
|
+
material.kind === "cookie-jar"
|
|
821
|
+
&& credential.sink.kind === "cookie-jar"
|
|
822
|
+
) {
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
throw new Error("portable plugin requested an incompatible credential sink");
|
|
826
|
+
}
|
|
827
|
+
if (request.body.kind !== "none") {
|
|
828
|
+
if (headers.has("content-type")) {
|
|
829
|
+
throw new Error("portable plugin body media type owns content-type");
|
|
830
|
+
}
|
|
831
|
+
headers.set("content-type", request.body.mediaType);
|
|
832
|
+
}
|
|
833
|
+
return headers;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
async function injectCookieMaterials(
|
|
837
|
+
request: Extract<PortablePluginCapabilityRequest, { readonly kind: "http.request" }>,
|
|
838
|
+
materials: ReadonlyMap<string, CapabilityMaterial>,
|
|
839
|
+
headers: Headers,
|
|
840
|
+
dependencies: PortableProviderRuntimeDependencies,
|
|
841
|
+
signal: AbortSignal,
|
|
842
|
+
cleanupBarrier: PortableProviderPluginCleanupBarrier,
|
|
843
|
+
operationDeadline?: WebSessionOperationDeadline,
|
|
844
|
+
): Promise<void> {
|
|
845
|
+
for (const credential of request.credentials) {
|
|
846
|
+
const material = materials.get(credential.handle);
|
|
847
|
+
if (
|
|
848
|
+
material?.kind !== "cookie-jar"
|
|
849
|
+
|| credential.sink.kind !== "cookie-jar"
|
|
850
|
+
) continue;
|
|
851
|
+
if (headers.has("cookie")) {
|
|
852
|
+
throw new Error("portable plugin repeated the cookie sink");
|
|
853
|
+
}
|
|
854
|
+
operationDeadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
|
|
855
|
+
if (signal.aborted) throw new Error("portable plugin invocation was cancelled");
|
|
856
|
+
const target = new URL(request.url);
|
|
857
|
+
const timeoutMs = operationDeadline === undefined
|
|
858
|
+
? request.timeoutMs
|
|
859
|
+
: Math.min(request.timeoutMs, operationDeadline.remainingTimeMs());
|
|
860
|
+
if (timeoutMs < 1) {
|
|
861
|
+
throw new Error("portable plugin operation deadline was exhausted");
|
|
862
|
+
}
|
|
863
|
+
const acquire = () => dependencies.acquireCookies(
|
|
864
|
+
cookieSelection(material.auth, timeoutMs),
|
|
865
|
+
target,
|
|
866
|
+
);
|
|
867
|
+
const acquisition = acquire();
|
|
868
|
+
let acquired: Awaited<ReturnType<typeof acquire>>;
|
|
869
|
+
try {
|
|
870
|
+
acquired = operationDeadline === undefined
|
|
871
|
+
? await acquisition
|
|
872
|
+
: await operationDeadline.run(
|
|
873
|
+
() => acquisition,
|
|
874
|
+
WEB_SESSION_OPERATION_LABEL,
|
|
875
|
+
);
|
|
876
|
+
} catch (error) {
|
|
877
|
+
if (operationDeadline?.signal.aborted === true) {
|
|
878
|
+
await settlesWithin(
|
|
879
|
+
acquisition,
|
|
880
|
+
PORTABLE_CAPABILITY_ABORT_JOIN_MS,
|
|
881
|
+
);
|
|
882
|
+
// The pinned sweet-cookie dependency resolves its timeout before its
|
|
883
|
+
// spawned keyring helper emits close. Its Promise therefore cannot
|
|
884
|
+
// prove process quiescence after a deadline wins the race.
|
|
885
|
+
cleanupBarrier.unsafe(new Error(
|
|
886
|
+
"portable plugin cookie acquisition outlived its request deadline",
|
|
887
|
+
));
|
|
888
|
+
}
|
|
889
|
+
throw error;
|
|
890
|
+
}
|
|
891
|
+
const filtered = filterCookies(acquired.cookies, target);
|
|
892
|
+
if (
|
|
893
|
+
filtered.rejected !== 0
|
|
894
|
+
|| filtered.cookies.length !== acquired.cookies.length
|
|
895
|
+
) {
|
|
896
|
+
throw new Error("portable plugin cookie source escaped its exact request scope");
|
|
897
|
+
}
|
|
898
|
+
headers.set("cookie", renderCookieHeader(filtered.cookies));
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function capabilityHost(options: {
|
|
903
|
+
readonly package: VerifiedPortableProviderPluginPackage;
|
|
904
|
+
readonly binding: PortableProviderPluginBindingV1;
|
|
905
|
+
readonly auth: GhostgetAuth;
|
|
906
|
+
readonly environment: Environment;
|
|
907
|
+
readonly files: ReadonlyMap<string, BoundInvocationFile>;
|
|
908
|
+
readonly beginDispatch: (dispatchId: string) => Promise<DispatchBoundary>;
|
|
909
|
+
readonly dependencies: PortableProviderRuntimeDependencies;
|
|
910
|
+
readonly fetch: PinnedHttpsFetch;
|
|
911
|
+
readonly activity: PortableCapabilityActivity;
|
|
912
|
+
readonly cleanupBarrier: PortableProviderPluginCleanupBarrier;
|
|
913
|
+
readonly operationDeadline?: WebSessionOperationDeadline;
|
|
914
|
+
}): PortableProviderPluginCapabilityHost {
|
|
915
|
+
const materials = new Map<string, CapabilityMaterial>();
|
|
916
|
+
const dispatches = new Map<string, DispatchBoundary>();
|
|
917
|
+
return Object.freeze({
|
|
918
|
+
handle: async (
|
|
919
|
+
request,
|
|
920
|
+
context,
|
|
921
|
+
): Promise<PortablePluginCapabilityResult> => {
|
|
922
|
+
const endActivity = options.activity.begin();
|
|
923
|
+
try {
|
|
924
|
+
options.operationDeadline?.throwIfUnavailable(
|
|
925
|
+
WEB_SESSION_OPERATION_LABEL,
|
|
926
|
+
);
|
|
927
|
+
if (request.kind === "dispatch.begin") {
|
|
928
|
+
const boundary = await options.beginDispatch(request.dispatchId);
|
|
929
|
+
const dispatchHandle = `dispatch-${randomUUID()}`;
|
|
930
|
+
dispatches.set(dispatchHandle, boundary);
|
|
931
|
+
return { kind: "dispatch.begin", dispatchHandle };
|
|
932
|
+
}
|
|
933
|
+
if (request.kind === "dispatch.verify") {
|
|
934
|
+
const boundary = dispatches.get(request.dispatchHandle);
|
|
935
|
+
if (boundary === undefined) {
|
|
936
|
+
throw new Error("portable plugin requested an unknown dispatch boundary");
|
|
937
|
+
}
|
|
938
|
+
await boundary.verify();
|
|
939
|
+
dispatches.delete(request.dispatchHandle);
|
|
940
|
+
return { kind: "dispatch.verify", verified: true };
|
|
941
|
+
}
|
|
942
|
+
if (request.kind === "session.acquire") {
|
|
943
|
+
let material: CapabilityMaterial;
|
|
944
|
+
if (
|
|
945
|
+
request.name === "oauth-access-token"
|
|
946
|
+
&& options.binding.transport === "provider-api"
|
|
947
|
+
&& options.auth.kind === "oauth-token-file"
|
|
948
|
+
) {
|
|
949
|
+
const token = options.dependencies.loadToken(
|
|
950
|
+
options.auth,
|
|
951
|
+
new Date(),
|
|
952
|
+
30_000,
|
|
953
|
+
);
|
|
954
|
+
material = { kind: "oauth-access-token", value: token.accessToken };
|
|
955
|
+
} else if (
|
|
956
|
+
request.name === "cookie-jar"
|
|
957
|
+
&& options.binding.transport === "web-session-api"
|
|
958
|
+
&& (
|
|
959
|
+
options.auth.kind === "cookie-source"
|
|
960
|
+
|| options.auth.kind === "cookies-file"
|
|
961
|
+
|| options.auth.kind === "browser-profile"
|
|
962
|
+
)
|
|
963
|
+
) {
|
|
964
|
+
material = { kind: "cookie-jar", auth: options.auth };
|
|
965
|
+
} else {
|
|
966
|
+
throw new Error("portable plugin requested unsupported session material");
|
|
967
|
+
}
|
|
968
|
+
const materialHandle = `material-${randomUUID()}`;
|
|
969
|
+
materials.set(materialHandle, material);
|
|
970
|
+
return { kind: "session.acquire", materialHandle };
|
|
971
|
+
}
|
|
972
|
+
if (request.kind === "http.request") {
|
|
973
|
+
if (new URL(request.url).origin !== options.binding.origin) {
|
|
974
|
+
throw new Error(
|
|
975
|
+
"portable plugin HTTP request escaped its binding's exact origin",
|
|
976
|
+
);
|
|
977
|
+
}
|
|
978
|
+
const headers = requestHeaders(request, materials);
|
|
979
|
+
const timeoutMs = Math.min(
|
|
980
|
+
request.timeoutMs,
|
|
981
|
+
options.operationDeadline?.remainingTimeMs() ?? request.timeoutMs,
|
|
982
|
+
);
|
|
983
|
+
if (timeoutMs < 1) {
|
|
984
|
+
throw new Error("portable plugin operation deadline was exhausted");
|
|
985
|
+
}
|
|
986
|
+
const requestDeadline = new OperationDeadline(timeoutMs, {
|
|
987
|
+
signal: options.operationDeadline?.signal ?? context.signal,
|
|
988
|
+
});
|
|
989
|
+
let response: Response | undefined;
|
|
990
|
+
try {
|
|
991
|
+
await injectCookieMaterials(
|
|
992
|
+
request,
|
|
993
|
+
materials,
|
|
994
|
+
headers,
|
|
995
|
+
options.dependencies,
|
|
996
|
+
requestDeadline.signal,
|
|
997
|
+
options.cleanupBarrier,
|
|
998
|
+
requestDeadline,
|
|
999
|
+
);
|
|
1000
|
+
const fetchOperation = Promise.resolve().then(() =>
|
|
1001
|
+
options.fetch(
|
|
1002
|
+
new URL(request.url),
|
|
1003
|
+
{
|
|
1004
|
+
method: request.method,
|
|
1005
|
+
headers,
|
|
1006
|
+
...(request.body.kind === "none"
|
|
1007
|
+
? {}
|
|
1008
|
+
: { body: bodyBytes(request.body) }),
|
|
1009
|
+
redirect: "error",
|
|
1010
|
+
signal: requestDeadline.signal,
|
|
1011
|
+
},
|
|
1012
|
+
timeoutMs,
|
|
1013
|
+
)
|
|
1014
|
+
);
|
|
1015
|
+
try {
|
|
1016
|
+
response = await requestDeadline.run(
|
|
1017
|
+
() => fetchOperation,
|
|
1018
|
+
WEB_SESSION_OPERATION_LABEL,
|
|
1019
|
+
);
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
if (requestDeadline.signal.aborted) {
|
|
1022
|
+
const quiescence = fetchOperation.then(
|
|
1023
|
+
async (lateResponse) => {
|
|
1024
|
+
await lateResponse.body?.cancel(
|
|
1025
|
+
"portable plugin request deadline expired",
|
|
1026
|
+
);
|
|
1027
|
+
},
|
|
1028
|
+
() => undefined,
|
|
1029
|
+
);
|
|
1030
|
+
const settled = await settlesWithin(
|
|
1031
|
+
quiescence,
|
|
1032
|
+
PORTABLE_CAPABILITY_ABORT_JOIN_MS,
|
|
1033
|
+
);
|
|
1034
|
+
if (settled === null || settled.status === "rejected") {
|
|
1035
|
+
options.cleanupBarrier.unsafe(new Error(
|
|
1036
|
+
"portable plugin HTTP request cleanup did not settle after cancellation",
|
|
1037
|
+
settled?.status === "rejected"
|
|
1038
|
+
? { cause: settled.reason }
|
|
1039
|
+
: undefined,
|
|
1040
|
+
));
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
throw error;
|
|
1044
|
+
}
|
|
1045
|
+
const bytes = await boundedResponseBytes(
|
|
1046
|
+
response,
|
|
1047
|
+
request.maxOutputBytes,
|
|
1048
|
+
options.cleanupBarrier,
|
|
1049
|
+
requestDeadline,
|
|
1050
|
+
);
|
|
1051
|
+
return {
|
|
1052
|
+
kind: "http.request",
|
|
1053
|
+
status: response.status,
|
|
1054
|
+
headers: responseHeaders(response),
|
|
1055
|
+
body: responseBody(bytes),
|
|
1056
|
+
finalUrl: request.url,
|
|
1057
|
+
};
|
|
1058
|
+
} finally {
|
|
1059
|
+
const requestAborted = requestDeadline.signal.aborted;
|
|
1060
|
+
requestDeadline.dispose();
|
|
1061
|
+
if (
|
|
1062
|
+
requestAborted
|
|
1063
|
+
&& response?.body !== null
|
|
1064
|
+
&& response?.body !== undefined
|
|
1065
|
+
&& !response.bodyUsed
|
|
1066
|
+
) {
|
|
1067
|
+
const settled = await settlesWithin(
|
|
1068
|
+
response.body.cancel(
|
|
1069
|
+
"portable plugin request deadline expired",
|
|
1070
|
+
),
|
|
1071
|
+
PORTABLE_CAPABILITY_ABORT_JOIN_MS,
|
|
1072
|
+
);
|
|
1073
|
+
if (settled === null || settled.status === "rejected") {
|
|
1074
|
+
options.cleanupBarrier.unsafe(new Error(
|
|
1075
|
+
"portable plugin HTTP response cleanup did not settle after cancellation",
|
|
1076
|
+
settled?.status === "rejected"
|
|
1077
|
+
? { cause: settled.reason }
|
|
1078
|
+
: undefined,
|
|
1079
|
+
));
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
if (request.kind === "file.read") {
|
|
1085
|
+
const file = options.files.get(request.handle);
|
|
1086
|
+
if (file === undefined) {
|
|
1087
|
+
throw new Error("portable plugin requested an unknown file handle");
|
|
1088
|
+
}
|
|
1089
|
+
const buffer = Buffer.alloc(request.length);
|
|
1090
|
+
const read = readSync(
|
|
1091
|
+
file.descriptor,
|
|
1092
|
+
buffer,
|
|
1093
|
+
0,
|
|
1094
|
+
request.length,
|
|
1095
|
+
request.offset,
|
|
1096
|
+
);
|
|
1097
|
+
return {
|
|
1098
|
+
kind: "file.read",
|
|
1099
|
+
data: buffer.subarray(0, read).toString("base64"),
|
|
1100
|
+
eof: request.offset + read >= file.protocol.bytes,
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
if (request.kind === "state.read") {
|
|
1104
|
+
const content = readPrivateStateFileIfPresent(
|
|
1105
|
+
statePath(
|
|
1106
|
+
options.package,
|
|
1107
|
+
options.binding,
|
|
1108
|
+
options.auth,
|
|
1109
|
+
request.key,
|
|
1110
|
+
options.environment,
|
|
1111
|
+
),
|
|
1112
|
+
MAX_PORTABLE_STATE_BYTES,
|
|
1113
|
+
"portable plugin namespaced state",
|
|
1114
|
+
options.environment,
|
|
1115
|
+
);
|
|
1116
|
+
if (content !== null) {
|
|
1117
|
+
return parseStoredState(
|
|
1118
|
+
content,
|
|
1119
|
+
request.key,
|
|
1120
|
+
request.includeVersion === true,
|
|
1121
|
+
);
|
|
1122
|
+
}
|
|
1123
|
+
if (request.includeVersion !== true) {
|
|
1124
|
+
return { kind: "state.read", found: false };
|
|
1125
|
+
}
|
|
1126
|
+
const missing: PortablePluginVersionedStateResult = {
|
|
1127
|
+
kind: "state.read",
|
|
1128
|
+
found: false,
|
|
1129
|
+
version: null,
|
|
1130
|
+
};
|
|
1131
|
+
return missing;
|
|
1132
|
+
}
|
|
1133
|
+
if (request.kind === "state.write") {
|
|
1134
|
+
const path = statePath(
|
|
1135
|
+
options.package,
|
|
1136
|
+
options.binding,
|
|
1137
|
+
options.auth,
|
|
1138
|
+
request.key,
|
|
1139
|
+
options.environment,
|
|
1140
|
+
);
|
|
1141
|
+
const state = portableStateRecord(request.key, request.value);
|
|
1142
|
+
const serialized = `${canonicalJson(state)}\n`;
|
|
1143
|
+
if (
|
|
1144
|
+
Buffer.byteLength(serialized, "utf8")
|
|
1145
|
+
> MAX_PORTABLE_STATE_BYTES
|
|
1146
|
+
) {
|
|
1147
|
+
throw new Error(
|
|
1148
|
+
"portable plugin namespaced state exceeds its persisted byte bound",
|
|
1149
|
+
);
|
|
1150
|
+
}
|
|
1151
|
+
let stored: boolean;
|
|
1152
|
+
if (request.expectedVersion === undefined) {
|
|
1153
|
+
writePrivateJson(path, state, { privateParent: true });
|
|
1154
|
+
stored = true;
|
|
1155
|
+
} else if (request.expectedVersion === null) {
|
|
1156
|
+
stored = createPrivateJsonIfAbsent(path, state, {
|
|
1157
|
+
environment: options.environment,
|
|
1158
|
+
privateParent: true,
|
|
1159
|
+
}).created;
|
|
1160
|
+
} else {
|
|
1161
|
+
stored = writePrivateJsonIfUnchanged(path, state, {
|
|
1162
|
+
expectedCurrentContentSha256: request.expectedVersion,
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
if (!stored) {
|
|
1166
|
+
throw new Error("portable plugin state version conflict");
|
|
1167
|
+
}
|
|
1168
|
+
const result = {
|
|
1169
|
+
kind: "state.write",
|
|
1170
|
+
stored: true,
|
|
1171
|
+
} as const;
|
|
1172
|
+
if (request.expectedVersion === undefined) return result;
|
|
1173
|
+
const versioned: PortablePluginVersionedStateResult = {
|
|
1174
|
+
...result,
|
|
1175
|
+
version: sha256(serialized),
|
|
1176
|
+
};
|
|
1177
|
+
return versioned;
|
|
1178
|
+
}
|
|
1179
|
+
if (request.kind === "state.delete") {
|
|
1180
|
+
const path = statePath(
|
|
1181
|
+
options.package,
|
|
1182
|
+
options.binding,
|
|
1183
|
+
options.auth,
|
|
1184
|
+
request.key,
|
|
1185
|
+
options.environment,
|
|
1186
|
+
);
|
|
1187
|
+
if (request.expectedVersion === undefined) {
|
|
1188
|
+
return {
|
|
1189
|
+
kind: "state.delete",
|
|
1190
|
+
removed: removePrivateStateFile(
|
|
1191
|
+
path,
|
|
1192
|
+
options.environment,
|
|
1193
|
+
),
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
return {
|
|
1197
|
+
kind: "state.delete",
|
|
1198
|
+
removed: removePrivateStateFileIfUnchanged(
|
|
1199
|
+
path,
|
|
1200
|
+
{ expectedCurrentContentSha256: request.expectedVersion },
|
|
1201
|
+
options.environment,
|
|
1202
|
+
),
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
return { kind: "log.write", accepted: true };
|
|
1206
|
+
} finally {
|
|
1207
|
+
endActivity();
|
|
1208
|
+
}
|
|
1209
|
+
},
|
|
1210
|
+
});
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
function fileValues(
|
|
1214
|
+
input: OperationInput,
|
|
1215
|
+
): readonly { readonly input: string; readonly value: FileInputValue }[] {
|
|
1216
|
+
const isFile = (candidate: unknown): candidate is FileInputValue =>
|
|
1217
|
+
typeof candidate === "object"
|
|
1218
|
+
&& candidate !== null
|
|
1219
|
+
&& !Array.isArray(candidate)
|
|
1220
|
+
&& "kind" in candidate
|
|
1221
|
+
&& candidate.kind === "file"
|
|
1222
|
+
&& "reference" in candidate
|
|
1223
|
+
&& typeof candidate.reference === "string";
|
|
1224
|
+
return Object.entries(input).flatMap(([name, value]) => {
|
|
1225
|
+
const values = Array.isArray(value) ? value : [value];
|
|
1226
|
+
return values.flatMap((candidate) =>
|
|
1227
|
+
isFile(candidate)
|
|
1228
|
+
? [{ input: name, value: candidate }]
|
|
1229
|
+
: []);
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
function bindFile(
|
|
1234
|
+
input: string,
|
|
1235
|
+
value: FileInputValue,
|
|
1236
|
+
path: string,
|
|
1237
|
+
cleanupBarrier: PortableProviderPluginCleanupBarrier,
|
|
1238
|
+
closeBoundFile: (descriptor: number) => void,
|
|
1239
|
+
closeBindingDescriptor: (descriptor: number) => void,
|
|
1240
|
+
removeBindingDirectory: (path: string, force: boolean) => void,
|
|
1241
|
+
): BoundInvocationFile {
|
|
1242
|
+
const summary = summarizePlanFile(value);
|
|
1243
|
+
let sourceDescriptor: number | null = openSync(
|
|
1244
|
+
path,
|
|
1245
|
+
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0),
|
|
1246
|
+
);
|
|
1247
|
+
let snapshotDirectory: string | null = null;
|
|
1248
|
+
let writer: number | null = null;
|
|
1249
|
+
let reader: number | null = null;
|
|
1250
|
+
const closeForTransfer = (
|
|
1251
|
+
descriptor: number,
|
|
1252
|
+
resource: string,
|
|
1253
|
+
): void => {
|
|
1254
|
+
try {
|
|
1255
|
+
closeBindingDescriptor(descriptor);
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
throw new UnverifiedPlanFileBindingCleanupError(resource, error);
|
|
1258
|
+
}
|
|
1259
|
+
};
|
|
1260
|
+
const removeForTransfer = (
|
|
1261
|
+
directory: string,
|
|
1262
|
+
force: boolean,
|
|
1263
|
+
): void => {
|
|
1264
|
+
try {
|
|
1265
|
+
removeBindingDirectory(directory, force);
|
|
1266
|
+
} catch (error) {
|
|
1267
|
+
throw new UnverifiedPlanFileBindingCleanupError(
|
|
1268
|
+
"snapshot-directory",
|
|
1269
|
+
error,
|
|
1270
|
+
);
|
|
1271
|
+
}
|
|
1272
|
+
};
|
|
1273
|
+
try {
|
|
1274
|
+
snapshotDirectory = mkdtempSync(
|
|
1275
|
+
join(tmpdir(), "wrench-portable-plan-file-"),
|
|
1276
|
+
);
|
|
1277
|
+
chmodSync(snapshotDirectory, 0o700);
|
|
1278
|
+
const snapshotPath = join(snapshotDirectory, "verified");
|
|
1279
|
+
const before = fstatSync(sourceDescriptor, { bigint: true });
|
|
1280
|
+
if (!before.isFile() || before.size !== BigInt(summary.bytes)) {
|
|
1281
|
+
throw new Error("portable plugin plan file changed before invocation");
|
|
1282
|
+
}
|
|
1283
|
+
writer = openSync(
|
|
1284
|
+
snapshotPath,
|
|
1285
|
+
constants.O_WRONLY
|
|
1286
|
+
| constants.O_CREAT
|
|
1287
|
+
| constants.O_EXCL
|
|
1288
|
+
| (constants.O_NOFOLLOW ?? 0),
|
|
1289
|
+
0o600,
|
|
1290
|
+
);
|
|
1291
|
+
const hash = createHash("sha256");
|
|
1292
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
1293
|
+
let total = 0;
|
|
1294
|
+
for (;;) {
|
|
1295
|
+
const count = readSync(
|
|
1296
|
+
sourceDescriptor,
|
|
1297
|
+
buffer,
|
|
1298
|
+
0,
|
|
1299
|
+
buffer.byteLength,
|
|
1300
|
+
total,
|
|
1301
|
+
);
|
|
1302
|
+
if (count === 0) break;
|
|
1303
|
+
total += count;
|
|
1304
|
+
if (total > summary.bytes) {
|
|
1305
|
+
throw new Error("portable plugin plan file grew before invocation");
|
|
1306
|
+
}
|
|
1307
|
+
hash.update(buffer.subarray(0, count));
|
|
1308
|
+
let written = 0;
|
|
1309
|
+
while (written < count) {
|
|
1310
|
+
const countWritten = writeSync(
|
|
1311
|
+
writer,
|
|
1312
|
+
buffer,
|
|
1313
|
+
written,
|
|
1314
|
+
count - written,
|
|
1315
|
+
total - count + written,
|
|
1316
|
+
);
|
|
1317
|
+
if (countWritten < 1) {
|
|
1318
|
+
throw new Error("portable plugin plan-file snapshot write stalled");
|
|
1319
|
+
}
|
|
1320
|
+
written += countWritten;
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
const after = fstatSync(sourceDescriptor, { bigint: true });
|
|
1324
|
+
if (
|
|
1325
|
+
total !== summary.bytes
|
|
1326
|
+
|| hash.digest("hex") !== summary.sha256
|
|
1327
|
+
|| after.dev !== before.dev
|
|
1328
|
+
|| after.ino !== before.ino
|
|
1329
|
+
|| after.size !== before.size
|
|
1330
|
+
|| after.mtimeNs !== before.mtimeNs
|
|
1331
|
+
|| after.ctimeNs !== before.ctimeNs
|
|
1332
|
+
) {
|
|
1333
|
+
throw new Error("portable plugin plan file failed its content hash");
|
|
1334
|
+
}
|
|
1335
|
+
fsyncSync(writer);
|
|
1336
|
+
const verifiedWriter = writer;
|
|
1337
|
+
writer = null;
|
|
1338
|
+
closeForTransfer(verifiedWriter, "snapshot-writer");
|
|
1339
|
+
chmodSync(snapshotPath, 0o400);
|
|
1340
|
+
reader = openSync(
|
|
1341
|
+
snapshotPath,
|
|
1342
|
+
constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0),
|
|
1343
|
+
);
|
|
1344
|
+
const snapshot = fstatSync(reader, { bigint: true });
|
|
1345
|
+
if (!snapshot.isFile() || snapshot.size !== BigInt(summary.bytes)) {
|
|
1346
|
+
throw new Error("portable plugin plan-file snapshot is invalid");
|
|
1347
|
+
}
|
|
1348
|
+
// An unlinked, read-only descriptor is an immutable invocation snapshot:
|
|
1349
|
+
// later source rewrites and pathname replacement cannot affect reads.
|
|
1350
|
+
unlinkSync(snapshotPath);
|
|
1351
|
+
const verifiedDirectory = snapshotDirectory;
|
|
1352
|
+
snapshotDirectory = null;
|
|
1353
|
+
removeForTransfer(verifiedDirectory, false);
|
|
1354
|
+
const verifiedSource = sourceDescriptor;
|
|
1355
|
+
sourceDescriptor = null;
|
|
1356
|
+
closeForTransfer(verifiedSource, "source-descriptor");
|
|
1357
|
+
const handle = `file-${randomUUID()}`;
|
|
1358
|
+
const descriptor = reader;
|
|
1359
|
+
reader = null;
|
|
1360
|
+
let disposed = false;
|
|
1361
|
+
return {
|
|
1362
|
+
descriptor,
|
|
1363
|
+
dispose: () => {
|
|
1364
|
+
if (disposed) return;
|
|
1365
|
+
closeBoundFile(descriptor);
|
|
1366
|
+
disposed = true;
|
|
1367
|
+
},
|
|
1368
|
+
protocol: Object.freeze({
|
|
1369
|
+
input,
|
|
1370
|
+
handle,
|
|
1371
|
+
bytes: summary.bytes,
|
|
1372
|
+
mediaType: summary.mediaType,
|
|
1373
|
+
sha256: summary.sha256,
|
|
1374
|
+
}),
|
|
1375
|
+
};
|
|
1376
|
+
} catch (error) {
|
|
1377
|
+
const cleanupFailures: unknown[] = [];
|
|
1378
|
+
if (error instanceof UnverifiedPlanFileBindingCleanupError) {
|
|
1379
|
+
cleanupFailures.push(error);
|
|
1380
|
+
}
|
|
1381
|
+
const cleanupDescriptor = (
|
|
1382
|
+
descriptor: number,
|
|
1383
|
+
resource: string,
|
|
1384
|
+
): void => {
|
|
1385
|
+
try {
|
|
1386
|
+
closeBindingDescriptor(descriptor);
|
|
1387
|
+
} catch (cleanupError) {
|
|
1388
|
+
cleanupFailures.push(
|
|
1389
|
+
new UnverifiedPlanFileBindingCleanupError(
|
|
1390
|
+
resource,
|
|
1391
|
+
cleanupError,
|
|
1392
|
+
),
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
if (writer !== null) {
|
|
1397
|
+
const pendingWriter = writer;
|
|
1398
|
+
writer = null;
|
|
1399
|
+
cleanupDescriptor(pendingWriter, "snapshot-writer");
|
|
1400
|
+
}
|
|
1401
|
+
if (reader !== null) {
|
|
1402
|
+
const pendingReader = reader;
|
|
1403
|
+
reader = null;
|
|
1404
|
+
cleanupDescriptor(pendingReader, "snapshot-reader");
|
|
1405
|
+
}
|
|
1406
|
+
if (snapshotDirectory !== null) {
|
|
1407
|
+
const pendingDirectory = snapshotDirectory;
|
|
1408
|
+
snapshotDirectory = null;
|
|
1409
|
+
try {
|
|
1410
|
+
removeBindingDirectory(pendingDirectory, true);
|
|
1411
|
+
} catch (cleanupError) {
|
|
1412
|
+
cleanupFailures.push(
|
|
1413
|
+
new UnverifiedPlanFileBindingCleanupError(
|
|
1414
|
+
"snapshot-directory",
|
|
1415
|
+
cleanupError,
|
|
1416
|
+
),
|
|
1417
|
+
);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
if (sourceDescriptor !== null) {
|
|
1421
|
+
const pendingSource = sourceDescriptor;
|
|
1422
|
+
sourceDescriptor = null;
|
|
1423
|
+
cleanupDescriptor(pendingSource, "source-descriptor");
|
|
1424
|
+
}
|
|
1425
|
+
if (cleanupFailures.length > 0) {
|
|
1426
|
+
const cause = new AggregateError(
|
|
1427
|
+
[
|
|
1428
|
+
error,
|
|
1429
|
+
...cleanupFailures.filter((failure) => failure !== error),
|
|
1430
|
+
],
|
|
1431
|
+
"portable plugin plan-file binding cleanup failed",
|
|
1432
|
+
);
|
|
1433
|
+
cleanupBarrier.unsafe(cause);
|
|
1434
|
+
throw new Error(
|
|
1435
|
+
"portable provider plugin plan-file cleanup failed",
|
|
1436
|
+
{ cause },
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1439
|
+
throw error;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
function portableInput(
|
|
1444
|
+
input: OperationInput,
|
|
1445
|
+
files: readonly BoundInvocationFile[],
|
|
1446
|
+
): PortablePluginJsonObject {
|
|
1447
|
+
const handles = new Map<string, string[]>();
|
|
1448
|
+
for (const file of files) {
|
|
1449
|
+
const values = handles.get(file.protocol.input) ?? [];
|
|
1450
|
+
values.push(file.protocol.handle);
|
|
1451
|
+
handles.set(file.protocol.input, values);
|
|
1452
|
+
}
|
|
1453
|
+
const output: Record<string, unknown> = {};
|
|
1454
|
+
for (const [name, value] of Object.entries(input)) {
|
|
1455
|
+
const fileHandles = handles.get(name);
|
|
1456
|
+
if (fileHandles === undefined) {
|
|
1457
|
+
output[name] = value;
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
output[name] = Array.isArray(value) ? fileHandles : fileHandles[0];
|
|
1461
|
+
}
|
|
1462
|
+
return normalizePortablePluginJsonObject(output, "portable plugin input");
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
async function resolveWebFiles(
|
|
1466
|
+
input: OperationInput,
|
|
1467
|
+
resolver: BrowserFileResolver | undefined,
|
|
1468
|
+
cleanupBarrier: PortableProviderPluginCleanupBarrier,
|
|
1469
|
+
closeBoundFile: (descriptor: number) => void,
|
|
1470
|
+
closeBindingDescriptor: (descriptor: number) => void,
|
|
1471
|
+
removeBindingDirectory: (path: string, force: boolean) => void,
|
|
1472
|
+
operationDeadline?: WebSessionOperationDeadline,
|
|
1473
|
+
): Promise<readonly BoundInvocationFile[]> {
|
|
1474
|
+
const values = fileValues(input);
|
|
1475
|
+
if (values.length === 0) return Object.freeze([]);
|
|
1476
|
+
if (resolver === undefined) {
|
|
1477
|
+
throw new Error("portable plugin plan-file resolver is unavailable");
|
|
1478
|
+
}
|
|
1479
|
+
const paths = await resolver(values.map((entry) => entry.value));
|
|
1480
|
+
operationDeadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
|
|
1481
|
+
if (paths.length !== values.length) {
|
|
1482
|
+
throw new Error("portable plugin plan-file resolver returned the wrong count");
|
|
1483
|
+
}
|
|
1484
|
+
const files: BoundInvocationFile[] = [];
|
|
1485
|
+
try {
|
|
1486
|
+
for (const [index, entry] of values.entries()) {
|
|
1487
|
+
operationDeadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
|
|
1488
|
+
const path = paths[index];
|
|
1489
|
+
if (path === undefined) {
|
|
1490
|
+
throw new Error("portable plugin plan-file resolver omitted a path");
|
|
1491
|
+
}
|
|
1492
|
+
files.push(bindFile(
|
|
1493
|
+
entry.input,
|
|
1494
|
+
entry.value,
|
|
1495
|
+
path,
|
|
1496
|
+
cleanupBarrier,
|
|
1497
|
+
closeBoundFile,
|
|
1498
|
+
closeBindingDescriptor,
|
|
1499
|
+
removeBindingDirectory,
|
|
1500
|
+
));
|
|
1501
|
+
}
|
|
1502
|
+
return Object.freeze(files);
|
|
1503
|
+
} catch (error) {
|
|
1504
|
+
disposePortableRuntimeResources(cleanupBarrier, undefined, files);
|
|
1505
|
+
throw error;
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
async function runWebPortableHost(options: {
|
|
1510
|
+
readonly package: VerifiedPortableProviderPluginPackage;
|
|
1511
|
+
readonly binding: PortableProviderPluginBindingV1;
|
|
1512
|
+
readonly operation: PortableProviderPluginOperationV1;
|
|
1513
|
+
readonly input: OperationInput;
|
|
1514
|
+
readonly auth: GhostgetAuth;
|
|
1515
|
+
readonly environment: Environment;
|
|
1516
|
+
readonly fileResolver?: BrowserFileResolver;
|
|
1517
|
+
readonly signal?: AbortSignal;
|
|
1518
|
+
readonly operationDeadline?: WebSessionOperationDeadline;
|
|
1519
|
+
readonly beforeDispatch?: (event: WebSessionDispatchEvent) => Promise<void>;
|
|
1520
|
+
readonly afterDispatchVerified?: (event: WebSessionDispatchEvent) => Promise<void>;
|
|
1521
|
+
readonly dependencies: PortableProviderRuntimeDependencies;
|
|
1522
|
+
}): Promise<PortableProviderPluginHostResult> {
|
|
1523
|
+
const cleanupBarrier = registerPortableProviderPluginCleanupBarrier();
|
|
1524
|
+
const deadline = options.operationDeadline;
|
|
1525
|
+
const plannedIds = options.operation.dispatch === "single"
|
|
1526
|
+
? [options.operation.name]
|
|
1527
|
+
: [];
|
|
1528
|
+
let started = 0;
|
|
1529
|
+
let verified = 0;
|
|
1530
|
+
let files: readonly BoundInvocationFile[] = Object.freeze([]);
|
|
1531
|
+
let fetchScope: PinnedHttpsFetchScope | undefined;
|
|
1532
|
+
let hostCompletion: Promise<void> | undefined;
|
|
1533
|
+
const capabilityActivity = createPortableCapabilityActivity();
|
|
1534
|
+
try {
|
|
1535
|
+
const resolveFiles = () =>
|
|
1536
|
+
resolveWebFiles(
|
|
1537
|
+
options.input,
|
|
1538
|
+
options.fileResolver,
|
|
1539
|
+
cleanupBarrier,
|
|
1540
|
+
options.dependencies.closeBoundFile,
|
|
1541
|
+
options.dependencies.closePlanFileBindingDescriptor,
|
|
1542
|
+
options.dependencies.removePlanFileBindingDirectory,
|
|
1543
|
+
options.operationDeadline,
|
|
1544
|
+
);
|
|
1545
|
+
deadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
|
|
1546
|
+
const fileResolution = resolveFiles();
|
|
1547
|
+
if (deadline === undefined) {
|
|
1548
|
+
files = await fileResolution;
|
|
1549
|
+
} else {
|
|
1550
|
+
try {
|
|
1551
|
+
files = await deadline.run(
|
|
1552
|
+
() => fileResolution,
|
|
1553
|
+
WEB_SESSION_OPERATION_LABEL,
|
|
1554
|
+
);
|
|
1555
|
+
} catch (error) {
|
|
1556
|
+
if (deadline.signal.aborted) {
|
|
1557
|
+
const settled = await settlesWithin(
|
|
1558
|
+
fileResolution,
|
|
1559
|
+
PORTABLE_CAPABILITY_ABORT_JOIN_MS,
|
|
1560
|
+
);
|
|
1561
|
+
if (settled?.status === "fulfilled") {
|
|
1562
|
+
// OperationDeadline performs a post-work check. Retain ownership
|
|
1563
|
+
// when binding completed just after the deadline, so the outer
|
|
1564
|
+
// resource boundary still closes every descriptor.
|
|
1565
|
+
files = settled.value;
|
|
1566
|
+
} else if (settled === null) {
|
|
1567
|
+
cleanupBarrier.unsafe(new Error(
|
|
1568
|
+
"portable plugin plan-file resolution outlived its operation deadline",
|
|
1569
|
+
));
|
|
1570
|
+
// The unsafe durable lease remains the fail-closed proof, but keep
|
|
1571
|
+
// a handler attached so a very late successful bind is still
|
|
1572
|
+
// disposed rather than orphaned in this process.
|
|
1573
|
+
void fileResolution.then(
|
|
1574
|
+
(lateFiles) => {
|
|
1575
|
+
try {
|
|
1576
|
+
disposePortableRuntimeResources(
|
|
1577
|
+
cleanupBarrier,
|
|
1578
|
+
undefined,
|
|
1579
|
+
lateFiles,
|
|
1580
|
+
);
|
|
1581
|
+
} catch {
|
|
1582
|
+
// The already-unsafe barrier and retained lease own recovery.
|
|
1583
|
+
}
|
|
1584
|
+
},
|
|
1585
|
+
() => undefined,
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
throw error;
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
deadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
|
|
1593
|
+
const byHandle = new Map(files.map((file) => [
|
|
1594
|
+
file.protocol.handle,
|
|
1595
|
+
file,
|
|
1596
|
+
]));
|
|
1597
|
+
const scope = options.dependencies.createFetchScope(
|
|
1598
|
+
options.binding.origin,
|
|
1599
|
+
);
|
|
1600
|
+
fetchScope = scope;
|
|
1601
|
+
const timeoutMs = Math.min(
|
|
1602
|
+
options.operation.timeoutMs,
|
|
1603
|
+
deadline?.remainingTimeMs() ?? options.operation.timeoutMs,
|
|
1604
|
+
);
|
|
1605
|
+
if (timeoutMs < 100) {
|
|
1606
|
+
throw new Error("portable plugin operation deadline was exhausted");
|
|
1607
|
+
}
|
|
1608
|
+
const invokeHost = () => {
|
|
1609
|
+
const host = trackPortableProviderPluginHostCompletion(
|
|
1610
|
+
options.dependencies.runHost({
|
|
1611
|
+
package: options.package,
|
|
1612
|
+
route: {
|
|
1613
|
+
transport: options.binding.transport,
|
|
1614
|
+
surfaceId: options.binding.surfaceId,
|
|
1615
|
+
operation: options.operation.name,
|
|
1616
|
+
contractVersion: options.operation.contractVersion,
|
|
1617
|
+
},
|
|
1618
|
+
input: portableInput(options.input, files),
|
|
1619
|
+
auth: {
|
|
1620
|
+
kind: options.auth.kind,
|
|
1621
|
+
handle: `auth-${randomUUID()}`,
|
|
1622
|
+
...(options.auth.subject === undefined
|
|
1623
|
+
? {}
|
|
1624
|
+
: { subject: options.auth.subject }),
|
|
1625
|
+
},
|
|
1626
|
+
files: files.map((file) => file.protocol),
|
|
1627
|
+
timeoutMs,
|
|
1628
|
+
hostVersion: PORTABLE_HOST_VERSION,
|
|
1629
|
+
plannedDispatchIds: plannedIds,
|
|
1630
|
+
capabilityHost: capabilityHost({
|
|
1631
|
+
package: options.package,
|
|
1632
|
+
binding: options.binding,
|
|
1633
|
+
auth: options.auth,
|
|
1634
|
+
environment: options.environment,
|
|
1635
|
+
files: byHandle,
|
|
1636
|
+
dependencies: options.dependencies,
|
|
1637
|
+
fetch: scope.fetch,
|
|
1638
|
+
activity: capabilityActivity,
|
|
1639
|
+
cleanupBarrier,
|
|
1640
|
+
...(deadline === undefined
|
|
1641
|
+
? {}
|
|
1642
|
+
: { operationDeadline: deadline }),
|
|
1643
|
+
beginDispatch: async (dispatchId) => {
|
|
1644
|
+
deadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
|
|
1645
|
+
const expected = plannedIds[started];
|
|
1646
|
+
if (expected !== dispatchId) {
|
|
1647
|
+
throw new Error("portable plugin changed its confirmed dispatch");
|
|
1648
|
+
}
|
|
1649
|
+
const index = started + 1;
|
|
1650
|
+
await options.beforeDispatch?.({
|
|
1651
|
+
id: dispatchId,
|
|
1652
|
+
index,
|
|
1653
|
+
progress: {
|
|
1654
|
+
planned: plannedIds.length,
|
|
1655
|
+
started,
|
|
1656
|
+
verified,
|
|
1657
|
+
},
|
|
1658
|
+
});
|
|
1659
|
+
started += 1;
|
|
1660
|
+
deadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
|
|
1661
|
+
let completed = false;
|
|
1662
|
+
return Object.freeze({
|
|
1663
|
+
verify: async () => {
|
|
1664
|
+
deadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
|
|
1665
|
+
if (completed || verified + 1 !== index) {
|
|
1666
|
+
throw new Error("portable plugin verified dispatch out of order");
|
|
1667
|
+
}
|
|
1668
|
+
await options.afterDispatchVerified?.({
|
|
1669
|
+
id: dispatchId,
|
|
1670
|
+
index,
|
|
1671
|
+
progress: {
|
|
1672
|
+
planned: plannedIds.length,
|
|
1673
|
+
started,
|
|
1674
|
+
verified: verified + 1,
|
|
1675
|
+
},
|
|
1676
|
+
});
|
|
1677
|
+
verified += 1;
|
|
1678
|
+
completed = true;
|
|
1679
|
+
},
|
|
1680
|
+
});
|
|
1681
|
+
},
|
|
1682
|
+
}),
|
|
1683
|
+
...(deadline === undefined
|
|
1684
|
+
? (options.signal === undefined ? {} : { signal: options.signal })
|
|
1685
|
+
: { signal: deadline.signal }),
|
|
1686
|
+
}),
|
|
1687
|
+
);
|
|
1688
|
+
hostCompletion = host.then(
|
|
1689
|
+
() => undefined,
|
|
1690
|
+
() => undefined,
|
|
1691
|
+
);
|
|
1692
|
+
return host;
|
|
1693
|
+
};
|
|
1694
|
+
return deadline === undefined
|
|
1695
|
+
? await invokeHost()
|
|
1696
|
+
: await deadline.run(invokeHost, WEB_SESSION_OPERATION_LABEL);
|
|
1697
|
+
} finally {
|
|
1698
|
+
// An operation deadline may win its race while the host is still using
|
|
1699
|
+
// file descriptors or the pinned fetch scope. Keep those resources alive
|
|
1700
|
+
// until the actual host boundary settles, then prove their disposal to the
|
|
1701
|
+
// lease scope.
|
|
1702
|
+
await hostCompletion;
|
|
1703
|
+
await capabilityActivity.whenIdle();
|
|
1704
|
+
disposePortableRuntimeResources(cleanupBarrier, fetchScope, files);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
function webExecutor(
|
|
1709
|
+
packageValue: VerifiedPortableProviderPluginPackage,
|
|
1710
|
+
binding: PortableProviderPluginBindingV1,
|
|
1711
|
+
operations: ReadonlyMap<string, PortableProviderPluginOperationV1>,
|
|
1712
|
+
environment: Environment,
|
|
1713
|
+
dependencies: PortableProviderRuntimeDependencies,
|
|
1714
|
+
): WebSessionOperationExecutor {
|
|
1715
|
+
return async (_manifest, recipe, input, auth, options): Promise<WebSessionExecution> => {
|
|
1716
|
+
const operation = operations.get(
|
|
1717
|
+
`${recipe.action}@${recipe.contractVersion}`,
|
|
1718
|
+
);
|
|
1719
|
+
if (operation === undefined) {
|
|
1720
|
+
throw new Error("portable plugin operation disappeared");
|
|
1721
|
+
}
|
|
1722
|
+
try {
|
|
1723
|
+
const result = await runWebPortableHost({
|
|
1724
|
+
package: packageValue,
|
|
1725
|
+
binding,
|
|
1726
|
+
operation,
|
|
1727
|
+
input,
|
|
1728
|
+
auth,
|
|
1729
|
+
environment: options.environment ?? environment,
|
|
1730
|
+
...(options.fileResolver === undefined
|
|
1731
|
+
? {}
|
|
1732
|
+
: { fileResolver: options.fileResolver }),
|
|
1733
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
1734
|
+
...(options.operationDeadline === undefined
|
|
1735
|
+
? {}
|
|
1736
|
+
: { operationDeadline: options.operationDeadline }),
|
|
1737
|
+
...(options.beforeDispatch === undefined
|
|
1738
|
+
? {}
|
|
1739
|
+
: { beforeDispatch: options.beforeDispatch }),
|
|
1740
|
+
...(options.afterDispatchVerified === undefined
|
|
1741
|
+
? {}
|
|
1742
|
+
: { afterDispatchVerified: options.afterDispatchVerified }),
|
|
1743
|
+
dependencies,
|
|
1744
|
+
});
|
|
1745
|
+
return {
|
|
1746
|
+
status: "succeeded",
|
|
1747
|
+
output: result.output,
|
|
1748
|
+
finalUrl: result.finalUrl,
|
|
1749
|
+
dispatchStarted: result.dispatch.started > 0,
|
|
1750
|
+
dispatch: result.dispatch,
|
|
1751
|
+
};
|
|
1752
|
+
} catch (error) {
|
|
1753
|
+
const dispatch = typeof error === "object"
|
|
1754
|
+
&& error !== null
|
|
1755
|
+
&& "dispatch" in error
|
|
1756
|
+
? (error as { readonly dispatch: WebSessionExecution["dispatch"] }).dispatch
|
|
1757
|
+
: {
|
|
1758
|
+
planned: operation.dispatch === "single" ? 1 : 0,
|
|
1759
|
+
started: 0,
|
|
1760
|
+
verified: 0,
|
|
1761
|
+
};
|
|
1762
|
+
return {
|
|
1763
|
+
status: dispatch.started > dispatch.verified
|
|
1764
|
+
? "indeterminate"
|
|
1765
|
+
: dispatch.verified > 0
|
|
1766
|
+
? "partial"
|
|
1767
|
+
: "failed",
|
|
1768
|
+
output: null,
|
|
1769
|
+
finalUrl: null,
|
|
1770
|
+
dispatchStarted: dispatch.started > 0,
|
|
1771
|
+
dispatch,
|
|
1772
|
+
error: "portable provider plugin execution failed",
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
async function runProviderPortableHost(
|
|
1779
|
+
packageValue: VerifiedPortableProviderPluginPackage,
|
|
1780
|
+
binding: PortableProviderPluginBindingV1,
|
|
1781
|
+
operation: PortableProviderPluginOperationV1,
|
|
1782
|
+
context: ProviderActionContext,
|
|
1783
|
+
dependencies: PortableProviderRuntimeDependencies,
|
|
1784
|
+
): Promise<void> {
|
|
1785
|
+
const cleanupBarrier = registerPortableProviderPluginCleanupBarrier();
|
|
1786
|
+
const values = fileValues(context.input);
|
|
1787
|
+
const files: BoundInvocationFile[] = [];
|
|
1788
|
+
try {
|
|
1789
|
+
for (const inputName of new Set(values.map((entry) => entry.input))) {
|
|
1790
|
+
const matching = values.filter((entry) => entry.input === inputName);
|
|
1791
|
+
const resolved = await context.resolveFiles(inputName);
|
|
1792
|
+
if (context.signal.aborted) {
|
|
1793
|
+
throw new Error("portable provider invocation was cancelled");
|
|
1794
|
+
}
|
|
1795
|
+
if (resolved.length !== matching.length) {
|
|
1796
|
+
throw new Error("portable provider file resolver returned the wrong count");
|
|
1797
|
+
}
|
|
1798
|
+
for (const [index, entry] of matching.entries()) {
|
|
1799
|
+
if (context.signal.aborted) {
|
|
1800
|
+
throw new Error("portable provider invocation was cancelled");
|
|
1801
|
+
}
|
|
1802
|
+
const file = resolved[index];
|
|
1803
|
+
if (file === undefined) {
|
|
1804
|
+
throw new Error("portable provider file resolver omitted a file");
|
|
1805
|
+
}
|
|
1806
|
+
files.push(bindFile(
|
|
1807
|
+
inputName,
|
|
1808
|
+
entry.value,
|
|
1809
|
+
file.path,
|
|
1810
|
+
cleanupBarrier,
|
|
1811
|
+
dependencies.closeBoundFile,
|
|
1812
|
+
dependencies.closePlanFileBindingDescriptor,
|
|
1813
|
+
dependencies.removePlanFileBindingDirectory,
|
|
1814
|
+
));
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
} catch (error) {
|
|
1818
|
+
disposePortableRuntimeResources(cleanupBarrier, undefined, files);
|
|
1819
|
+
throw error;
|
|
1820
|
+
}
|
|
1821
|
+
const byHandle = new Map(files.map((file) => [
|
|
1822
|
+
file.protocol.handle,
|
|
1823
|
+
file,
|
|
1824
|
+
]));
|
|
1825
|
+
const plannedIds = operation.dispatch === "single" ? [operation.name] : [];
|
|
1826
|
+
let fetchScope: PinnedHttpsFetchScope | undefined;
|
|
1827
|
+
const capabilityActivity = createPortableCapabilityActivity();
|
|
1828
|
+
try {
|
|
1829
|
+
fetchScope = dependencies.createFetchScope(binding.origin);
|
|
1830
|
+
const timeoutMs = Math.min(
|
|
1831
|
+
operation.timeoutMs,
|
|
1832
|
+
context.remainingTimeMs(),
|
|
1833
|
+
);
|
|
1834
|
+
if (timeoutMs < 100) {
|
|
1835
|
+
throw new Error("portable provider operation deadline was exhausted");
|
|
1836
|
+
}
|
|
1837
|
+
const result = await trackPortableProviderPluginHostCompletion(
|
|
1838
|
+
dependencies.runHost({
|
|
1839
|
+
package: packageValue,
|
|
1840
|
+
route: {
|
|
1841
|
+
transport: binding.transport,
|
|
1842
|
+
surfaceId: binding.surfaceId,
|
|
1843
|
+
operation: operation.name,
|
|
1844
|
+
contractVersion: operation.contractVersion,
|
|
1845
|
+
},
|
|
1846
|
+
input: portableInput(context.input, files),
|
|
1847
|
+
auth: {
|
|
1848
|
+
kind: context.auth.kind,
|
|
1849
|
+
handle: `auth-${randomUUID()}`,
|
|
1850
|
+
...(context.auth.subject === undefined
|
|
1851
|
+
? {}
|
|
1852
|
+
: { subject: context.auth.subject }),
|
|
1853
|
+
},
|
|
1854
|
+
files: files.map((file) => file.protocol),
|
|
1855
|
+
timeoutMs,
|
|
1856
|
+
hostVersion: PORTABLE_HOST_VERSION,
|
|
1857
|
+
plannedDispatchIds: plannedIds,
|
|
1858
|
+
capabilityHost: capabilityHost({
|
|
1859
|
+
package: packageValue,
|
|
1860
|
+
binding,
|
|
1861
|
+
auth: context.auth,
|
|
1862
|
+
environment: context.environment,
|
|
1863
|
+
files: byHandle,
|
|
1864
|
+
dependencies,
|
|
1865
|
+
fetch: fetchScope.fetch,
|
|
1866
|
+
activity: capabilityActivity,
|
|
1867
|
+
cleanupBarrier,
|
|
1868
|
+
beginDispatch: async (dispatchId) => {
|
|
1869
|
+
if (dispatchId !== operation.name) {
|
|
1870
|
+
throw new Error("portable plugin changed its confirmed dispatch");
|
|
1871
|
+
}
|
|
1872
|
+
return context.beginDispatch();
|
|
1873
|
+
},
|
|
1874
|
+
}),
|
|
1875
|
+
signal: context.signal,
|
|
1876
|
+
}),
|
|
1877
|
+
);
|
|
1878
|
+
context.setOutput(result.output);
|
|
1879
|
+
if (result.finalUrl !== null) context.setFinalUrl(result.finalUrl);
|
|
1880
|
+
} finally {
|
|
1881
|
+
await capabilityActivity.whenIdle();
|
|
1882
|
+
disposePortableRuntimeResources(cleanupBarrier, fetchScope, files);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1886
|
+
/**
|
|
1887
|
+
* Construct the only in-process hooks portable packages may receive. The
|
|
1888
|
+
* caller still cannot admit these raw values into a registry: the authority
|
|
1889
|
+
* module brands only the exact frozen objects returned here.
|
|
1890
|
+
*/
|
|
1891
|
+
export function createKernelPortableProviderPluginBindingProjections(
|
|
1892
|
+
packageValue: VerifiedPortableProviderPluginPackage,
|
|
1893
|
+
environment: Environment,
|
|
1894
|
+
dependencies: PortableProviderRuntimeDependencies,
|
|
1895
|
+
): readonly KernelPortableProviderPluginBindingProjection[] {
|
|
1896
|
+
if (!isResolvedPortableProviderRuntimeDependencies(dependencies)) {
|
|
1897
|
+
throw new Error(
|
|
1898
|
+
"portable provider plugin wrappers require resolved kernel runtime dependencies",
|
|
1899
|
+
);
|
|
1900
|
+
}
|
|
1901
|
+
const bindings = packageValue.manifest.bindings.map((binding) => {
|
|
1902
|
+
if (
|
|
1903
|
+
binding.transport === "linked-device"
|
|
1904
|
+
&& binding.operations.some((operation) => operation.state === "observed")
|
|
1905
|
+
) {
|
|
1906
|
+
throw new Error(
|
|
1907
|
+
`portable plugin ${packageValue.manifest.id} linked-device execution requires a future lifecycle protocol`,
|
|
1908
|
+
);
|
|
1909
|
+
}
|
|
1910
|
+
const operationByKey = new Map(binding.operations.map((operation) => [
|
|
1911
|
+
`${operation.name}@${operation.contractVersion}`,
|
|
1912
|
+
operation,
|
|
1913
|
+
]));
|
|
1914
|
+
const subject = portableSubject(binding.subject);
|
|
1915
|
+
const manifest = virtualManifest(packageValue, binding);
|
|
1916
|
+
let projectedBinding: ProviderPluginBindingDefinitionV1;
|
|
1917
|
+
if (binding.transport === "provider-api") {
|
|
1918
|
+
const definitions = Object.freeze(
|
|
1919
|
+
binding.operations.map((operation) =>
|
|
1920
|
+
Object.freeze(portableProviderOperation(operation))),
|
|
1921
|
+
);
|
|
1922
|
+
projectedBinding = Object.freeze({
|
|
1923
|
+
transport: binding.transport,
|
|
1924
|
+
surfaceId: binding.surfaceId,
|
|
1925
|
+
origin: binding.origin,
|
|
1926
|
+
authKinds: binding.authKinds,
|
|
1927
|
+
subject,
|
|
1928
|
+
operations: definitions,
|
|
1929
|
+
runtime: Object.freeze({
|
|
1930
|
+
loadRuntime: () => Promise.resolve(Object.freeze({
|
|
1931
|
+
execute: async (context: ProviderActionContext) => {
|
|
1932
|
+
const operation = operationByKey.get(
|
|
1933
|
+
`${context.recipe.action}@${context.recipe.contractVersion}`,
|
|
1934
|
+
);
|
|
1935
|
+
if (operation === undefined) {
|
|
1936
|
+
throw new Error("portable provider operation disappeared");
|
|
1937
|
+
}
|
|
1938
|
+
await runProviderPortableHost(
|
|
1939
|
+
packageValue,
|
|
1940
|
+
binding,
|
|
1941
|
+
operation,
|
|
1942
|
+
context,
|
|
1943
|
+
dependencies,
|
|
1944
|
+
);
|
|
1945
|
+
},
|
|
1946
|
+
})),
|
|
1947
|
+
}),
|
|
1948
|
+
});
|
|
1949
|
+
} else {
|
|
1950
|
+
const definitions = Object.freeze(
|
|
1951
|
+
binding.operations.map((operation) =>
|
|
1952
|
+
Object.freeze(portableSessionOperation(operation))),
|
|
1953
|
+
);
|
|
1954
|
+
const execute = webExecutor(
|
|
1955
|
+
packageValue,
|
|
1956
|
+
binding,
|
|
1957
|
+
operationByKey,
|
|
1958
|
+
environment,
|
|
1959
|
+
dependencies,
|
|
1960
|
+
);
|
|
1961
|
+
projectedBinding = Object.freeze({
|
|
1962
|
+
transport: binding.transport,
|
|
1963
|
+
surfaceId: binding.surfaceId,
|
|
1964
|
+
origin: binding.origin,
|
|
1965
|
+
authKinds: binding.authKinds,
|
|
1966
|
+
subject,
|
|
1967
|
+
operations: definitions,
|
|
1968
|
+
runtime: Object.freeze({
|
|
1969
|
+
loadRuntime: () => Promise.resolve(Object.freeze({
|
|
1970
|
+
probe: async (
|
|
1971
|
+
auth: GhostgetAuth,
|
|
1972
|
+
options?: ProviderPluginSubjectProbeOptionsV1,
|
|
1973
|
+
) => {
|
|
1974
|
+
const probe = binding.subject.probe;
|
|
1975
|
+
if (probe === null) {
|
|
1976
|
+
throw new Error("portable plugin has no current-account probe");
|
|
1977
|
+
}
|
|
1978
|
+
const operation = operationByKey.get(
|
|
1979
|
+
`${probe.operation}@${probe.contractVersion}`,
|
|
1980
|
+
);
|
|
1981
|
+
if (operation === undefined) {
|
|
1982
|
+
throw new Error("portable plugin subject probe disappeared");
|
|
1983
|
+
}
|
|
1984
|
+
const identity = createPortableOperationIdentityV1({
|
|
1985
|
+
package: {
|
|
1986
|
+
id: packageValue.manifest.id,
|
|
1987
|
+
version: packageValue.manifest.version,
|
|
1988
|
+
hostApiVersion: packageValue.manifest.hostApiVersion,
|
|
1989
|
+
bundleSha256: packageValue.bundleSha256,
|
|
1990
|
+
manifestSha256: packageValue.manifestSha256,
|
|
1991
|
+
capabilities: packageValue.manifest.capabilities,
|
|
1992
|
+
},
|
|
1993
|
+
binding,
|
|
1994
|
+
operation,
|
|
1995
|
+
});
|
|
1996
|
+
const lease = acquirePortableProviderPluginInvocationLease(
|
|
1997
|
+
identity,
|
|
1998
|
+
randomUUID(),
|
|
1999
|
+
environment,
|
|
2000
|
+
);
|
|
2001
|
+
const containment =
|
|
2002
|
+
createPortableProviderPluginInvocationLeaseContainmentController(
|
|
2003
|
+
lease,
|
|
2004
|
+
environment,
|
|
2005
|
+
);
|
|
2006
|
+
const outcome = await settlePortableProviderPluginCleanup(
|
|
2007
|
+
async () => {
|
|
2008
|
+
const result = await runWebPortableHost({
|
|
2009
|
+
package: packageValue,
|
|
2010
|
+
binding,
|
|
2011
|
+
operation,
|
|
2012
|
+
input: {},
|
|
2013
|
+
auth,
|
|
2014
|
+
environment,
|
|
2015
|
+
dependencies,
|
|
2016
|
+
...(options?.signal === undefined
|
|
2017
|
+
? {}
|
|
2018
|
+
: { signal: options.signal }),
|
|
2019
|
+
});
|
|
2020
|
+
if (
|
|
2021
|
+
typeof result.output !== "string"
|
|
2022
|
+
|| !subject.matches(result.output)
|
|
2023
|
+
) {
|
|
2024
|
+
throw new Error(
|
|
2025
|
+
"portable plugin returned an invalid account subject",
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
2028
|
+
return result.output;
|
|
2029
|
+
},
|
|
2030
|
+
{
|
|
2031
|
+
containment,
|
|
2032
|
+
cleanupComplete: containment.cleanupComplete,
|
|
2033
|
+
},
|
|
2034
|
+
);
|
|
2035
|
+
releasePortableProviderPluginInvocationLease(
|
|
2036
|
+
containment.current,
|
|
2037
|
+
environment,
|
|
2038
|
+
);
|
|
2039
|
+
if (outcome.status === "rejected") {
|
|
2040
|
+
throw outcome.reason;
|
|
2041
|
+
}
|
|
2042
|
+
return outcome.value;
|
|
2043
|
+
},
|
|
2044
|
+
execute,
|
|
2045
|
+
})),
|
|
2046
|
+
}),
|
|
2047
|
+
});
|
|
2048
|
+
}
|
|
2049
|
+
return Object.freeze({
|
|
2050
|
+
adapterId: binding.adapterId,
|
|
2051
|
+
manifest,
|
|
2052
|
+
portableBinding: binding,
|
|
2053
|
+
binding: projectedBinding,
|
|
2054
|
+
});
|
|
2055
|
+
});
|
|
2056
|
+
return Object.freeze(bindings);
|
|
2057
|
+
}
|