@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,2155 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
closeSync,
|
|
5
|
+
constants,
|
|
6
|
+
existsSync,
|
|
7
|
+
fchmodSync,
|
|
8
|
+
fstatSync,
|
|
9
|
+
fsyncSync,
|
|
10
|
+
linkSync,
|
|
11
|
+
lstatSync,
|
|
12
|
+
mkdirSync,
|
|
13
|
+
openSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
readdirSync,
|
|
16
|
+
realpathSync,
|
|
17
|
+
renameSync,
|
|
18
|
+
rmSync,
|
|
19
|
+
unlinkSync,
|
|
20
|
+
writeFileSync,
|
|
21
|
+
} from "node:fs";
|
|
22
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
23
|
+
|
|
24
|
+
import { canonicalJson } from "./canonical-json";
|
|
25
|
+
import {
|
|
26
|
+
PORTABLE_PROVIDER_PLUGIN_HOST_API_VERSION,
|
|
27
|
+
PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME,
|
|
28
|
+
isPortableProviderPluginVersion,
|
|
29
|
+
verifyPortableProviderPluginPackageDirectory,
|
|
30
|
+
type PortableProviderPluginManifestV1,
|
|
31
|
+
type VerifiedPortableProviderPluginPackage,
|
|
32
|
+
} from "./provider-plugin-package";
|
|
33
|
+
import {
|
|
34
|
+
currentProcessStartIdentity,
|
|
35
|
+
processOwnerStatus,
|
|
36
|
+
type ProcessOwnerIdentity,
|
|
37
|
+
} from "./process-identity";
|
|
38
|
+
import { ensurePrivateDirectory } from "./storage";
|
|
39
|
+
|
|
40
|
+
export const PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION = 1 as const;
|
|
41
|
+
|
|
42
|
+
export type PortableProviderPluginTrustApprovalV1 = {
|
|
43
|
+
readonly decision: "trust-executable-code";
|
|
44
|
+
readonly pluginId: string;
|
|
45
|
+
readonly pluginVersion: string;
|
|
46
|
+
readonly bundleSha256: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type PortableProviderPluginTrustRecordV1 = {
|
|
50
|
+
readonly schemaVersion:
|
|
51
|
+
typeof PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION;
|
|
52
|
+
readonly decision: "trust-executable-code";
|
|
53
|
+
readonly pluginId: string;
|
|
54
|
+
readonly pluginVersion: string;
|
|
55
|
+
readonly hostApiVersion: typeof PORTABLE_PROVIDER_PLUGIN_HOST_API_VERSION;
|
|
56
|
+
readonly bundleSha256: string;
|
|
57
|
+
readonly manifestSha256: string;
|
|
58
|
+
readonly provenance: PortableProviderPluginManifestV1["provenance"];
|
|
59
|
+
readonly trustedAt: string;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
type PortableProviderPluginActivationBaseV1 = {
|
|
63
|
+
readonly schemaVersion:
|
|
64
|
+
typeof PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION;
|
|
65
|
+
readonly pluginId: string;
|
|
66
|
+
readonly pluginVersion: string;
|
|
67
|
+
readonly bundleSha256: string;
|
|
68
|
+
readonly activatedAt: string;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type PortableProviderPluginActiveRecordV1 =
|
|
72
|
+
| PortableProviderPluginActivationBaseV1 & {
|
|
73
|
+
readonly status: "enabled";
|
|
74
|
+
}
|
|
75
|
+
| PortableProviderPluginActivationBaseV1 & {
|
|
76
|
+
readonly status: "disabled";
|
|
77
|
+
readonly disabledAt: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type PortableProviderPluginLockRecordV1 = ProcessOwnerIdentity & {
|
|
81
|
+
readonly schemaVersion:
|
|
82
|
+
typeof PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION;
|
|
83
|
+
readonly pluginId: string;
|
|
84
|
+
readonly acquiredAt: string;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
type PortableProviderPluginCatalogLockRecordV1 = ProcessOwnerIdentity & {
|
|
88
|
+
readonly schemaVersion:
|
|
89
|
+
typeof PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION;
|
|
90
|
+
readonly scope: "catalog-mutation";
|
|
91
|
+
readonly acquiredAt: string;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
type PortableProviderPluginLockClaimPhase =
|
|
95
|
+
| "waiting"
|
|
96
|
+
| "candidate"
|
|
97
|
+
| "held";
|
|
98
|
+
|
|
99
|
+
type PortableProviderPluginLockClaimV1 = ProcessOwnerIdentity & {
|
|
100
|
+
readonly schemaVersion:
|
|
101
|
+
typeof PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION;
|
|
102
|
+
readonly kind: "portable-provider-plugin-lock-claim";
|
|
103
|
+
readonly targetSha256: string;
|
|
104
|
+
readonly claimId: string;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export type PortableProviderPluginAssertActivatable = (
|
|
108
|
+
verifiedPackage: VerifiedPortableProviderPluginPackage,
|
|
109
|
+
) => void;
|
|
110
|
+
|
|
111
|
+
export type PortableProviderPluginAssertQuiescent = (
|
|
112
|
+
bundleSha256: string,
|
|
113
|
+
artifactPath: string,
|
|
114
|
+
) => void;
|
|
115
|
+
|
|
116
|
+
export type InstalledPortableProviderPlugin = {
|
|
117
|
+
readonly artifactPath: string;
|
|
118
|
+
readonly package: VerifiedPortableProviderPluginPackage;
|
|
119
|
+
readonly trust: PortableProviderPluginTrustRecordV1;
|
|
120
|
+
readonly active: PortableProviderPluginActiveRecordV1;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export type PortableProviderPluginStorePaths = {
|
|
124
|
+
readonly root: string;
|
|
125
|
+
readonly artifacts: string;
|
|
126
|
+
readonly trust: string;
|
|
127
|
+
readonly active: string;
|
|
128
|
+
readonly locks: string;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const MAX_STORE_RECORD_BYTES = 64 * 1024;
|
|
132
|
+
const MAX_LOCK_CLAIMS = 1_024;
|
|
133
|
+
const MAX_LOCK_PUBLICATIONS = 1_024;
|
|
134
|
+
const TEST_BARRIER_TIMEOUT_MS = 90_000;
|
|
135
|
+
const CATALOG_MUTATION_LOCK_NAME = ".catalog-mutation.lock";
|
|
136
|
+
const lockClaimIdPattern =
|
|
137
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
138
|
+
const lockPublicationPrefix = ".lock-publication-";
|
|
139
|
+
const lockPublicationNamePattern =
|
|
140
|
+
/^\.lock-publication-([1-9][0-9]*)-([a-f0-9]{64})-([a-f0-9]{64})-([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\.tmp$/u;
|
|
141
|
+
const pluginIdPattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u;
|
|
142
|
+
const sha256Pattern = /^[a-f0-9]{64}$/u;
|
|
143
|
+
const activeCatalogMutationLocks = new Set<string>();
|
|
144
|
+
const activePluginMutations = new Set<string>();
|
|
145
|
+
|
|
146
|
+
function hasCode(error: unknown, code: string): boolean {
|
|
147
|
+
return (
|
|
148
|
+
typeof error === "object"
|
|
149
|
+
&& error !== null
|
|
150
|
+
&& "code" in error
|
|
151
|
+
&& error.code === code
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
156
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
const prototype = Object.getPrototypeOf(value) as unknown;
|
|
160
|
+
if (prototype !== Object.prototype && prototype !== null) return false;
|
|
161
|
+
return Reflect.ownKeys(value).every((key) => {
|
|
162
|
+
if (typeof key !== "string") return false;
|
|
163
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
164
|
+
return descriptor !== undefined
|
|
165
|
+
&& descriptor.enumerable
|
|
166
|
+
&& "value" in descriptor;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function record(value: unknown, label: string): Record<string, unknown> {
|
|
171
|
+
if (!isRecord(value)) throw new Error(`${label} must be an object`);
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function exactKeys(
|
|
176
|
+
value: Readonly<Record<string, unknown>>,
|
|
177
|
+
expected: readonly string[],
|
|
178
|
+
label: string,
|
|
179
|
+
): void {
|
|
180
|
+
const actual = Object.keys(value).sort();
|
|
181
|
+
const wanted = [...expected].sort();
|
|
182
|
+
if (
|
|
183
|
+
actual.length !== wanted.length
|
|
184
|
+
|| actual.some((key, index) => key !== wanted[index])
|
|
185
|
+
) {
|
|
186
|
+
throw new Error(`${label} must contain exactly: ${wanted.join(", ")}`);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function pluginId(value: unknown, label: string): string {
|
|
191
|
+
if (typeof value !== "string" || !pluginIdPattern.test(value)) {
|
|
192
|
+
throw new Error(`${label} must be strict lowercase kebab-case`);
|
|
193
|
+
}
|
|
194
|
+
return value;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function pluginVersion(value: unknown, label: string): string {
|
|
198
|
+
if (
|
|
199
|
+
typeof value !== "string"
|
|
200
|
+
|| value.length > 128
|
|
201
|
+
|| !isPortableProviderPluginVersion(value)
|
|
202
|
+
) {
|
|
203
|
+
throw new Error(`${label} must be strict semantic version text`);
|
|
204
|
+
}
|
|
205
|
+
return value;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function sha256(value: unknown, label: string): string {
|
|
209
|
+
if (typeof value !== "string" || !sha256Pattern.test(value)) {
|
|
210
|
+
throw new Error(`${label} must be a lowercase SHA-256 digest`);
|
|
211
|
+
}
|
|
212
|
+
return value;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function timestamp(value: unknown, label: string): string {
|
|
216
|
+
if (
|
|
217
|
+
typeof value !== "string"
|
|
218
|
+
|| !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(value)
|
|
219
|
+
|| new Date(value).toISOString() !== value
|
|
220
|
+
) {
|
|
221
|
+
throw new Error(`${label} must be one canonical UTC timestamp`);
|
|
222
|
+
}
|
|
223
|
+
return value;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function parseProvenance(
|
|
227
|
+
value: unknown,
|
|
228
|
+
): PortableProviderPluginManifestV1["provenance"] {
|
|
229
|
+
const parsed = record(value, "plugin trust provenance");
|
|
230
|
+
if (parsed.kind === "local") {
|
|
231
|
+
exactKeys(parsed, ["kind"], "local plugin trust provenance");
|
|
232
|
+
return Object.freeze({ kind: "local" });
|
|
233
|
+
}
|
|
234
|
+
if (parsed.kind === "git") {
|
|
235
|
+
exactKeys(
|
|
236
|
+
parsed,
|
|
237
|
+
["kind", "repository", "revision"],
|
|
238
|
+
"git plugin trust provenance",
|
|
239
|
+
);
|
|
240
|
+
if (
|
|
241
|
+
typeof parsed.repository !== "string"
|
|
242
|
+
|| !parsed.repository.startsWith("https://")
|
|
243
|
+
|| typeof parsed.revision !== "string"
|
|
244
|
+
|| !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(parsed.revision)
|
|
245
|
+
) {
|
|
246
|
+
throw new Error("git plugin trust provenance is invalid");
|
|
247
|
+
}
|
|
248
|
+
return Object.freeze({
|
|
249
|
+
kind: "git",
|
|
250
|
+
repository: parsed.repository as `https://${string}`,
|
|
251
|
+
revision: parsed.revision,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
throw new Error("plugin trust provenance kind is unsupported");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function parsePortableProviderPluginTrustApproval(
|
|
258
|
+
value: unknown,
|
|
259
|
+
): PortableProviderPluginTrustApprovalV1 {
|
|
260
|
+
const parsed = record(value, "portable plugin trust approval");
|
|
261
|
+
exactKeys(
|
|
262
|
+
parsed,
|
|
263
|
+
["bundleSha256", "decision", "pluginId", "pluginVersion"],
|
|
264
|
+
"portable plugin trust approval",
|
|
265
|
+
);
|
|
266
|
+
if (parsed.decision !== "trust-executable-code") {
|
|
267
|
+
throw new Error(
|
|
268
|
+
"portable plugin trust approval must explicitly trust executable code",
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
return Object.freeze({
|
|
272
|
+
decision: parsed.decision,
|
|
273
|
+
pluginId: pluginId(parsed.pluginId, "approved plugin ID"),
|
|
274
|
+
pluginVersion: pluginVersion(
|
|
275
|
+
parsed.pluginVersion,
|
|
276
|
+
"approved plugin version",
|
|
277
|
+
),
|
|
278
|
+
bundleSha256: sha256(
|
|
279
|
+
parsed.bundleSha256,
|
|
280
|
+
"approved plugin bundle digest",
|
|
281
|
+
),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function parseTrustRecord(value: unknown): PortableProviderPluginTrustRecordV1 {
|
|
286
|
+
const parsed = record(value, "portable plugin trust record");
|
|
287
|
+
exactKeys(
|
|
288
|
+
parsed,
|
|
289
|
+
[
|
|
290
|
+
"bundleSha256",
|
|
291
|
+
"decision",
|
|
292
|
+
"hostApiVersion",
|
|
293
|
+
"manifestSha256",
|
|
294
|
+
"pluginId",
|
|
295
|
+
"pluginVersion",
|
|
296
|
+
"provenance",
|
|
297
|
+
"schemaVersion",
|
|
298
|
+
"trustedAt",
|
|
299
|
+
],
|
|
300
|
+
"portable plugin trust record",
|
|
301
|
+
);
|
|
302
|
+
if (
|
|
303
|
+
parsed.schemaVersion !== PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION
|
|
304
|
+
|| parsed.hostApiVersion !== PORTABLE_PROVIDER_PLUGIN_HOST_API_VERSION
|
|
305
|
+
|| parsed.decision !== "trust-executable-code"
|
|
306
|
+
) {
|
|
307
|
+
throw new Error("portable plugin trust record version or decision is invalid");
|
|
308
|
+
}
|
|
309
|
+
return Object.freeze({
|
|
310
|
+
schemaVersion: parsed.schemaVersion,
|
|
311
|
+
decision: parsed.decision,
|
|
312
|
+
pluginId: pluginId(parsed.pluginId, "trusted plugin ID"),
|
|
313
|
+
pluginVersion: pluginVersion(
|
|
314
|
+
parsed.pluginVersion,
|
|
315
|
+
"trusted plugin version",
|
|
316
|
+
),
|
|
317
|
+
hostApiVersion: parsed.hostApiVersion,
|
|
318
|
+
bundleSha256: sha256(
|
|
319
|
+
parsed.bundleSha256,
|
|
320
|
+
"trusted plugin bundle digest",
|
|
321
|
+
),
|
|
322
|
+
manifestSha256: sha256(
|
|
323
|
+
parsed.manifestSha256,
|
|
324
|
+
"trusted plugin manifest digest",
|
|
325
|
+
),
|
|
326
|
+
provenance: parseProvenance(parsed.provenance),
|
|
327
|
+
trustedAt: timestamp(parsed.trustedAt, "plugin trust time"),
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function parseActiveRecord(value: unknown): PortableProviderPluginActiveRecordV1 {
|
|
332
|
+
const parsed = record(value, "portable plugin active record");
|
|
333
|
+
const disabled = parsed.status === "disabled";
|
|
334
|
+
exactKeys(
|
|
335
|
+
parsed,
|
|
336
|
+
[
|
|
337
|
+
"activatedAt",
|
|
338
|
+
"bundleSha256",
|
|
339
|
+
...(disabled ? ["disabledAt"] : []),
|
|
340
|
+
"pluginId",
|
|
341
|
+
"pluginVersion",
|
|
342
|
+
"schemaVersion",
|
|
343
|
+
"status",
|
|
344
|
+
],
|
|
345
|
+
"portable plugin active record",
|
|
346
|
+
);
|
|
347
|
+
if (
|
|
348
|
+
parsed.schemaVersion !== PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION
|
|
349
|
+
|| (parsed.status !== "enabled" && parsed.status !== "disabled")
|
|
350
|
+
) {
|
|
351
|
+
throw new Error("portable plugin active record version is invalid");
|
|
352
|
+
}
|
|
353
|
+
const base = {
|
|
354
|
+
schemaVersion: parsed.schemaVersion,
|
|
355
|
+
pluginId: pluginId(parsed.pluginId, "active plugin ID"),
|
|
356
|
+
pluginVersion: pluginVersion(
|
|
357
|
+
parsed.pluginVersion,
|
|
358
|
+
"active plugin version",
|
|
359
|
+
),
|
|
360
|
+
bundleSha256: sha256(
|
|
361
|
+
parsed.bundleSha256,
|
|
362
|
+
"active plugin bundle digest",
|
|
363
|
+
),
|
|
364
|
+
activatedAt: timestamp(parsed.activatedAt, "plugin activation time"),
|
|
365
|
+
} as const;
|
|
366
|
+
return parsed.status === "enabled"
|
|
367
|
+
? Object.freeze({ ...base, status: parsed.status })
|
|
368
|
+
: Object.freeze({
|
|
369
|
+
...base,
|
|
370
|
+
status: parsed.status,
|
|
371
|
+
disabledAt: timestamp(
|
|
372
|
+
parsed.disabledAt,
|
|
373
|
+
"plugin disable time",
|
|
374
|
+
),
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function parseLockRecord(value: unknown): PortableProviderPluginLockRecordV1 {
|
|
379
|
+
const parsed = record(value, "portable plugin activation lock");
|
|
380
|
+
exactKeys(
|
|
381
|
+
parsed,
|
|
382
|
+
[
|
|
383
|
+
"acquiredAt",
|
|
384
|
+
"bootId",
|
|
385
|
+
"pid",
|
|
386
|
+
"pluginId",
|
|
387
|
+
"processStartId",
|
|
388
|
+
"schemaVersion",
|
|
389
|
+
],
|
|
390
|
+
"portable plugin activation lock",
|
|
391
|
+
);
|
|
392
|
+
if (
|
|
393
|
+
parsed.schemaVersion !== PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION
|
|
394
|
+
|| !Number.isSafeInteger(parsed.pid)
|
|
395
|
+
|| typeof parsed.pid !== "number"
|
|
396
|
+
|| parsed.pid < 1
|
|
397
|
+
|| typeof parsed.bootId !== "string"
|
|
398
|
+
|| !sha256Pattern.test(parsed.bootId)
|
|
399
|
+
|| typeof parsed.processStartId !== "string"
|
|
400
|
+
|| !sha256Pattern.test(parsed.processStartId)
|
|
401
|
+
) {
|
|
402
|
+
throw new Error("portable plugin activation lock is invalid");
|
|
403
|
+
}
|
|
404
|
+
return Object.freeze({
|
|
405
|
+
schemaVersion: parsed.schemaVersion,
|
|
406
|
+
pluginId: pluginId(parsed.pluginId, "locked plugin ID"),
|
|
407
|
+
acquiredAt: timestamp(parsed.acquiredAt, "plugin lock acquisition time"),
|
|
408
|
+
pid: parsed.pid,
|
|
409
|
+
bootId: parsed.bootId,
|
|
410
|
+
processStartId: parsed.processStartId,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function parseCatalogLockRecord(
|
|
415
|
+
value: unknown,
|
|
416
|
+
): PortableProviderPluginCatalogLockRecordV1 {
|
|
417
|
+
const parsed = record(value, "portable plugin catalog mutation lock");
|
|
418
|
+
exactKeys(
|
|
419
|
+
parsed,
|
|
420
|
+
[
|
|
421
|
+
"acquiredAt",
|
|
422
|
+
"bootId",
|
|
423
|
+
"pid",
|
|
424
|
+
"processStartId",
|
|
425
|
+
"schemaVersion",
|
|
426
|
+
"scope",
|
|
427
|
+
],
|
|
428
|
+
"portable plugin catalog mutation lock",
|
|
429
|
+
);
|
|
430
|
+
if (
|
|
431
|
+
parsed.schemaVersion !== PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION
|
|
432
|
+
|| parsed.scope !== "catalog-mutation"
|
|
433
|
+
|| !Number.isSafeInteger(parsed.pid)
|
|
434
|
+
|| typeof parsed.pid !== "number"
|
|
435
|
+
|| parsed.pid < 1
|
|
436
|
+
|| typeof parsed.bootId !== "string"
|
|
437
|
+
|| !sha256Pattern.test(parsed.bootId)
|
|
438
|
+
|| typeof parsed.processStartId !== "string"
|
|
439
|
+
|| !sha256Pattern.test(parsed.processStartId)
|
|
440
|
+
) {
|
|
441
|
+
throw new Error("portable plugin catalog mutation lock is invalid");
|
|
442
|
+
}
|
|
443
|
+
return Object.freeze({
|
|
444
|
+
schemaVersion: parsed.schemaVersion,
|
|
445
|
+
scope: parsed.scope,
|
|
446
|
+
acquiredAt: timestamp(
|
|
447
|
+
parsed.acquiredAt,
|
|
448
|
+
"plugin catalog lock acquisition time",
|
|
449
|
+
),
|
|
450
|
+
pid: parsed.pid,
|
|
451
|
+
bootId: parsed.bootId,
|
|
452
|
+
processStartId: parsed.processStartId,
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function parseLockClaimRecord(
|
|
457
|
+
value: unknown,
|
|
458
|
+
targetSha256: string,
|
|
459
|
+
claimId: string,
|
|
460
|
+
): PortableProviderPluginLockClaimV1 {
|
|
461
|
+
const parsed = record(value, "portable plugin lock arbitration claim");
|
|
462
|
+
exactKeys(
|
|
463
|
+
parsed,
|
|
464
|
+
[
|
|
465
|
+
"bootId",
|
|
466
|
+
"claimId",
|
|
467
|
+
"kind",
|
|
468
|
+
"pid",
|
|
469
|
+
"processStartId",
|
|
470
|
+
"schemaVersion",
|
|
471
|
+
"targetSha256",
|
|
472
|
+
],
|
|
473
|
+
"portable plugin lock arbitration claim",
|
|
474
|
+
);
|
|
475
|
+
if (
|
|
476
|
+
parsed.schemaVersion !== PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION
|
|
477
|
+
|| parsed.kind !== "portable-provider-plugin-lock-claim"
|
|
478
|
+
|| parsed.targetSha256 !== targetSha256
|
|
479
|
+
|| parsed.claimId !== claimId
|
|
480
|
+
|| !lockClaimIdPattern.test(claimId)
|
|
481
|
+
|| !Number.isSafeInteger(parsed.pid)
|
|
482
|
+
|| typeof parsed.pid !== "number"
|
|
483
|
+
|| parsed.pid < 1
|
|
484
|
+
|| typeof parsed.bootId !== "string"
|
|
485
|
+
|| !sha256Pattern.test(parsed.bootId)
|
|
486
|
+
|| typeof parsed.processStartId !== "string"
|
|
487
|
+
|| !sha256Pattern.test(parsed.processStartId)
|
|
488
|
+
) {
|
|
489
|
+
throw new Error("portable plugin lock arbitration claim is invalid");
|
|
490
|
+
}
|
|
491
|
+
return Object.freeze({
|
|
492
|
+
schemaVersion: parsed.schemaVersion,
|
|
493
|
+
kind: parsed.kind,
|
|
494
|
+
targetSha256: parsed.targetSha256,
|
|
495
|
+
claimId: parsed.claimId,
|
|
496
|
+
pid: parsed.pid,
|
|
497
|
+
bootId: parsed.bootId,
|
|
498
|
+
processStartId: parsed.processStartId,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function physicalPathThroughExistingAncestor(path: string): string {
|
|
503
|
+
let ancestor = resolve(path);
|
|
504
|
+
const missingSegments: string[] = [];
|
|
505
|
+
for (;;) {
|
|
506
|
+
try {
|
|
507
|
+
return join(
|
|
508
|
+
realpathSync(ancestor),
|
|
509
|
+
...[...missingSegments].reverse(),
|
|
510
|
+
);
|
|
511
|
+
} catch (error) {
|
|
512
|
+
if (!hasCode(error, "ENOENT") && !hasCode(error, "ENOTDIR")) {
|
|
513
|
+
throw new Error(
|
|
514
|
+
"portable plugin store path could not be resolved safely",
|
|
515
|
+
{ cause: error },
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
const parent = dirname(ancestor);
|
|
519
|
+
if (parent === ancestor) {
|
|
520
|
+
throw new Error(
|
|
521
|
+
"portable plugin store path has no resolvable physical ancestor",
|
|
522
|
+
{ cause: error },
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
missingSegments.push(basename(ancestor));
|
|
526
|
+
ancestor = parent;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function physicalPathThroughExistingParent(path: string): string {
|
|
532
|
+
const absolute = resolve(path);
|
|
533
|
+
return join(
|
|
534
|
+
physicalPathThroughExistingAncestor(dirname(absolute)),
|
|
535
|
+
basename(absolute),
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
export function portableProviderPluginStorePaths(
|
|
540
|
+
root: string,
|
|
541
|
+
): PortableProviderPluginStorePaths {
|
|
542
|
+
const canonicalRoot = physicalPathThroughExistingAncestor(root);
|
|
543
|
+
return Object.freeze({
|
|
544
|
+
root: canonicalRoot,
|
|
545
|
+
artifacts: join(canonicalRoot, "artifacts"),
|
|
546
|
+
trust: join(canonicalRoot, "trust"),
|
|
547
|
+
active: join(canonicalRoot, "active"),
|
|
548
|
+
locks: join(canonicalRoot, "locks"),
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function ownedByCurrentUser(stats: { readonly uid: number }): boolean {
|
|
553
|
+
const currentUid = typeof process.getuid === "function"
|
|
554
|
+
? process.getuid()
|
|
555
|
+
: undefined;
|
|
556
|
+
return currentUid === undefined || stats.uid === currentUid;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function sameFileIdentity(
|
|
560
|
+
left: ReturnType<typeof fstatSync>,
|
|
561
|
+
right: ReturnType<typeof fstatSync>,
|
|
562
|
+
): boolean {
|
|
563
|
+
return left.dev === right.dev
|
|
564
|
+
&& left.ino === right.ino
|
|
565
|
+
&& left.size === right.size
|
|
566
|
+
&& left.mtimeMs === right.mtimeMs
|
|
567
|
+
&& left.ctimeMs === right.ctimeMs;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function sameInodeIdentity(
|
|
571
|
+
left: ReturnType<typeof fstatSync>,
|
|
572
|
+
right: ReturnType<typeof fstatSync>,
|
|
573
|
+
): boolean {
|
|
574
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
type PrivateRecordSnapshot<T> = {
|
|
578
|
+
readonly value: T;
|
|
579
|
+
readonly identity: ReturnType<typeof fstatSync>;
|
|
580
|
+
};
|
|
581
|
+
|
|
582
|
+
class PrivateRecordChangedDuringReadError extends Error {}
|
|
583
|
+
|
|
584
|
+
function changedDuringPrivateRecordRead(
|
|
585
|
+
label: string,
|
|
586
|
+
): PrivateRecordChangedDuringReadError {
|
|
587
|
+
return new PrivateRecordChangedDuringReadError(
|
|
588
|
+
`${label} changed while it was being read`,
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function pauseAfterLockClaimReadForTest(
|
|
593
|
+
path: string,
|
|
594
|
+
label: string,
|
|
595
|
+
): void {
|
|
596
|
+
if (
|
|
597
|
+
process.env.NODE_ENV !== "test"
|
|
598
|
+
|| label !== "portable plugin lock arbitration claim"
|
|
599
|
+
) {
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
const readyPath = (process.env.GHOSTGET_TEST_PLUGIN_LOCK_CLAIM_READ_READY ?? process.env.WRENCH_TEST_PLUGIN_LOCK_CLAIM_READ_READY);
|
|
603
|
+
const releasePath = (process.env.GHOSTGET_TEST_PLUGIN_LOCK_CLAIM_READ_RELEASE ?? process.env.WRENCH_TEST_PLUGIN_LOCK_CLAIM_READ_RELEASE);
|
|
604
|
+
const targetName = (process.env.GHOSTGET_TEST_PLUGIN_LOCK_CLAIM_READ_TARGET ?? process.env.WRENCH_TEST_PLUGIN_LOCK_CLAIM_READ_TARGET);
|
|
605
|
+
if (
|
|
606
|
+
readyPath === undefined
|
|
607
|
+
|| releasePath === undefined
|
|
608
|
+
|| targetName === undefined
|
|
609
|
+
|| basename(path) !== targetName
|
|
610
|
+
|| existsSync(readyPath)
|
|
611
|
+
) {
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
writePrivateFile(readyPath, Buffer.from("read\n", "utf8"));
|
|
615
|
+
syncDirectory(dirname(readyPath));
|
|
616
|
+
const deadline = Date.now() + TEST_BARRIER_TIMEOUT_MS;
|
|
617
|
+
while (!existsSync(releasePath)) {
|
|
618
|
+
if (Date.now() >= deadline) {
|
|
619
|
+
throw new Error("portable plugin lock claim read test barrier timed out");
|
|
620
|
+
}
|
|
621
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function readPrivateRecordSnapshotIfPresent<T>(
|
|
626
|
+
path: string,
|
|
627
|
+
label: string,
|
|
628
|
+
parse: (value: unknown) => T,
|
|
629
|
+
): PrivateRecordSnapshot<T> | null {
|
|
630
|
+
const noFollow = "O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0;
|
|
631
|
+
let descriptor: number;
|
|
632
|
+
try {
|
|
633
|
+
descriptor = openSync(path, constants.O_RDONLY | noFollow);
|
|
634
|
+
} catch (error) {
|
|
635
|
+
if (hasCode(error, "ENOENT")) return null;
|
|
636
|
+
throw new Error(`could not safely open ${label}`, { cause: error });
|
|
637
|
+
}
|
|
638
|
+
try {
|
|
639
|
+
const before = fstatSync(descriptor);
|
|
640
|
+
if (
|
|
641
|
+
!before.isFile()
|
|
642
|
+
|| !ownedByCurrentUser(before)
|
|
643
|
+
|| (before.mode & 0o077) !== 0
|
|
644
|
+
|| before.size < 2
|
|
645
|
+
|| before.size > MAX_STORE_RECORD_BYTES
|
|
646
|
+
) {
|
|
647
|
+
throw new Error(`${label} must be one bounded private regular file`);
|
|
648
|
+
}
|
|
649
|
+
const bytes = readFileSync(descriptor);
|
|
650
|
+
pauseAfterLockClaimReadForTest(path, label);
|
|
651
|
+
const after = fstatSync(descriptor);
|
|
652
|
+
if (
|
|
653
|
+
bytes.byteLength !== before.size
|
|
654
|
+
|| !sameFileIdentity(before, after)
|
|
655
|
+
) {
|
|
656
|
+
throw changedDuringPrivateRecordRead(label);
|
|
657
|
+
}
|
|
658
|
+
let linked: ReturnType<typeof lstatSync>;
|
|
659
|
+
try {
|
|
660
|
+
linked = lstatSync(path);
|
|
661
|
+
} catch (error) {
|
|
662
|
+
if (hasCode(error, "ENOENT")) {
|
|
663
|
+
throw changedDuringPrivateRecordRead(label);
|
|
664
|
+
}
|
|
665
|
+
throw new Error(`could not safely verify ${label}`, { cause: error });
|
|
666
|
+
}
|
|
667
|
+
if (
|
|
668
|
+
linked.isSymbolicLink()
|
|
669
|
+
|| !linked.isFile()
|
|
670
|
+
|| !sameInodeIdentity(after, linked)
|
|
671
|
+
) {
|
|
672
|
+
throw changedDuringPrivateRecordRead(label);
|
|
673
|
+
}
|
|
674
|
+
let text: string;
|
|
675
|
+
try {
|
|
676
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
677
|
+
} catch {
|
|
678
|
+
throw new Error(`${label} must contain valid UTF-8`);
|
|
679
|
+
}
|
|
680
|
+
let value: unknown;
|
|
681
|
+
try {
|
|
682
|
+
value = JSON.parse(text) as unknown;
|
|
683
|
+
} catch {
|
|
684
|
+
throw new Error(`${label} must contain valid JSON`);
|
|
685
|
+
}
|
|
686
|
+
const result = parse(value);
|
|
687
|
+
if (text !== `${canonicalJson(result)}\n`) {
|
|
688
|
+
throw new Error(`${label} must use canonical JSON encoding`);
|
|
689
|
+
}
|
|
690
|
+
return Object.freeze({ value: result, identity: after });
|
|
691
|
+
} finally {
|
|
692
|
+
closeSync(descriptor);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function readPrivateRecordIfPresent<T>(
|
|
697
|
+
path: string,
|
|
698
|
+
label: string,
|
|
699
|
+
parse: (value: unknown) => T,
|
|
700
|
+
): T | null {
|
|
701
|
+
return readPrivateRecordSnapshotIfPresent(path, label, parse)?.value ?? null;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function syncDirectory(path: string): void {
|
|
705
|
+
const directoryFlag = "O_DIRECTORY" in constants ? constants.O_DIRECTORY : 0;
|
|
706
|
+
const descriptor = openSync(path, constants.O_RDONLY | directoryFlag);
|
|
707
|
+
try {
|
|
708
|
+
fsyncSync(descriptor);
|
|
709
|
+
} finally {
|
|
710
|
+
closeSync(descriptor);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
type LockPublicationKind = "claim" | "lock";
|
|
715
|
+
|
|
716
|
+
type PublishedPrivateLockFile = {
|
|
717
|
+
readonly descriptor: number;
|
|
718
|
+
readonly identity: ReturnType<typeof fstatSync>;
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
function lockPublicationStagePath(
|
|
722
|
+
parent: string,
|
|
723
|
+
owner: ProcessOwnerIdentity,
|
|
724
|
+
): string {
|
|
725
|
+
return join(
|
|
726
|
+
parent,
|
|
727
|
+
`${lockPublicationPrefix}${owner.pid}-${owner.bootId}-`
|
|
728
|
+
+ `${owner.processStartId}-${randomUUID()}.tmp`,
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function parseLockPublicationOwner(name: string): ProcessOwnerIdentity {
|
|
733
|
+
const match = lockPublicationNamePattern.exec(name);
|
|
734
|
+
if (match === null) {
|
|
735
|
+
throw new Error("portable plugin lock publication filename is invalid");
|
|
736
|
+
}
|
|
737
|
+
const pid = Number(match[1]);
|
|
738
|
+
if (!Number.isSafeInteger(pid) || pid < 1) {
|
|
739
|
+
throw new Error("portable plugin lock publication owner is invalid");
|
|
740
|
+
}
|
|
741
|
+
return Object.freeze({
|
|
742
|
+
pid,
|
|
743
|
+
bootId: match[2]!,
|
|
744
|
+
processStartId: match[3]!,
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function stableProcessOwnerStatus(
|
|
749
|
+
owner: ProcessOwnerIdentity,
|
|
750
|
+
): ReturnType<typeof processOwnerStatus> {
|
|
751
|
+
let status = processOwnerStatus(owner);
|
|
752
|
+
for (let attempt = 0; status === "unknown" && attempt < 3; attempt += 1) {
|
|
753
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
754
|
+
status = processOwnerStatus(owner);
|
|
755
|
+
}
|
|
756
|
+
return status;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function reclaimDeadLockPublications(parent: string): void {
|
|
760
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
761
|
+
const names = readdirSync(parent)
|
|
762
|
+
.filter((name) => name.startsWith(lockPublicationPrefix))
|
|
763
|
+
.sort();
|
|
764
|
+
if (names.length > MAX_LOCK_PUBLICATIONS) {
|
|
765
|
+
throw new Error("portable plugin lock publication count exceeds its bound");
|
|
766
|
+
}
|
|
767
|
+
let retry = false;
|
|
768
|
+
let removed = false;
|
|
769
|
+
for (const name of names) {
|
|
770
|
+
const owner = parseLockPublicationOwner(name);
|
|
771
|
+
const status = stableProcessOwnerStatus(owner);
|
|
772
|
+
if (status === "unknown") {
|
|
773
|
+
throw new Error(
|
|
774
|
+
"portable plugin lock publication owner cannot be inspected safely",
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
if (status === "exact-live-owner") continue;
|
|
778
|
+
const path = join(parent, name);
|
|
779
|
+
let identity: ReturnType<typeof fstatSync>;
|
|
780
|
+
try {
|
|
781
|
+
const descriptor = openSync(
|
|
782
|
+
path,
|
|
783
|
+
constants.O_RDONLY
|
|
784
|
+
| ("O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0),
|
|
785
|
+
);
|
|
786
|
+
try {
|
|
787
|
+
identity = fstatSync(descriptor);
|
|
788
|
+
if (
|
|
789
|
+
!identity.isFile()
|
|
790
|
+
|| !ownedByCurrentUser(identity)
|
|
791
|
+
|| (identity.mode & 0o077) !== 0
|
|
792
|
+
|| identity.size > MAX_STORE_RECORD_BYTES
|
|
793
|
+
) {
|
|
794
|
+
throw new Error(
|
|
795
|
+
"portable plugin lock publication must be one bounded "
|
|
796
|
+
+ "private regular file",
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
} finally {
|
|
800
|
+
closeSync(descriptor);
|
|
801
|
+
}
|
|
802
|
+
} catch (error) {
|
|
803
|
+
if (hasCode(error, "ENOENT")) {
|
|
804
|
+
retry = true;
|
|
805
|
+
break;
|
|
806
|
+
}
|
|
807
|
+
throw error;
|
|
808
|
+
}
|
|
809
|
+
if (removeExactPrivateFile(
|
|
810
|
+
path,
|
|
811
|
+
identity,
|
|
812
|
+
"portable plugin lock publication",
|
|
813
|
+
)) {
|
|
814
|
+
removed = true;
|
|
815
|
+
} else {
|
|
816
|
+
retry = true;
|
|
817
|
+
break;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
if (retry || removed) continue;
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
throw new Error(
|
|
824
|
+
"portable plugin lock publications did not reach a stable snapshot",
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function crashLockPublicationForTest(
|
|
829
|
+
kind: LockPublicationKind,
|
|
830
|
+
logicalLockPath: string,
|
|
831
|
+
descriptor: number,
|
|
832
|
+
bytes: Uint8Array,
|
|
833
|
+
): void {
|
|
834
|
+
if (process.env.NODE_ENV !== "test") return;
|
|
835
|
+
const fault = (process.env.GHOSTGET_TEST_PLUGIN_LOCK_PUBLICATION_CRASH ?? process.env.WRENCH_TEST_PLUGIN_LOCK_PUBLICATION_CRASH);
|
|
836
|
+
const targetName = (process.env.GHOSTGET_TEST_PLUGIN_LOCK_PUBLICATION_TARGET ?? process.env.WRENCH_TEST_PLUGIN_LOCK_PUBLICATION_TARGET);
|
|
837
|
+
if (
|
|
838
|
+
targetName === undefined
|
|
839
|
+
|| basename(logicalLockPath) !== targetName
|
|
840
|
+
|| (
|
|
841
|
+
fault !== `${kind}-empty-stage`
|
|
842
|
+
&& fault !== `${kind}-partial-stage`
|
|
843
|
+
)
|
|
844
|
+
) {
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
if (fault.endsWith("partial-stage")) {
|
|
848
|
+
writeFileSync(
|
|
849
|
+
descriptor,
|
|
850
|
+
bytes.subarray(0, Math.max(1, Math.floor(bytes.byteLength / 2))),
|
|
851
|
+
);
|
|
852
|
+
}
|
|
853
|
+
fchmodSync(descriptor, 0o600);
|
|
854
|
+
fsyncSync(descriptor);
|
|
855
|
+
process.exit(92);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
function unlinkIfPresent(path: string): boolean {
|
|
859
|
+
try {
|
|
860
|
+
unlinkSync(path);
|
|
861
|
+
return true;
|
|
862
|
+
} catch (error) {
|
|
863
|
+
if (hasCode(error, "ENOENT")) return false;
|
|
864
|
+
throw error;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function cleanupFailedLockPublication(
|
|
869
|
+
path: string,
|
|
870
|
+
stage: string,
|
|
871
|
+
parent: string,
|
|
872
|
+
descriptor: number | null,
|
|
873
|
+
identity: ReturnType<typeof fstatSync> | null,
|
|
874
|
+
targetLinked: boolean,
|
|
875
|
+
directoryChanged: boolean,
|
|
876
|
+
): void {
|
|
877
|
+
let cleanupError: unknown;
|
|
878
|
+
if (targetLinked && identity !== null) {
|
|
879
|
+
try {
|
|
880
|
+
removeExactPrivateFile(
|
|
881
|
+
path,
|
|
882
|
+
identity,
|
|
883
|
+
"portable plugin lock publication",
|
|
884
|
+
);
|
|
885
|
+
} catch (error) {
|
|
886
|
+
cleanupError = error;
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
if (descriptor !== null) {
|
|
890
|
+
try {
|
|
891
|
+
closeSync(descriptor);
|
|
892
|
+
} catch (error) {
|
|
893
|
+
cleanupError ??= error;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
try {
|
|
897
|
+
if (unlinkIfPresent(stage)) directoryChanged = true;
|
|
898
|
+
} catch (error) {
|
|
899
|
+
cleanupError ??= error;
|
|
900
|
+
}
|
|
901
|
+
if (directoryChanged) {
|
|
902
|
+
try {
|
|
903
|
+
syncDirectory(parent);
|
|
904
|
+
} catch (error) {
|
|
905
|
+
cleanupError ??= error;
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
if (cleanupError !== undefined) {
|
|
909
|
+
throw cleanupError instanceof Error
|
|
910
|
+
? cleanupError
|
|
911
|
+
: new Error("portable plugin lock publication cleanup failed", {
|
|
912
|
+
cause: cleanupError,
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function publishPrivateLockFileExclusive(
|
|
918
|
+
path: string,
|
|
919
|
+
bytes: Uint8Array,
|
|
920
|
+
owner: ProcessOwnerIdentity,
|
|
921
|
+
kind: LockPublicationKind,
|
|
922
|
+
logicalLockPath: string,
|
|
923
|
+
): PublishedPrivateLockFile | null {
|
|
924
|
+
const parent = dirname(path);
|
|
925
|
+
const stage = lockPublicationStagePath(parent, owner);
|
|
926
|
+
const noFollow = "O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0;
|
|
927
|
+
let descriptor: number | null = null;
|
|
928
|
+
let identity: ReturnType<typeof fstatSync> | null = null;
|
|
929
|
+
let targetLinked = false;
|
|
930
|
+
let transferred = false;
|
|
931
|
+
let directoryChanged = false;
|
|
932
|
+
try {
|
|
933
|
+
// The stage name carries enough process identity to reclaim even an empty
|
|
934
|
+
// crash remnant. The hard link below is the only publication point, so a
|
|
935
|
+
// reader can observe either no lock or one complete, fsynced record.
|
|
936
|
+
descriptor = openSync(
|
|
937
|
+
stage,
|
|
938
|
+
constants.O_WRONLY
|
|
939
|
+
| constants.O_CREAT
|
|
940
|
+
| constants.O_EXCL
|
|
941
|
+
| noFollow,
|
|
942
|
+
0o600,
|
|
943
|
+
);
|
|
944
|
+
directoryChanged = true;
|
|
945
|
+
crashLockPublicationForTest(
|
|
946
|
+
kind,
|
|
947
|
+
logicalLockPath,
|
|
948
|
+
descriptor,
|
|
949
|
+
bytes,
|
|
950
|
+
);
|
|
951
|
+
writeFileSync(descriptor, bytes);
|
|
952
|
+
fchmodSync(descriptor, 0o600);
|
|
953
|
+
fsyncSync(descriptor);
|
|
954
|
+
identity = fstatSync(descriptor);
|
|
955
|
+
try {
|
|
956
|
+
linkSync(stage, path);
|
|
957
|
+
targetLinked = true;
|
|
958
|
+
} catch (error) {
|
|
959
|
+
if (!hasCode(error, "EEXIST")) throw error;
|
|
960
|
+
return null;
|
|
961
|
+
}
|
|
962
|
+
unlinkSync(stage);
|
|
963
|
+
syncDirectory(parent);
|
|
964
|
+
directoryChanged = false;
|
|
965
|
+
transferred = true;
|
|
966
|
+
return Object.freeze({ descriptor, identity });
|
|
967
|
+
} finally {
|
|
968
|
+
if (!transferred) {
|
|
969
|
+
cleanupFailedLockPublication(
|
|
970
|
+
path,
|
|
971
|
+
stage,
|
|
972
|
+
parent,
|
|
973
|
+
descriptor,
|
|
974
|
+
identity,
|
|
975
|
+
targetLinked,
|
|
976
|
+
directoryChanged,
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function writePrivateFile(path: string, bytes: Uint8Array): void {
|
|
983
|
+
const noFollow = "O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0;
|
|
984
|
+
const descriptor = openSync(
|
|
985
|
+
path,
|
|
986
|
+
constants.O_WRONLY
|
|
987
|
+
| constants.O_CREAT
|
|
988
|
+
| constants.O_EXCL
|
|
989
|
+
| noFollow,
|
|
990
|
+
0o600,
|
|
991
|
+
);
|
|
992
|
+
try {
|
|
993
|
+
writeFileSync(descriptor, bytes);
|
|
994
|
+
fchmodSync(descriptor, 0o600);
|
|
995
|
+
fsyncSync(descriptor);
|
|
996
|
+
} finally {
|
|
997
|
+
closeSync(descriptor);
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function lockClaimTargetSha256(path: string): string {
|
|
1002
|
+
return createHash("sha256")
|
|
1003
|
+
.update("portable-provider-plugin-lock", "utf8")
|
|
1004
|
+
.update("\0", "utf8")
|
|
1005
|
+
.update(path, "utf8")
|
|
1006
|
+
.digest("hex");
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function lockClaimPrefix(targetSha256: string): string {
|
|
1010
|
+
return `.lock-claim-${targetSha256}-`;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function lockClaimName(
|
|
1014
|
+
parent: string,
|
|
1015
|
+
targetSha256: string,
|
|
1016
|
+
phase: PortableProviderPluginLockClaimPhase,
|
|
1017
|
+
claimId: string,
|
|
1018
|
+
): string {
|
|
1019
|
+
return join(
|
|
1020
|
+
parent,
|
|
1021
|
+
`${lockClaimPrefix(targetSha256)}${phase}-${claimId}.json`,
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function parseLockClaimName(
|
|
1026
|
+
name: string,
|
|
1027
|
+
targetSha256: string,
|
|
1028
|
+
): {
|
|
1029
|
+
readonly phase: PortableProviderPluginLockClaimPhase;
|
|
1030
|
+
readonly claimId: string;
|
|
1031
|
+
} {
|
|
1032
|
+
const prefix = lockClaimPrefix(targetSha256);
|
|
1033
|
+
if (!name.startsWith(prefix)) {
|
|
1034
|
+
throw new Error("portable plugin lock claim does not match its target");
|
|
1035
|
+
}
|
|
1036
|
+
const match =
|
|
1037
|
+
/^(waiting|candidate|held)-([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\.json$/u
|
|
1038
|
+
.exec(name.slice(prefix.length));
|
|
1039
|
+
if (match === null) {
|
|
1040
|
+
throw new Error("portable plugin lock claim filename is invalid");
|
|
1041
|
+
}
|
|
1042
|
+
return {
|
|
1043
|
+
phase: match[1] as PortableProviderPluginLockClaimPhase,
|
|
1044
|
+
claimId: match[2]!,
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function listLiveLockClaims(
|
|
1049
|
+
parent: string,
|
|
1050
|
+
targetSha256: string,
|
|
1051
|
+
): readonly {
|
|
1052
|
+
readonly path: string;
|
|
1053
|
+
readonly phase: PortableProviderPluginLockClaimPhase;
|
|
1054
|
+
readonly claim: PortableProviderPluginLockClaimV1;
|
|
1055
|
+
}[] {
|
|
1056
|
+
const prefix = lockClaimPrefix(targetSha256);
|
|
1057
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
1058
|
+
const names = readdirSync(parent)
|
|
1059
|
+
.filter((name) => name.startsWith(prefix))
|
|
1060
|
+
.sort();
|
|
1061
|
+
if (names.length > MAX_LOCK_CLAIMS) {
|
|
1062
|
+
throw new Error("portable plugin lock claim count exceeds its bound");
|
|
1063
|
+
}
|
|
1064
|
+
let retry = false;
|
|
1065
|
+
let removedStaleClaim = false;
|
|
1066
|
+
const live: {
|
|
1067
|
+
readonly path: string;
|
|
1068
|
+
readonly phase: PortableProviderPluginLockClaimPhase;
|
|
1069
|
+
readonly claim: PortableProviderPluginLockClaimV1;
|
|
1070
|
+
}[] = [];
|
|
1071
|
+
for (const name of names) {
|
|
1072
|
+
const parsedName = parseLockClaimName(name, targetSha256);
|
|
1073
|
+
const path = join(parent, name);
|
|
1074
|
+
let snapshot: PrivateRecordSnapshot<
|
|
1075
|
+
PortableProviderPluginLockClaimV1
|
|
1076
|
+
> | null;
|
|
1077
|
+
try {
|
|
1078
|
+
snapshot = readPrivateRecordSnapshotIfPresent(
|
|
1079
|
+
path,
|
|
1080
|
+
"portable plugin lock arbitration claim",
|
|
1081
|
+
(value) => parseLockClaimRecord(
|
|
1082
|
+
value,
|
|
1083
|
+
targetSha256,
|
|
1084
|
+
parsedName.claimId,
|
|
1085
|
+
),
|
|
1086
|
+
);
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
if (error instanceof PrivateRecordChangedDuringReadError) {
|
|
1089
|
+
retry = true;
|
|
1090
|
+
break;
|
|
1091
|
+
}
|
|
1092
|
+
throw error;
|
|
1093
|
+
}
|
|
1094
|
+
if (snapshot === null) {
|
|
1095
|
+
retry = true;
|
|
1096
|
+
break;
|
|
1097
|
+
}
|
|
1098
|
+
const status = stableProcessOwnerStatus(snapshot.value);
|
|
1099
|
+
if (status === "unknown") {
|
|
1100
|
+
throw new Error(
|
|
1101
|
+
"portable plugin lock arbitration owner cannot be inspected safely",
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
if (status === "different-or-dead") {
|
|
1105
|
+
if (removeExactPrivateFile(
|
|
1106
|
+
path,
|
|
1107
|
+
snapshot.identity,
|
|
1108
|
+
"portable plugin lock arbitration claim",
|
|
1109
|
+
)) {
|
|
1110
|
+
removedStaleClaim = true;
|
|
1111
|
+
} else {
|
|
1112
|
+
retry = true;
|
|
1113
|
+
break;
|
|
1114
|
+
}
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
live.push({
|
|
1118
|
+
path,
|
|
1119
|
+
phase: parsedName.phase,
|
|
1120
|
+
claim: snapshot.value,
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
if (retry || removedStaleClaim) continue;
|
|
1124
|
+
return live;
|
|
1125
|
+
}
|
|
1126
|
+
throw new Error(
|
|
1127
|
+
"portable plugin lock claims did not reach a stable snapshot",
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function acquireLockClaim(path: string): (() => void) | null {
|
|
1132
|
+
const logicalLockPath = physicalPathThroughExistingParent(path);
|
|
1133
|
+
const parent = dirname(logicalLockPath);
|
|
1134
|
+
const targetSha256 = lockClaimTargetSha256(logicalLockPath);
|
|
1135
|
+
const claimId = randomUUID();
|
|
1136
|
+
const claim: PortableProviderPluginLockClaimV1 = Object.freeze({
|
|
1137
|
+
schemaVersion: PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION,
|
|
1138
|
+
kind: "portable-provider-plugin-lock-claim",
|
|
1139
|
+
targetSha256,
|
|
1140
|
+
claimId,
|
|
1141
|
+
pid: process.pid,
|
|
1142
|
+
...currentProcessStartIdentity(),
|
|
1143
|
+
});
|
|
1144
|
+
let claimPath = lockClaimName(
|
|
1145
|
+
parent,
|
|
1146
|
+
targetSha256,
|
|
1147
|
+
"waiting",
|
|
1148
|
+
claimId,
|
|
1149
|
+
);
|
|
1150
|
+
reclaimDeadLockPublications(parent);
|
|
1151
|
+
const publication = publishPrivateLockFileExclusive(
|
|
1152
|
+
claimPath,
|
|
1153
|
+
Buffer.from(`${canonicalJson(claim)}\n`, "utf8"),
|
|
1154
|
+
claim,
|
|
1155
|
+
"claim",
|
|
1156
|
+
logicalLockPath,
|
|
1157
|
+
);
|
|
1158
|
+
if (publication === null) {
|
|
1159
|
+
throw new Error("portable plugin lock claim ID was already published");
|
|
1160
|
+
}
|
|
1161
|
+
const claimIdentity = publication.identity;
|
|
1162
|
+
const release = (): void => {
|
|
1163
|
+
removeExactPrivateFile(
|
|
1164
|
+
claimPath,
|
|
1165
|
+
claimIdentity,
|
|
1166
|
+
"portable plugin lock arbitration claim",
|
|
1167
|
+
);
|
|
1168
|
+
};
|
|
1169
|
+
try {
|
|
1170
|
+
closeSync(publication.descriptor);
|
|
1171
|
+
pauseAfterWaitingLockClaimForTest(logicalLockPath);
|
|
1172
|
+
const waiting = listLiveLockClaims(parent, targetSha256);
|
|
1173
|
+
if (
|
|
1174
|
+
waiting.some((candidate) =>
|
|
1175
|
+
candidate.claim.claimId !== claimId
|
|
1176
|
+
&& (
|
|
1177
|
+
candidate.phase !== "waiting"
|
|
1178
|
+
|| candidate.claim.claimId < claimId
|
|
1179
|
+
)
|
|
1180
|
+
)
|
|
1181
|
+
) {
|
|
1182
|
+
release();
|
|
1183
|
+
return null;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
const candidatePath = lockClaimName(
|
|
1187
|
+
parent,
|
|
1188
|
+
targetSha256,
|
|
1189
|
+
"candidate",
|
|
1190
|
+
claimId,
|
|
1191
|
+
);
|
|
1192
|
+
renameSync(claimPath, candidatePath);
|
|
1193
|
+
claimPath = candidatePath;
|
|
1194
|
+
syncDirectory(parent);
|
|
1195
|
+
const candidates = listLiveLockClaims(parent, targetSha256);
|
|
1196
|
+
if (
|
|
1197
|
+
candidates.some((candidate) =>
|
|
1198
|
+
candidate.claim.claimId !== claimId
|
|
1199
|
+
&& (
|
|
1200
|
+
candidate.phase === "held"
|
|
1201
|
+
|| candidate.claim.claimId < claimId
|
|
1202
|
+
)
|
|
1203
|
+
)
|
|
1204
|
+
) {
|
|
1205
|
+
release();
|
|
1206
|
+
return null;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
const heldPath = lockClaimName(
|
|
1210
|
+
parent,
|
|
1211
|
+
targetSha256,
|
|
1212
|
+
"held",
|
|
1213
|
+
claimId,
|
|
1214
|
+
);
|
|
1215
|
+
renameSync(claimPath, heldPath);
|
|
1216
|
+
claimPath = heldPath;
|
|
1217
|
+
syncDirectory(parent);
|
|
1218
|
+
const held = listLiveLockClaims(parent, targetSha256);
|
|
1219
|
+
if (
|
|
1220
|
+
held.some((candidate) =>
|
|
1221
|
+
candidate.claim.claimId !== claimId
|
|
1222
|
+
&& candidate.phase === "held"
|
|
1223
|
+
)
|
|
1224
|
+
) {
|
|
1225
|
+
throw new Error(
|
|
1226
|
+
"portable plugin lock arbitration admitted two owners",
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
return release;
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
release();
|
|
1232
|
+
throw error;
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function pauseBeforeLockClaimForTest(path: string): void {
|
|
1237
|
+
if (process.env.NODE_ENV !== "test") return;
|
|
1238
|
+
const readyPath = (process.env.GHOSTGET_TEST_PLUGIN_LOCK_CLAIM_READY ?? process.env.WRENCH_TEST_PLUGIN_LOCK_CLAIM_READY);
|
|
1239
|
+
const releasePath = (process.env.GHOSTGET_TEST_PLUGIN_LOCK_CLAIM_RELEASE ?? process.env.WRENCH_TEST_PLUGIN_LOCK_CLAIM_RELEASE);
|
|
1240
|
+
const targetName = (process.env.GHOSTGET_TEST_PLUGIN_LOCK_CLAIM_TARGET ?? process.env.WRENCH_TEST_PLUGIN_LOCK_CLAIM_TARGET);
|
|
1241
|
+
if (
|
|
1242
|
+
readyPath === undefined
|
|
1243
|
+
|| releasePath === undefined
|
|
1244
|
+
|| targetName === undefined
|
|
1245
|
+
|| basename(path) !== targetName
|
|
1246
|
+
) {
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
writePrivateFile(readyPath, Buffer.from("ready\n", "utf8"));
|
|
1250
|
+
syncDirectory(dirname(readyPath));
|
|
1251
|
+
const deadline = Date.now() + TEST_BARRIER_TIMEOUT_MS;
|
|
1252
|
+
while (!existsSync(releasePath)) {
|
|
1253
|
+
if (Date.now() >= deadline) {
|
|
1254
|
+
throw new Error("portable plugin lock claim test barrier timed out");
|
|
1255
|
+
}
|
|
1256
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function pauseAfterWaitingLockClaimForTest(path: string): void {
|
|
1261
|
+
if (process.env.NODE_ENV !== "test") return;
|
|
1262
|
+
const readyPath = (process.env.GHOSTGET_TEST_PLUGIN_LOCK_WAITING_READY ?? process.env.WRENCH_TEST_PLUGIN_LOCK_WAITING_READY);
|
|
1263
|
+
const releasePath = (process.env.GHOSTGET_TEST_PLUGIN_LOCK_WAITING_RELEASE ?? process.env.WRENCH_TEST_PLUGIN_LOCK_WAITING_RELEASE);
|
|
1264
|
+
const targetName = (process.env.GHOSTGET_TEST_PLUGIN_LOCK_WAITING_TARGET ?? process.env.WRENCH_TEST_PLUGIN_LOCK_WAITING_TARGET);
|
|
1265
|
+
if (
|
|
1266
|
+
readyPath === undefined
|
|
1267
|
+
|| releasePath === undefined
|
|
1268
|
+
|| targetName === undefined
|
|
1269
|
+
|| basename(path) !== targetName
|
|
1270
|
+
) {
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
if (existsSync(readyPath)) return;
|
|
1274
|
+
writePrivateFile(readyPath, Buffer.from("waiting\n", "utf8"));
|
|
1275
|
+
syncDirectory(dirname(readyPath));
|
|
1276
|
+
const deadline = Date.now() + TEST_BARRIER_TIMEOUT_MS;
|
|
1277
|
+
while (!existsSync(releasePath)) {
|
|
1278
|
+
if (Date.now() >= deadline) {
|
|
1279
|
+
throw new Error("portable plugin waiting-claim test barrier timed out");
|
|
1280
|
+
}
|
|
1281
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
function removeExactPrivateFile(
|
|
1286
|
+
path: string,
|
|
1287
|
+
expectedIdentity: ReturnType<typeof fstatSync>,
|
|
1288
|
+
label: string,
|
|
1289
|
+
changedMessage = `${label} changed during exact removal`,
|
|
1290
|
+
): boolean {
|
|
1291
|
+
const parent = dirname(path);
|
|
1292
|
+
const quarantine = join(
|
|
1293
|
+
parent,
|
|
1294
|
+
`.lock-reclaim-${process.pid}-${randomUUID()}`,
|
|
1295
|
+
);
|
|
1296
|
+
try {
|
|
1297
|
+
renameSync(path, quarantine);
|
|
1298
|
+
} catch (error) {
|
|
1299
|
+
if (hasCode(error, "ENOENT")) return false;
|
|
1300
|
+
throw error;
|
|
1301
|
+
}
|
|
1302
|
+
const moved = lstatSync(quarantine);
|
|
1303
|
+
if (
|
|
1304
|
+
moved.isSymbolicLink()
|
|
1305
|
+
|| !moved.isFile()
|
|
1306
|
+
|| !sameInodeIdentity(expectedIdentity, moved)
|
|
1307
|
+
|| !ownedByCurrentUser(moved)
|
|
1308
|
+
|| (moved.mode & 0o077) !== 0
|
|
1309
|
+
) {
|
|
1310
|
+
try {
|
|
1311
|
+
linkSync(quarantine, path);
|
|
1312
|
+
unlinkSync(quarantine);
|
|
1313
|
+
syncDirectory(parent);
|
|
1314
|
+
} catch (error) {
|
|
1315
|
+
throw new Error(
|
|
1316
|
+
`${changedMessage} and could not be restored`,
|
|
1317
|
+
{ cause: error },
|
|
1318
|
+
);
|
|
1319
|
+
}
|
|
1320
|
+
throw new Error(changedMessage);
|
|
1321
|
+
}
|
|
1322
|
+
unlinkSync(quarantine);
|
|
1323
|
+
syncDirectory(parent);
|
|
1324
|
+
return true;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function removeExactStaleLock<T>(
|
|
1328
|
+
path: string,
|
|
1329
|
+
snapshot: PrivateRecordSnapshot<T>,
|
|
1330
|
+
label: string,
|
|
1331
|
+
): boolean {
|
|
1332
|
+
return removeExactPrivateFile(path, snapshot.identity, label);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function writeAtomicPrivateRecord(path: string, value: unknown): void {
|
|
1336
|
+
const parent = dirname(path);
|
|
1337
|
+
const temporary = join(
|
|
1338
|
+
parent,
|
|
1339
|
+
`.${basename(path)}.stage-${process.pid}-${randomUUID()}`,
|
|
1340
|
+
);
|
|
1341
|
+
try {
|
|
1342
|
+
writePrivateFile(
|
|
1343
|
+
temporary,
|
|
1344
|
+
Buffer.from(`${canonicalJson(value)}\n`, "utf8"),
|
|
1345
|
+
);
|
|
1346
|
+
renameSync(temporary, path);
|
|
1347
|
+
syncDirectory(parent);
|
|
1348
|
+
} finally {
|
|
1349
|
+
rmSync(temporary, { force: true });
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
function writeImmutablePrivateRecord<T>(
|
|
1354
|
+
path: string,
|
|
1355
|
+
value: T,
|
|
1356
|
+
label: string,
|
|
1357
|
+
parse: (candidate: unknown) => T,
|
|
1358
|
+
): T {
|
|
1359
|
+
const parent = dirname(path);
|
|
1360
|
+
const temporary = join(
|
|
1361
|
+
parent,
|
|
1362
|
+
`.${basename(path)}.stage-${process.pid}-${randomUUID()}`,
|
|
1363
|
+
);
|
|
1364
|
+
try {
|
|
1365
|
+
writePrivateFile(
|
|
1366
|
+
temporary,
|
|
1367
|
+
Buffer.from(`${canonicalJson(value)}\n`, "utf8"),
|
|
1368
|
+
);
|
|
1369
|
+
try {
|
|
1370
|
+
linkSync(temporary, path);
|
|
1371
|
+
syncDirectory(parent);
|
|
1372
|
+
return value;
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
if (!hasCode(error, "EEXIST")) throw error;
|
|
1375
|
+
const existing = readPrivateRecordIfPresent(path, label, parse);
|
|
1376
|
+
if (existing === null || canonicalJson(existing) !== canonicalJson(value)) {
|
|
1377
|
+
throw new Error(`${label} already exists with different content`);
|
|
1378
|
+
}
|
|
1379
|
+
return existing;
|
|
1380
|
+
}
|
|
1381
|
+
} finally {
|
|
1382
|
+
rmSync(temporary, { force: true });
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function ensureStore(root: string): PortableProviderPluginStorePaths {
|
|
1387
|
+
const paths = portableProviderPluginStorePaths(root);
|
|
1388
|
+
ensurePrivateDirectory(paths.root);
|
|
1389
|
+
ensurePrivateDirectory(paths.artifacts);
|
|
1390
|
+
ensurePrivateDirectory(paths.trust);
|
|
1391
|
+
ensurePrivateDirectory(paths.active);
|
|
1392
|
+
ensurePrivateDirectory(paths.locks);
|
|
1393
|
+
return paths;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function openStoreIfPresent(
|
|
1397
|
+
root: string,
|
|
1398
|
+
): PortableProviderPluginStorePaths | null {
|
|
1399
|
+
const paths = portableProviderPluginStorePaths(root);
|
|
1400
|
+
if (!existsSync(paths.root)) return null;
|
|
1401
|
+
ensurePrivateDirectory(paths.root);
|
|
1402
|
+
for (const directory of [
|
|
1403
|
+
paths.artifacts,
|
|
1404
|
+
paths.trust,
|
|
1405
|
+
paths.active,
|
|
1406
|
+
paths.locks,
|
|
1407
|
+
]) {
|
|
1408
|
+
if (!existsSync(directory)) {
|
|
1409
|
+
throw new Error("portable plugin store is missing a required directory");
|
|
1410
|
+
}
|
|
1411
|
+
ensurePrivateDirectory(directory);
|
|
1412
|
+
}
|
|
1413
|
+
return paths;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function writeVerifiedArtifact(
|
|
1417
|
+
artifactsRoot: string,
|
|
1418
|
+
source: VerifiedPortableProviderPluginPackage,
|
|
1419
|
+
): string {
|
|
1420
|
+
const destination = join(artifactsRoot, source.bundleSha256);
|
|
1421
|
+
if (existsSync(destination)) {
|
|
1422
|
+
const existing = verifyPortableProviderPluginPackageDirectory(destination);
|
|
1423
|
+
if (existing.bundleSha256 !== source.bundleSha256) {
|
|
1424
|
+
throw new Error("installed portable plugin artifact digest is corrupt");
|
|
1425
|
+
}
|
|
1426
|
+
return destination;
|
|
1427
|
+
}
|
|
1428
|
+
const stage = join(
|
|
1429
|
+
artifactsRoot,
|
|
1430
|
+
`.stage-${source.bundleSha256}-${process.pid}-${randomUUID()}`,
|
|
1431
|
+
);
|
|
1432
|
+
mkdirSync(stage, { mode: 0o700 });
|
|
1433
|
+
chmodSync(stage, 0o700);
|
|
1434
|
+
try {
|
|
1435
|
+
for (const file of source.files) {
|
|
1436
|
+
const target = join(stage, ...file.path.split("/"));
|
|
1437
|
+
ensurePrivateDirectory(dirname(target));
|
|
1438
|
+
writePrivateFile(target, file.bytes);
|
|
1439
|
+
}
|
|
1440
|
+
writePrivateFile(
|
|
1441
|
+
join(stage, PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME),
|
|
1442
|
+
source.manifestBytes,
|
|
1443
|
+
);
|
|
1444
|
+
const staged = verifyPortableProviderPluginPackageDirectory(stage);
|
|
1445
|
+
if (staged.bundleSha256 !== source.bundleSha256) {
|
|
1446
|
+
throw new Error("staged portable plugin artifact digest changed");
|
|
1447
|
+
}
|
|
1448
|
+
try {
|
|
1449
|
+
renameSync(stage, destination);
|
|
1450
|
+
syncDirectory(artifactsRoot);
|
|
1451
|
+
} catch (error) {
|
|
1452
|
+
if (!hasCode(error, "EEXIST") && !hasCode(error, "ENOTEMPTY")) {
|
|
1453
|
+
throw error;
|
|
1454
|
+
}
|
|
1455
|
+
const existing = verifyPortableProviderPluginPackageDirectory(destination);
|
|
1456
|
+
if (existing.bundleSha256 !== source.bundleSha256) {
|
|
1457
|
+
throw new Error("concurrent portable plugin artifact is corrupt");
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
return destination;
|
|
1461
|
+
} finally {
|
|
1462
|
+
rmSync(stage, { recursive: true, force: true });
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
function acquireOwnedLock<T extends ProcessOwnerIdentity>(
|
|
1467
|
+
path: string,
|
|
1468
|
+
value: T,
|
|
1469
|
+
options: {
|
|
1470
|
+
readonly label: string;
|
|
1471
|
+
readonly parse: (candidate: unknown) => T;
|
|
1472
|
+
readonly assertExisting?: (existing: T) => void;
|
|
1473
|
+
readonly liveOwnerMessage: string;
|
|
1474
|
+
readonly unknownOwnerMessage: string;
|
|
1475
|
+
readonly acquisitionFailureMessage: string;
|
|
1476
|
+
readonly changedBeforeReleaseMessage: string;
|
|
1477
|
+
},
|
|
1478
|
+
): () => void {
|
|
1479
|
+
const lockPath = physicalPathThroughExistingParent(path);
|
|
1480
|
+
let descriptor: number | null = null;
|
|
1481
|
+
let owned: ReturnType<typeof fstatSync> | null = null;
|
|
1482
|
+
if (existsSync(lockPath)) pauseBeforeLockClaimForTest(lockPath);
|
|
1483
|
+
|
|
1484
|
+
acquire: for (let claimAttempt = 0; claimAttempt < 64; claimAttempt += 1) {
|
|
1485
|
+
const releaseClaim = acquireLockClaim(lockPath);
|
|
1486
|
+
if (releaseClaim === null) {
|
|
1487
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
|
|
1488
|
+
continue;
|
|
1489
|
+
}
|
|
1490
|
+
try {
|
|
1491
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
1492
|
+
const publication = publishPrivateLockFileExclusive(
|
|
1493
|
+
lockPath,
|
|
1494
|
+
Buffer.from(`${canonicalJson(value)}\n`, "utf8"),
|
|
1495
|
+
value,
|
|
1496
|
+
"lock",
|
|
1497
|
+
lockPath,
|
|
1498
|
+
);
|
|
1499
|
+
if (publication !== null) {
|
|
1500
|
+
descriptor = publication.descriptor;
|
|
1501
|
+
owned = publication.identity;
|
|
1502
|
+
break acquire;
|
|
1503
|
+
}
|
|
1504
|
+
const existing = readPrivateRecordSnapshotIfPresent(
|
|
1505
|
+
lockPath,
|
|
1506
|
+
options.label,
|
|
1507
|
+
options.parse,
|
|
1508
|
+
);
|
|
1509
|
+
if (existing === null) continue;
|
|
1510
|
+
options.assertExisting?.(existing.value);
|
|
1511
|
+
const status = processOwnerStatus(existing.value);
|
|
1512
|
+
if (status === "exact-live-owner") {
|
|
1513
|
+
throw new Error(options.liveOwnerMessage);
|
|
1514
|
+
}
|
|
1515
|
+
if (status === "unknown") {
|
|
1516
|
+
throw new Error(options.unknownOwnerMessage);
|
|
1517
|
+
}
|
|
1518
|
+
removeExactStaleLock(lockPath, existing, options.label);
|
|
1519
|
+
}
|
|
1520
|
+
} finally {
|
|
1521
|
+
releaseClaim();
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
if (descriptor === null || owned === null) {
|
|
1525
|
+
throw new Error(options.acquisitionFailureMessage);
|
|
1526
|
+
}
|
|
1527
|
+
const ownedDescriptor = descriptor;
|
|
1528
|
+
const ownedIdentity = owned;
|
|
1529
|
+
return () => {
|
|
1530
|
+
try {
|
|
1531
|
+
if (!removeExactPrivateFile(
|
|
1532
|
+
lockPath,
|
|
1533
|
+
ownedIdentity,
|
|
1534
|
+
options.label,
|
|
1535
|
+
options.changedBeforeReleaseMessage,
|
|
1536
|
+
)) {
|
|
1537
|
+
throw new Error(options.changedBeforeReleaseMessage);
|
|
1538
|
+
}
|
|
1539
|
+
} finally {
|
|
1540
|
+
closeSync(ownedDescriptor);
|
|
1541
|
+
}
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
function acquirePluginLock(
|
|
1546
|
+
path: string,
|
|
1547
|
+
id: string,
|
|
1548
|
+
now: Date,
|
|
1549
|
+
): () => void {
|
|
1550
|
+
const processIdentity = currentProcessStartIdentity();
|
|
1551
|
+
const lock: PortableProviderPluginLockRecordV1 = Object.freeze({
|
|
1552
|
+
schemaVersion: PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION,
|
|
1553
|
+
pluginId: id,
|
|
1554
|
+
acquiredAt: canonicalNow(now),
|
|
1555
|
+
pid: process.pid,
|
|
1556
|
+
...processIdentity,
|
|
1557
|
+
});
|
|
1558
|
+
return acquireOwnedLock(path, lock, {
|
|
1559
|
+
label: "portable plugin activation lock",
|
|
1560
|
+
parse: parseLockRecord,
|
|
1561
|
+
assertExisting: (existing) => {
|
|
1562
|
+
if (existing.pluginId !== id) {
|
|
1563
|
+
throw new Error(
|
|
1564
|
+
"portable plugin activation lock does not match its filename",
|
|
1565
|
+
);
|
|
1566
|
+
}
|
|
1567
|
+
},
|
|
1568
|
+
liveOwnerMessage: "portable plugin activation is already locked",
|
|
1569
|
+
unknownOwnerMessage:
|
|
1570
|
+
"portable plugin activation lock owner cannot be inspected safely",
|
|
1571
|
+
acquisitionFailureMessage:
|
|
1572
|
+
"portable plugin activation lock could not be acquired",
|
|
1573
|
+
changedBeforeReleaseMessage:
|
|
1574
|
+
"portable plugin activation lock changed before release",
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
function acquireCatalogMutationLock(
|
|
1579
|
+
paths: PortableProviderPluginStorePaths,
|
|
1580
|
+
now: Date,
|
|
1581
|
+
): () => void {
|
|
1582
|
+
const processIdentity = currentProcessStartIdentity();
|
|
1583
|
+
const lock: PortableProviderPluginCatalogLockRecordV1 = Object.freeze({
|
|
1584
|
+
schemaVersion: PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION,
|
|
1585
|
+
scope: "catalog-mutation",
|
|
1586
|
+
acquiredAt: canonicalNow(now),
|
|
1587
|
+
pid: process.pid,
|
|
1588
|
+
...processIdentity,
|
|
1589
|
+
});
|
|
1590
|
+
return acquireOwnedLock(
|
|
1591
|
+
join(paths.locks, CATALOG_MUTATION_LOCK_NAME),
|
|
1592
|
+
lock,
|
|
1593
|
+
{
|
|
1594
|
+
label: "portable plugin catalog mutation lock",
|
|
1595
|
+
parse: parseCatalogLockRecord,
|
|
1596
|
+
liveOwnerMessage:
|
|
1597
|
+
"portable plugin catalog mutation is already locked",
|
|
1598
|
+
unknownOwnerMessage:
|
|
1599
|
+
"portable plugin catalog mutation lock owner cannot be inspected safely",
|
|
1600
|
+
acquisitionFailureMessage:
|
|
1601
|
+
"portable plugin catalog mutation lock could not be acquired",
|
|
1602
|
+
changedBeforeReleaseMessage:
|
|
1603
|
+
"portable plugin catalog mutation lock changed before release",
|
|
1604
|
+
},
|
|
1605
|
+
);
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
function runSynchronousCatalogOperation<T>(operation: () => T): T {
|
|
1609
|
+
const result = operation();
|
|
1610
|
+
if (result instanceof Promise) {
|
|
1611
|
+
throw new Error(
|
|
1612
|
+
"portable plugin catalog lock callback must complete synchronously",
|
|
1613
|
+
);
|
|
1614
|
+
}
|
|
1615
|
+
return result;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
function withCatalogMutationLock<T>(
|
|
1619
|
+
paths: PortableProviderPluginStorePaths,
|
|
1620
|
+
now: Date,
|
|
1621
|
+
operation: () => T,
|
|
1622
|
+
): T {
|
|
1623
|
+
const lockPath = join(paths.locks, CATALOG_MUTATION_LOCK_NAME);
|
|
1624
|
+
if (activeCatalogMutationLocks.has(lockPath)) {
|
|
1625
|
+
return runSynchronousCatalogOperation(operation);
|
|
1626
|
+
}
|
|
1627
|
+
const release = acquireCatalogMutationLock(paths, now);
|
|
1628
|
+
activeCatalogMutationLocks.add(lockPath);
|
|
1629
|
+
try {
|
|
1630
|
+
return runSynchronousCatalogOperation(operation);
|
|
1631
|
+
} finally {
|
|
1632
|
+
activeCatalogMutationLocks.delete(lockPath);
|
|
1633
|
+
release();
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
/**
|
|
1638
|
+
* Serialize a synchronous kernel transition against every portable-plugin
|
|
1639
|
+
* activation mutation. The callback receives no auth or session material.
|
|
1640
|
+
*/
|
|
1641
|
+
export function withPortableProviderPluginCatalogLock<T>(
|
|
1642
|
+
storeRoot: string,
|
|
1643
|
+
now: Date,
|
|
1644
|
+
operation: () => T,
|
|
1645
|
+
): T {
|
|
1646
|
+
return withCatalogMutationLock(ensureStore(storeRoot), now, operation);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
function withPluginMutationLocks<T>(
|
|
1650
|
+
paths: PortableProviderPluginStorePaths,
|
|
1651
|
+
id: string,
|
|
1652
|
+
now: Date,
|
|
1653
|
+
operation: () => T,
|
|
1654
|
+
): T {
|
|
1655
|
+
return withCatalogMutationLock(paths, now, () => {
|
|
1656
|
+
if (activePluginMutations.has(paths.root)) {
|
|
1657
|
+
throw new Error("portable plugin catalog mutation cannot be nested");
|
|
1658
|
+
}
|
|
1659
|
+
activePluginMutations.add(paths.root);
|
|
1660
|
+
try {
|
|
1661
|
+
const releasePlugin = acquirePluginLock(
|
|
1662
|
+
join(paths.locks, `${id}.lock`),
|
|
1663
|
+
id,
|
|
1664
|
+
now,
|
|
1665
|
+
);
|
|
1666
|
+
try {
|
|
1667
|
+
return operation();
|
|
1668
|
+
} finally {
|
|
1669
|
+
releasePlugin();
|
|
1670
|
+
}
|
|
1671
|
+
} finally {
|
|
1672
|
+
activePluginMutations.delete(paths.root);
|
|
1673
|
+
}
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
export function assertPortableProviderPluginTrustApprovalMatches(
|
|
1678
|
+
approval: PortableProviderPluginTrustApprovalV1,
|
|
1679
|
+
source: VerifiedPortableProviderPluginPackage,
|
|
1680
|
+
): void {
|
|
1681
|
+
if (
|
|
1682
|
+
approval.pluginId !== source.manifest.id
|
|
1683
|
+
|| approval.pluginVersion !== source.manifest.version
|
|
1684
|
+
|| approval.bundleSha256 !== source.bundleSha256
|
|
1685
|
+
) {
|
|
1686
|
+
throw new Error(
|
|
1687
|
+
"portable plugin trust approval does not match the verified package identity",
|
|
1688
|
+
);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
function assertExpectedActive(
|
|
1693
|
+
current: PortableProviderPluginActiveRecordV1 | null,
|
|
1694
|
+
expectedBundleSha256: string | null,
|
|
1695
|
+
): void {
|
|
1696
|
+
if (expectedBundleSha256 === null) {
|
|
1697
|
+
if (current !== null) {
|
|
1698
|
+
throw new Error(
|
|
1699
|
+
"portable plugin activation changed: an installed version already exists",
|
|
1700
|
+
);
|
|
1701
|
+
}
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
const expected = sha256(
|
|
1705
|
+
expectedBundleSha256,
|
|
1706
|
+
"expected active plugin bundle digest",
|
|
1707
|
+
);
|
|
1708
|
+
if (current?.bundleSha256 !== expected) {
|
|
1709
|
+
throw new Error(
|
|
1710
|
+
"portable plugin activation changed from its expected bundle digest",
|
|
1711
|
+
);
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
function canonicalNow(now: Date): string {
|
|
1716
|
+
const value = now.toISOString();
|
|
1717
|
+
return timestamp(value, "portable plugin store time");
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
function runAssertActivatable(
|
|
1721
|
+
hook: PortableProviderPluginAssertActivatable,
|
|
1722
|
+
verifiedPackage: VerifiedPortableProviderPluginPackage,
|
|
1723
|
+
): void {
|
|
1724
|
+
const result: unknown = hook(verifiedPackage);
|
|
1725
|
+
if (result !== undefined) {
|
|
1726
|
+
throw new Error(
|
|
1727
|
+
"portable plugin activatability hook must complete synchronously without returning a value",
|
|
1728
|
+
);
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
function runAssertQuiescent(
|
|
1733
|
+
hook: PortableProviderPluginAssertQuiescent,
|
|
1734
|
+
bundleSha256: string,
|
|
1735
|
+
artifactPath: string,
|
|
1736
|
+
): void {
|
|
1737
|
+
const result: unknown = hook(bundleSha256, artifactPath);
|
|
1738
|
+
if (result !== undefined) {
|
|
1739
|
+
throw new Error(
|
|
1740
|
+
"portable plugin quiescence hook must complete synchronously without returning a value",
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
function trustMatchesPackage(
|
|
1746
|
+
trust: PortableProviderPluginTrustRecordV1,
|
|
1747
|
+
packageValue: VerifiedPortableProviderPluginPackage,
|
|
1748
|
+
): boolean {
|
|
1749
|
+
return (
|
|
1750
|
+
trust.bundleSha256 === packageValue.bundleSha256
|
|
1751
|
+
&& trust.pluginId === packageValue.manifest.id
|
|
1752
|
+
&& trust.pluginVersion === packageValue.manifest.version
|
|
1753
|
+
&& trust.hostApiVersion === packageValue.manifest.hostApiVersion
|
|
1754
|
+
&& trust.manifestSha256 === packageValue.manifestSha256
|
|
1755
|
+
&& canonicalJson(trust.provenance)
|
|
1756
|
+
=== canonicalJson(packageValue.manifest.provenance)
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
export function installPortableProviderPluginPackage(
|
|
1761
|
+
sourcePath: string,
|
|
1762
|
+
options: {
|
|
1763
|
+
readonly storeRoot: string;
|
|
1764
|
+
readonly approval: unknown;
|
|
1765
|
+
/**
|
|
1766
|
+
* `null` means the plugin must be absent. An update must name the exact
|
|
1767
|
+
* currently active digest; blind replacement is intentionally unsupported.
|
|
1768
|
+
*/
|
|
1769
|
+
readonly expectedCurrentBundleSha256: string | null;
|
|
1770
|
+
/**
|
|
1771
|
+
* Kernel catalog check. It receives only the verified immutable package
|
|
1772
|
+
* and runs under the catalog-wide mutation lock.
|
|
1773
|
+
*/
|
|
1774
|
+
readonly assertActivatable: PortableProviderPluginAssertActivatable;
|
|
1775
|
+
/**
|
|
1776
|
+
* Kernel lease/journal check for the exact activation being replaced. It is
|
|
1777
|
+
* required for a uniform fail-closed API and invoked only for an update.
|
|
1778
|
+
*/
|
|
1779
|
+
readonly assertCurrentQuiescent: PortableProviderPluginAssertQuiescent;
|
|
1780
|
+
readonly now?: Date;
|
|
1781
|
+
/** Test seam for a deterministic activation race. */
|
|
1782
|
+
readonly beforeActivate?: () => void;
|
|
1783
|
+
},
|
|
1784
|
+
): InstalledPortableProviderPlugin {
|
|
1785
|
+
const source = verifyPortableProviderPluginPackageDirectory(sourcePath);
|
|
1786
|
+
const approval = parsePortableProviderPluginTrustApproval(options.approval);
|
|
1787
|
+
assertPortableProviderPluginTrustApprovalMatches(approval, source);
|
|
1788
|
+
const paths = ensureStore(options.storeRoot);
|
|
1789
|
+
const nowValue = options.now ?? new Date();
|
|
1790
|
+
return withPluginMutationLocks(
|
|
1791
|
+
paths,
|
|
1792
|
+
source.manifest.id,
|
|
1793
|
+
nowValue,
|
|
1794
|
+
() => {
|
|
1795
|
+
const activePath = join(paths.active, `${source.manifest.id}.json`);
|
|
1796
|
+
const current = readPrivateRecordIfPresent(
|
|
1797
|
+
activePath,
|
|
1798
|
+
"portable plugin active record",
|
|
1799
|
+
parseActiveRecord,
|
|
1800
|
+
);
|
|
1801
|
+
assertExpectedActive(current, options.expectedCurrentBundleSha256);
|
|
1802
|
+
const currentInstallation = current === null
|
|
1803
|
+
? null
|
|
1804
|
+
: loadInstalledWithPaths(paths, source.manifest.id);
|
|
1805
|
+
if (current !== null && currentInstallation === null) {
|
|
1806
|
+
throw new Error("current portable plugin installation is incomplete");
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
const artifactPath = writeVerifiedArtifact(paths.artifacts, source);
|
|
1810
|
+
const storedPackage =
|
|
1811
|
+
verifyPortableProviderPluginPackageDirectory(artifactPath);
|
|
1812
|
+
if (storedPackage.bundleSha256 !== source.bundleSha256) {
|
|
1813
|
+
throw new Error("installed portable plugin artifact digest changed");
|
|
1814
|
+
}
|
|
1815
|
+
const now = canonicalNow(nowValue);
|
|
1816
|
+
const trust: PortableProviderPluginTrustRecordV1 = Object.freeze({
|
|
1817
|
+
schemaVersion: PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION,
|
|
1818
|
+
decision: "trust-executable-code",
|
|
1819
|
+
pluginId: source.manifest.id,
|
|
1820
|
+
pluginVersion: source.manifest.version,
|
|
1821
|
+
hostApiVersion: source.manifest.hostApiVersion,
|
|
1822
|
+
bundleSha256: source.bundleSha256,
|
|
1823
|
+
manifestSha256: source.manifestSha256,
|
|
1824
|
+
provenance: source.manifest.provenance,
|
|
1825
|
+
trustedAt: now,
|
|
1826
|
+
});
|
|
1827
|
+
const trustPath = join(paths.trust, `${source.bundleSha256}.json`);
|
|
1828
|
+
const existingTrust = readPrivateRecordIfPresent(
|
|
1829
|
+
trustPath,
|
|
1830
|
+
"portable plugin trust record",
|
|
1831
|
+
parseTrustRecord,
|
|
1832
|
+
);
|
|
1833
|
+
if (
|
|
1834
|
+
existingTrust !== null
|
|
1835
|
+
&& !trustMatchesPackage(existingTrust, source)
|
|
1836
|
+
) {
|
|
1837
|
+
throw new Error(
|
|
1838
|
+
"portable plugin trust record does not match its content-addressed package",
|
|
1839
|
+
);
|
|
1840
|
+
}
|
|
1841
|
+
const storedTrust = existingTrust ?? writeImmutablePrivateRecord(
|
|
1842
|
+
trustPath,
|
|
1843
|
+
trust,
|
|
1844
|
+
"portable plugin trust record",
|
|
1845
|
+
parseTrustRecord,
|
|
1846
|
+
);
|
|
1847
|
+
const active: PortableProviderPluginActiveRecordV1 = Object.freeze({
|
|
1848
|
+
schemaVersion: PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION,
|
|
1849
|
+
pluginId: source.manifest.id,
|
|
1850
|
+
pluginVersion: source.manifest.version,
|
|
1851
|
+
bundleSha256: source.bundleSha256,
|
|
1852
|
+
activatedAt: now,
|
|
1853
|
+
status: "enabled",
|
|
1854
|
+
});
|
|
1855
|
+
options.beforeActivate?.();
|
|
1856
|
+
const currentAfterWork = readPrivateRecordIfPresent(
|
|
1857
|
+
activePath,
|
|
1858
|
+
"portable plugin active record",
|
|
1859
|
+
parseActiveRecord,
|
|
1860
|
+
);
|
|
1861
|
+
if (canonicalJson(currentAfterWork) !== canonicalJson(current)) {
|
|
1862
|
+
throw new Error(
|
|
1863
|
+
"portable plugin activation changed during installation",
|
|
1864
|
+
);
|
|
1865
|
+
}
|
|
1866
|
+
runAssertActivatable(options.assertActivatable, storedPackage);
|
|
1867
|
+
const currentAfterHook = readPrivateRecordIfPresent(
|
|
1868
|
+
activePath,
|
|
1869
|
+
"portable plugin active record",
|
|
1870
|
+
parseActiveRecord,
|
|
1871
|
+
);
|
|
1872
|
+
if (canonicalJson(currentAfterHook) !== canonicalJson(current)) {
|
|
1873
|
+
throw new Error(
|
|
1874
|
+
"portable plugin activation changed during activatability check",
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1877
|
+
if (currentInstallation !== null) {
|
|
1878
|
+
runAssertQuiescent(
|
|
1879
|
+
options.assertCurrentQuiescent,
|
|
1880
|
+
currentInstallation.package.bundleSha256,
|
|
1881
|
+
currentInstallation.artifactPath,
|
|
1882
|
+
);
|
|
1883
|
+
}
|
|
1884
|
+
const currentAfterQuiescence = readPrivateRecordIfPresent(
|
|
1885
|
+
activePath,
|
|
1886
|
+
"portable plugin active record",
|
|
1887
|
+
parseActiveRecord,
|
|
1888
|
+
);
|
|
1889
|
+
if (
|
|
1890
|
+
canonicalJson(currentAfterQuiescence) !== canonicalJson(current)
|
|
1891
|
+
) {
|
|
1892
|
+
throw new Error(
|
|
1893
|
+
"portable plugin activation changed during current-bundle quiescence check",
|
|
1894
|
+
);
|
|
1895
|
+
}
|
|
1896
|
+
if (currentInstallation !== null) {
|
|
1897
|
+
const installationAfterQuiescence = loadInstalledWithPaths(
|
|
1898
|
+
paths,
|
|
1899
|
+
source.manifest.id,
|
|
1900
|
+
);
|
|
1901
|
+
if (
|
|
1902
|
+
installationAfterQuiescence === null
|
|
1903
|
+
|| installationAfterQuiescence.package.bundleSha256
|
|
1904
|
+
!== currentInstallation.package.bundleSha256
|
|
1905
|
+
|| installationAfterQuiescence.artifactPath
|
|
1906
|
+
!== currentInstallation.artifactPath
|
|
1907
|
+
) {
|
|
1908
|
+
throw new Error(
|
|
1909
|
+
"current portable plugin artifact changed during quiescence check",
|
|
1910
|
+
);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
const packageAfterHook =
|
|
1914
|
+
verifyPortableProviderPluginPackageDirectory(artifactPath);
|
|
1915
|
+
if (packageAfterHook.bundleSha256 !== source.bundleSha256) {
|
|
1916
|
+
throw new Error(
|
|
1917
|
+
"portable plugin artifact changed during activatability check",
|
|
1918
|
+
);
|
|
1919
|
+
}
|
|
1920
|
+
writeAtomicPrivateRecord(activePath, active);
|
|
1921
|
+
return Object.freeze({
|
|
1922
|
+
artifactPath,
|
|
1923
|
+
package: packageAfterHook,
|
|
1924
|
+
trust: storedTrust,
|
|
1925
|
+
active,
|
|
1926
|
+
});
|
|
1927
|
+
},
|
|
1928
|
+
);
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
export function disablePortableProviderPluginPackage(
|
|
1932
|
+
storeRoot: string,
|
|
1933
|
+
id: string,
|
|
1934
|
+
options: {
|
|
1935
|
+
readonly expectedBundleSha256: string;
|
|
1936
|
+
/**
|
|
1937
|
+
* Kernel lease/journal check. It receives only immutable artifact identity
|
|
1938
|
+
* and runs under the catalog-wide mutation lock.
|
|
1939
|
+
*/
|
|
1940
|
+
readonly assertQuiescent: PortableProviderPluginAssertQuiescent;
|
|
1941
|
+
readonly now?: Date;
|
|
1942
|
+
},
|
|
1943
|
+
): InstalledPortableProviderPlugin {
|
|
1944
|
+
const paths = openStoreIfPresent(storeRoot);
|
|
1945
|
+
if (paths === null) {
|
|
1946
|
+
throw new Error("portable plugin is not installed");
|
|
1947
|
+
}
|
|
1948
|
+
const safeId = pluginId(id, "installed plugin ID");
|
|
1949
|
+
const nowValue = options.now ?? new Date();
|
|
1950
|
+
return withPluginMutationLocks(
|
|
1951
|
+
paths,
|
|
1952
|
+
safeId,
|
|
1953
|
+
nowValue,
|
|
1954
|
+
() => {
|
|
1955
|
+
const activePath = join(paths.active, `${safeId}.json`);
|
|
1956
|
+
const current = readPrivateRecordIfPresent(
|
|
1957
|
+
activePath,
|
|
1958
|
+
"portable plugin active record",
|
|
1959
|
+
parseActiveRecord,
|
|
1960
|
+
);
|
|
1961
|
+
if (current === null) throw new Error("portable plugin is not installed");
|
|
1962
|
+
assertExpectedActive(current, options.expectedBundleSha256);
|
|
1963
|
+
const installed = loadInstalledWithPaths(paths, safeId);
|
|
1964
|
+
if (installed === null) throw new Error("portable plugin is not installed");
|
|
1965
|
+
if (current.status === "disabled") return installed;
|
|
1966
|
+
const disabled: PortableProviderPluginActiveRecordV1 = Object.freeze({
|
|
1967
|
+
...current,
|
|
1968
|
+
status: "disabled",
|
|
1969
|
+
disabledAt: canonicalNow(nowValue),
|
|
1970
|
+
});
|
|
1971
|
+
runAssertQuiescent(
|
|
1972
|
+
options.assertQuiescent,
|
|
1973
|
+
installed.package.bundleSha256,
|
|
1974
|
+
installed.artifactPath,
|
|
1975
|
+
);
|
|
1976
|
+
const installedAfterHook = loadInstalledWithPaths(paths, safeId);
|
|
1977
|
+
if (
|
|
1978
|
+
installedAfterHook === null
|
|
1979
|
+
|| canonicalJson(installedAfterHook.active) !== canonicalJson(current)
|
|
1980
|
+
|| installedAfterHook.package.bundleSha256
|
|
1981
|
+
!== installed.package.bundleSha256
|
|
1982
|
+
|| installedAfterHook.artifactPath !== installed.artifactPath
|
|
1983
|
+
) {
|
|
1984
|
+
throw new Error(
|
|
1985
|
+
"portable plugin installation changed during quiescence check",
|
|
1986
|
+
);
|
|
1987
|
+
}
|
|
1988
|
+
writeAtomicPrivateRecord(activePath, disabled);
|
|
1989
|
+
return Object.freeze({ ...installedAfterHook, active: disabled });
|
|
1990
|
+
},
|
|
1991
|
+
);
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
export function removePortableProviderPluginPackage(
|
|
1995
|
+
storeRoot: string,
|
|
1996
|
+
id: string,
|
|
1997
|
+
options: {
|
|
1998
|
+
readonly expectedBundleSha256: string;
|
|
1999
|
+
/**
|
|
2000
|
+
* Kernel lease/journal check. It receives only immutable artifact identity
|
|
2001
|
+
* and runs under the catalog-wide mutation lock.
|
|
2002
|
+
*/
|
|
2003
|
+
readonly assertQuiescent: PortableProviderPluginAssertQuiescent;
|
|
2004
|
+
readonly now?: Date;
|
|
2005
|
+
},
|
|
2006
|
+
): InstalledPortableProviderPlugin {
|
|
2007
|
+
const paths = openStoreIfPresent(storeRoot);
|
|
2008
|
+
if (paths === null) {
|
|
2009
|
+
throw new Error("portable plugin is not installed");
|
|
2010
|
+
}
|
|
2011
|
+
const safeId = pluginId(id, "installed plugin ID");
|
|
2012
|
+
const nowValue = options.now ?? new Date();
|
|
2013
|
+
return withPluginMutationLocks(
|
|
2014
|
+
paths,
|
|
2015
|
+
safeId,
|
|
2016
|
+
nowValue,
|
|
2017
|
+
() => {
|
|
2018
|
+
const activePath = join(paths.active, `${safeId}.json`);
|
|
2019
|
+
const current = readPrivateRecordIfPresent(
|
|
2020
|
+
activePath,
|
|
2021
|
+
"portable plugin active record",
|
|
2022
|
+
parseActiveRecord,
|
|
2023
|
+
);
|
|
2024
|
+
if (current === null) throw new Error("portable plugin is not installed");
|
|
2025
|
+
assertExpectedActive(current, options.expectedBundleSha256);
|
|
2026
|
+
const installed = loadInstalledWithPaths(paths, safeId);
|
|
2027
|
+
if (installed === null) throw new Error("portable plugin is not installed");
|
|
2028
|
+
runAssertQuiescent(
|
|
2029
|
+
options.assertQuiescent,
|
|
2030
|
+
installed.package.bundleSha256,
|
|
2031
|
+
installed.artifactPath,
|
|
2032
|
+
);
|
|
2033
|
+
const installedAfterHook = loadInstalledWithPaths(paths, safeId);
|
|
2034
|
+
if (
|
|
2035
|
+
installedAfterHook === null
|
|
2036
|
+
|| canonicalJson(installedAfterHook.active) !== canonicalJson(current)
|
|
2037
|
+
|| installedAfterHook.package.bundleSha256
|
|
2038
|
+
!== installed.package.bundleSha256
|
|
2039
|
+
|| installedAfterHook.artifactPath !== installed.artifactPath
|
|
2040
|
+
) {
|
|
2041
|
+
throw new Error(
|
|
2042
|
+
"portable plugin installation changed during quiescence check",
|
|
2043
|
+
);
|
|
2044
|
+
}
|
|
2045
|
+
unlinkSync(activePath);
|
|
2046
|
+
syncDirectory(paths.active);
|
|
2047
|
+
return installedAfterHook;
|
|
2048
|
+
},
|
|
2049
|
+
);
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
function loadInstalledWithPaths(
|
|
2053
|
+
paths: PortableProviderPluginStorePaths,
|
|
2054
|
+
id: string,
|
|
2055
|
+
): InstalledPortableProviderPlugin | null {
|
|
2056
|
+
const safeId = pluginId(id, "installed plugin ID");
|
|
2057
|
+
const active = readPrivateRecordIfPresent(
|
|
2058
|
+
join(paths.active, `${safeId}.json`),
|
|
2059
|
+
"portable plugin active record",
|
|
2060
|
+
parseActiveRecord,
|
|
2061
|
+
);
|
|
2062
|
+
if (active === null) return null;
|
|
2063
|
+
if (active.pluginId !== safeId) {
|
|
2064
|
+
throw new Error("portable plugin active record does not match its filename");
|
|
2065
|
+
}
|
|
2066
|
+
const artifactPath = join(paths.artifacts, active.bundleSha256);
|
|
2067
|
+
const packageValue =
|
|
2068
|
+
verifyPortableProviderPluginPackageDirectory(artifactPath);
|
|
2069
|
+
const trust = readPrivateRecordIfPresent(
|
|
2070
|
+
join(paths.trust, `${active.bundleSha256}.json`),
|
|
2071
|
+
"portable plugin trust record",
|
|
2072
|
+
parseTrustRecord,
|
|
2073
|
+
);
|
|
2074
|
+
if (trust === null) {
|
|
2075
|
+
throw new Error("active portable plugin has no executable-code trust record");
|
|
2076
|
+
}
|
|
2077
|
+
if (
|
|
2078
|
+
packageValue.bundleSha256 !== active.bundleSha256
|
|
2079
|
+
|| packageValue.manifest.id !== active.pluginId
|
|
2080
|
+
|| packageValue.manifest.version !== active.pluginVersion
|
|
2081
|
+
|| !trustMatchesPackage(trust, packageValue)
|
|
2082
|
+
) {
|
|
2083
|
+
throw new Error(
|
|
2084
|
+
"portable plugin artifact, trust, and active records do not share one exact identity",
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
return Object.freeze({
|
|
2088
|
+
artifactPath,
|
|
2089
|
+
package: packageValue,
|
|
2090
|
+
trust,
|
|
2091
|
+
active,
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
export function loadInstalledPortableProviderPlugin(
|
|
2096
|
+
storeRoot: string,
|
|
2097
|
+
id: string,
|
|
2098
|
+
): InstalledPortableProviderPlugin | null {
|
|
2099
|
+
const paths = openStoreIfPresent(storeRoot);
|
|
2100
|
+
if (paths === null) return null;
|
|
2101
|
+
const installed = loadInstalledWithPaths(paths, id);
|
|
2102
|
+
return installed?.active.status === "enabled" ? installed : null;
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
export function loadPortableProviderPluginInstallation(
|
|
2106
|
+
storeRoot: string,
|
|
2107
|
+
id: string,
|
|
2108
|
+
): InstalledPortableProviderPlugin | null {
|
|
2109
|
+
const paths = openStoreIfPresent(storeRoot);
|
|
2110
|
+
return paths === null ? null : loadInstalledWithPaths(paths, id);
|
|
2111
|
+
}
|
|
2112
|
+
|
|
2113
|
+
export function listPortableProviderPluginInstallations(
|
|
2114
|
+
storeRoot: string,
|
|
2115
|
+
): readonly InstalledPortableProviderPlugin[] {
|
|
2116
|
+
const paths = openStoreIfPresent(storeRoot);
|
|
2117
|
+
if (paths === null) return Object.freeze([]);
|
|
2118
|
+
const ids: string[] = [];
|
|
2119
|
+
for (const entry of readdirSync(paths.active, { withFileTypes: true })) {
|
|
2120
|
+
if (
|
|
2121
|
+
!entry.isFile()
|
|
2122
|
+
|| entry.isSymbolicLink()
|
|
2123
|
+
|| !entry.name.endsWith(".json")
|
|
2124
|
+
) {
|
|
2125
|
+
throw new Error(
|
|
2126
|
+
`portable plugin active store contains unexpected entry ${entry.name}`,
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
ids.push(
|
|
2130
|
+
pluginId(
|
|
2131
|
+
entry.name.slice(0, -".json".length),
|
|
2132
|
+
"installed plugin filename",
|
|
2133
|
+
),
|
|
2134
|
+
);
|
|
2135
|
+
}
|
|
2136
|
+
ids.sort();
|
|
2137
|
+
return Object.freeze(
|
|
2138
|
+
ids.map((id) => {
|
|
2139
|
+
const installed = loadInstalledWithPaths(paths, id);
|
|
2140
|
+
if (installed === null) {
|
|
2141
|
+
throw new Error("portable plugin disappeared while listing the store");
|
|
2142
|
+
}
|
|
2143
|
+
return installed;
|
|
2144
|
+
}),
|
|
2145
|
+
);
|
|
2146
|
+
}
|
|
2147
|
+
|
|
2148
|
+
export function listInstalledPortableProviderPlugins(
|
|
2149
|
+
storeRoot: string,
|
|
2150
|
+
): readonly InstalledPortableProviderPlugin[] {
|
|
2151
|
+
return Object.freeze(
|
|
2152
|
+
listPortableProviderPluginInstallations(storeRoot)
|
|
2153
|
+
.filter((installed) => installed.active.status === "enabled"),
|
|
2154
|
+
);
|
|
2155
|
+
}
|