@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,1491 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
closeSync,
|
|
4
|
+
constants,
|
|
5
|
+
existsSync,
|
|
6
|
+
fstatSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
openSync,
|
|
9
|
+
opendirSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
readlinkSync,
|
|
12
|
+
renameSync,
|
|
13
|
+
symlinkSync,
|
|
14
|
+
unlinkSync,
|
|
15
|
+
writeFileSync,
|
|
16
|
+
type BigIntStats,
|
|
17
|
+
} from "node:fs";
|
|
18
|
+
import { basename, join, resolve } from "node:path";
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
captureProcessOwnerIdentity,
|
|
22
|
+
processOwnerStatus,
|
|
23
|
+
type ProcessOwnerIdentity,
|
|
24
|
+
} from "../process-identity";
|
|
25
|
+
|
|
26
|
+
const maximumDirectoryEntries = 10_000;
|
|
27
|
+
const maximumInstallStages = 64;
|
|
28
|
+
const maximumInstallStageBytes = 64 * 1024;
|
|
29
|
+
const maximumMutationClaims = 64;
|
|
30
|
+
const TEST_BARRIER_TIMEOUT_MS = 90_000;
|
|
31
|
+
const installStageNamePattern = /^\.(wrench|oh)-install\.[A-Za-z0-9]{6}$/u;
|
|
32
|
+
const installLeaseName = ".wrench-install.lease";
|
|
33
|
+
const installLeaseMarker = "wrench-install-lease-v1:";
|
|
34
|
+
const installLeaseTokenPattern = /^[A-Za-z0-9_-]{32}$/u;
|
|
35
|
+
const installMutationClaimPrefix = ".wrench-install.claim-";
|
|
36
|
+
const installMutationClaimMarker = "wrench-install-claim-v1:";
|
|
37
|
+
const installMutationRetiredClaimPrefix = ".wrench-install.retired-claim-";
|
|
38
|
+
const installMutationClaimNamePattern =
|
|
39
|
+
/^\.wrench-install\.claim-(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})$/u;
|
|
40
|
+
const installMutationRetiredClaimNamePattern =
|
|
41
|
+
/^\.wrench-install\.retired-claim-([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})-([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/u;
|
|
42
|
+
const ghostgetManagedMarker =
|
|
43
|
+
"# Generated by the Ghostget local installer; safe to replace.";
|
|
44
|
+
const predecessorManagedMarker =
|
|
45
|
+
"# Generated by skills/oh/scripts/install-local.sh; safe to replace.";
|
|
46
|
+
|
|
47
|
+
type InstallStageSnapshot = {
|
|
48
|
+
readonly bytes: Buffer;
|
|
49
|
+
readonly name: string;
|
|
50
|
+
readonly path: string;
|
|
51
|
+
readonly stats: BigIntStats;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
type ManagedWrapperSnapshot = {
|
|
55
|
+
readonly bytes: Buffer;
|
|
56
|
+
readonly path: string;
|
|
57
|
+
readonly stats: BigIntStats;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type InstallLeaseRecord = {
|
|
61
|
+
readonly schemaVersion: 1;
|
|
62
|
+
readonly owner: ProcessOwnerIdentity;
|
|
63
|
+
readonly token: string;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type InstallLeaseSnapshot = {
|
|
67
|
+
readonly path: string;
|
|
68
|
+
readonly record: InstallLeaseRecord;
|
|
69
|
+
readonly stats: BigIntStats;
|
|
70
|
+
readonly target: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
type InstallMutationClaimPhase = "waiting" | "candidate" | "held";
|
|
74
|
+
|
|
75
|
+
type InstallMutationClaimRecord = {
|
|
76
|
+
readonly schemaVersion: 1;
|
|
77
|
+
readonly kind: "wrench-install-lease-mutation-claim";
|
|
78
|
+
readonly claimId: string;
|
|
79
|
+
readonly owner: ProcessOwnerIdentity;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
type InstallMutationClaimSnapshot = {
|
|
83
|
+
readonly path: string;
|
|
84
|
+
readonly phase: InstallMutationClaimPhase;
|
|
85
|
+
readonly record: InstallMutationClaimRecord;
|
|
86
|
+
readonly stats: BigIntStats;
|
|
87
|
+
readonly target: string;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
type RetiredInstallMutationClaimSnapshot = Omit<
|
|
91
|
+
InstallMutationClaimSnapshot,
|
|
92
|
+
"phase"
|
|
93
|
+
>;
|
|
94
|
+
|
|
95
|
+
type HeldInstallMutationClaim = {
|
|
96
|
+
readonly directory: string;
|
|
97
|
+
readonly rootStats: BigIntStats;
|
|
98
|
+
readonly release: () => void;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
class InstallMutationClaimChangedError extends Error {}
|
|
102
|
+
|
|
103
|
+
export type InstallMutationClaimSnapshotFileSystemForTest = {
|
|
104
|
+
readonly lstat: (path: string) => BigIntStats;
|
|
105
|
+
readonly readlink: (path: string) => string;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const installMutationClaimSnapshotFileSystem:
|
|
109
|
+
InstallMutationClaimSnapshotFileSystemForTest = Object.freeze({
|
|
110
|
+
lstat: (path: string) => lstatSync(path, { bigint: true }),
|
|
111
|
+
readlink: (path: string) => readlinkSync(path, "utf8"),
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
function sameFileIdentity(left: BigIntStats, right: BigIntStats): boolean {
|
|
115
|
+
return left.dev === right.dev
|
|
116
|
+
&& left.ino === right.ino
|
|
117
|
+
&& left.size === right.size
|
|
118
|
+
&& left.mode === right.mode
|
|
119
|
+
&& left.uid === right.uid
|
|
120
|
+
&& left.gid === right.gid
|
|
121
|
+
&& left.nlink === right.nlink
|
|
122
|
+
&& left.mtimeNs === right.mtimeNs
|
|
123
|
+
&& left.ctimeNs === right.ctimeNs;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function sameDirectoryObject(left: BigIntStats, right: BigIntStats): boolean {
|
|
127
|
+
return left.dev === right.dev
|
|
128
|
+
&& left.ino === right.ino
|
|
129
|
+
&& left.mode === right.mode
|
|
130
|
+
&& left.uid === right.uid
|
|
131
|
+
&& left.gid === right.gid;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function ownedByCurrentUser(stats: BigIntStats): boolean {
|
|
135
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
136
|
+
return uid === undefined || stats.uid === BigInt(uid);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function requireStableInstallMutationClaimSymlink(
|
|
140
|
+
path: string,
|
|
141
|
+
before: BigIntStats,
|
|
142
|
+
label: "mutation claim" | "retired mutation claim",
|
|
143
|
+
fileSystem: InstallMutationClaimSnapshotFileSystemForTest,
|
|
144
|
+
): void {
|
|
145
|
+
if (
|
|
146
|
+
before.isSymbolicLink()
|
|
147
|
+
&& ownedByCurrentUser(before)
|
|
148
|
+
&& before.nlink === 1n
|
|
149
|
+
) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// APFS can expose a final transient lstat result while another owner unlinks
|
|
153
|
+
// the retired pathname. Confirm the invalid shape is stable before treating
|
|
154
|
+
// it as hostile state. A stable regular file or non-exact symlink still
|
|
155
|
+
// rejects; a vanished or changed object asks the bounded caller to rescan.
|
|
156
|
+
let after: BigIntStats;
|
|
157
|
+
try {
|
|
158
|
+
after = fileSystem.lstat(path);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (errorCode(error) === "ENOENT") {
|
|
161
|
+
throw new InstallMutationClaimChangedError(
|
|
162
|
+
`local installer ${label} changed during inspection`,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
if (!sameFileIdentity(before, after)) {
|
|
168
|
+
throw new InstallMutationClaimChangedError(
|
|
169
|
+
`local installer ${label} changed during inspection`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
throw new Error(
|
|
173
|
+
`local installer ${label} must be an owned exact symbolic claim`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function errorCode(error: unknown): string | undefined {
|
|
178
|
+
return error instanceof Error && "code" in error
|
|
179
|
+
? String(error.code)
|
|
180
|
+
: undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function expectedMarker(name: string): string {
|
|
184
|
+
return name.startsWith(".wrench-install.")
|
|
185
|
+
? ghostgetManagedMarker
|
|
186
|
+
: predecessorManagedMarker;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function validateRecoveryDirectory(directoryValue: string): {
|
|
190
|
+
readonly directory: string;
|
|
191
|
+
readonly stats: BigIntStats;
|
|
192
|
+
} {
|
|
193
|
+
if (!directoryValue.startsWith("/")) {
|
|
194
|
+
throw new Error("local installer recovery directory must be absolute");
|
|
195
|
+
}
|
|
196
|
+
const directory = resolve(directoryValue);
|
|
197
|
+
if (basename(directory) === "") {
|
|
198
|
+
throw new Error("local installer recovery directory cannot be a filesystem root");
|
|
199
|
+
}
|
|
200
|
+
const stats = lstatSync(directory, { bigint: true });
|
|
201
|
+
if (
|
|
202
|
+
stats.isSymbolicLink()
|
|
203
|
+
|| !stats.isDirectory()
|
|
204
|
+
|| !ownedByCurrentUser(stats)
|
|
205
|
+
) {
|
|
206
|
+
throw new Error("local installer recovery directory must be an owned real directory");
|
|
207
|
+
}
|
|
208
|
+
return Object.freeze({ directory, stats });
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function encodeInstallLease(record: InstallLeaseRecord): string {
|
|
212
|
+
return `${installLeaseMarker}${Buffer.from(JSON.stringify(record), "utf8").toString("base64url")}`;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function parseInstallLease(target: string): InstallLeaseRecord {
|
|
216
|
+
if (!target.startsWith(installLeaseMarker) || target.length > 1_024) {
|
|
217
|
+
throw new Error("local installer lease has an invalid exact marker");
|
|
218
|
+
}
|
|
219
|
+
let value: unknown;
|
|
220
|
+
try {
|
|
221
|
+
value = JSON.parse(
|
|
222
|
+
Buffer.from(target.slice(installLeaseMarker.length), "base64url").toString("utf8"),
|
|
223
|
+
);
|
|
224
|
+
} catch {
|
|
225
|
+
throw new Error("local installer lease has invalid owner metadata");
|
|
226
|
+
}
|
|
227
|
+
if (
|
|
228
|
+
typeof value !== "object"
|
|
229
|
+
|| value === null
|
|
230
|
+
|| Array.isArray(value)
|
|
231
|
+
|| Object.keys(value).sort().join("\0")
|
|
232
|
+
!== ["owner", "schemaVersion", "token"].join("\0")
|
|
233
|
+
) {
|
|
234
|
+
throw new Error("local installer lease has invalid owner metadata");
|
|
235
|
+
}
|
|
236
|
+
const candidate = value as Record<string, unknown>;
|
|
237
|
+
const owner = candidate["owner"];
|
|
238
|
+
if (
|
|
239
|
+
candidate["schemaVersion"] !== 1
|
|
240
|
+
|| typeof owner !== "object"
|
|
241
|
+
|| owner === null
|
|
242
|
+
|| Array.isArray(owner)
|
|
243
|
+
|| Object.keys(owner).sort().join("\0")
|
|
244
|
+
!== ["bootId", "pid", "processStartId"].join("\0")
|
|
245
|
+
|| typeof candidate["token"] !== "string"
|
|
246
|
+
|| !installLeaseTokenPattern.test(candidate["token"])
|
|
247
|
+
) {
|
|
248
|
+
throw new Error("local installer lease has invalid owner metadata");
|
|
249
|
+
}
|
|
250
|
+
const ownerCandidate = owner as Record<string, unknown>;
|
|
251
|
+
if (
|
|
252
|
+
!Number.isSafeInteger(ownerCandidate["pid"])
|
|
253
|
+
|| (ownerCandidate["pid"] as number) <= 0
|
|
254
|
+
|| (ownerCandidate["pid"] as number) > 2_147_483_647
|
|
255
|
+
|| typeof ownerCandidate["bootId"] !== "string"
|
|
256
|
+
|| !/^[a-f0-9]{64}$/u.test(ownerCandidate["bootId"])
|
|
257
|
+
|| typeof ownerCandidate["processStartId"] !== "string"
|
|
258
|
+
|| !/^[a-f0-9]{64}$/u.test(ownerCandidate["processStartId"])
|
|
259
|
+
) {
|
|
260
|
+
throw new Error("local installer lease has invalid owner metadata");
|
|
261
|
+
}
|
|
262
|
+
const record: InstallLeaseRecord = Object.freeze({
|
|
263
|
+
schemaVersion: 1,
|
|
264
|
+
owner: Object.freeze({
|
|
265
|
+
pid: ownerCandidate["pid"] as number,
|
|
266
|
+
bootId: ownerCandidate["bootId"],
|
|
267
|
+
processStartId: ownerCandidate["processStartId"],
|
|
268
|
+
}),
|
|
269
|
+
token: candidate["token"],
|
|
270
|
+
});
|
|
271
|
+
if (encodeInstallLease(record) !== target) {
|
|
272
|
+
throw new Error("local installer lease owner metadata is not canonical");
|
|
273
|
+
}
|
|
274
|
+
return record;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function installMutationClaimPath(
|
|
278
|
+
directory: string,
|
|
279
|
+
phase: InstallMutationClaimPhase,
|
|
280
|
+
claimId: string,
|
|
281
|
+
): string {
|
|
282
|
+
return join(directory, `${installMutationClaimPrefix}${phase}-${claimId}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function parseInstallMutationClaimName(name: string): {
|
|
286
|
+
readonly phase: InstallMutationClaimPhase;
|
|
287
|
+
readonly claimId: string;
|
|
288
|
+
} {
|
|
289
|
+
const match = installMutationClaimNamePattern.exec(name);
|
|
290
|
+
if (match === null) {
|
|
291
|
+
throw new Error("local installer mutation claim filename is invalid");
|
|
292
|
+
}
|
|
293
|
+
return Object.freeze({
|
|
294
|
+
phase: match[1] as InstallMutationClaimPhase,
|
|
295
|
+
claimId: match[2]!,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function encodeInstallMutationClaim(record: InstallMutationClaimRecord): string {
|
|
300
|
+
return `${installMutationClaimMarker}${Buffer.from(JSON.stringify(record), "utf8").toString("base64url")}`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function parseInstallMutationClaim(
|
|
304
|
+
target: string,
|
|
305
|
+
expectedClaimId: string,
|
|
306
|
+
): InstallMutationClaimRecord {
|
|
307
|
+
if (!target.startsWith(installMutationClaimMarker) || target.length > 1_024) {
|
|
308
|
+
throw new Error("local installer mutation claim has an invalid exact marker");
|
|
309
|
+
}
|
|
310
|
+
let value: unknown;
|
|
311
|
+
try {
|
|
312
|
+
value = JSON.parse(
|
|
313
|
+
Buffer.from(
|
|
314
|
+
target.slice(installMutationClaimMarker.length),
|
|
315
|
+
"base64url",
|
|
316
|
+
).toString("utf8"),
|
|
317
|
+
);
|
|
318
|
+
} catch {
|
|
319
|
+
throw new Error("local installer mutation claim has invalid owner metadata");
|
|
320
|
+
}
|
|
321
|
+
if (
|
|
322
|
+
typeof value !== "object"
|
|
323
|
+
|| value === null
|
|
324
|
+
|| Array.isArray(value)
|
|
325
|
+
|| Object.keys(value).sort().join("\0")
|
|
326
|
+
!== ["claimId", "kind", "owner", "schemaVersion"].join("\0")
|
|
327
|
+
) {
|
|
328
|
+
throw new Error("local installer mutation claim has invalid owner metadata");
|
|
329
|
+
}
|
|
330
|
+
const candidate = value as Record<string, unknown>;
|
|
331
|
+
const owner = candidate["owner"];
|
|
332
|
+
if (
|
|
333
|
+
candidate["schemaVersion"] !== 1
|
|
334
|
+
|| candidate["kind"] !== "wrench-install-lease-mutation-claim"
|
|
335
|
+
|| candidate["claimId"] !== expectedClaimId
|
|
336
|
+
|| typeof owner !== "object"
|
|
337
|
+
|| owner === null
|
|
338
|
+
|| Array.isArray(owner)
|
|
339
|
+
|| Object.keys(owner).sort().join("\0")
|
|
340
|
+
!== ["bootId", "pid", "processStartId"].join("\0")
|
|
341
|
+
) {
|
|
342
|
+
throw new Error("local installer mutation claim has invalid owner metadata");
|
|
343
|
+
}
|
|
344
|
+
const ownerCandidate = owner as Record<string, unknown>;
|
|
345
|
+
if (
|
|
346
|
+
!Number.isSafeInteger(ownerCandidate["pid"])
|
|
347
|
+
|| (ownerCandidate["pid"] as number) <= 0
|
|
348
|
+
|| (ownerCandidate["pid"] as number) > 2_147_483_647
|
|
349
|
+
|| typeof ownerCandidate["bootId"] !== "string"
|
|
350
|
+
|| !/^[a-f0-9]{64}$/u.test(ownerCandidate["bootId"])
|
|
351
|
+
|| typeof ownerCandidate["processStartId"] !== "string"
|
|
352
|
+
|| !/^[a-f0-9]{64}$/u.test(ownerCandidate["processStartId"])
|
|
353
|
+
) {
|
|
354
|
+
throw new Error("local installer mutation claim has invalid owner metadata");
|
|
355
|
+
}
|
|
356
|
+
const record: InstallMutationClaimRecord = Object.freeze({
|
|
357
|
+
schemaVersion: 1,
|
|
358
|
+
kind: "wrench-install-lease-mutation-claim",
|
|
359
|
+
claimId: expectedClaimId,
|
|
360
|
+
owner: Object.freeze({
|
|
361
|
+
pid: ownerCandidate["pid"] as number,
|
|
362
|
+
bootId: ownerCandidate["bootId"],
|
|
363
|
+
processStartId: ownerCandidate["processStartId"],
|
|
364
|
+
}),
|
|
365
|
+
});
|
|
366
|
+
if (encodeInstallMutationClaim(record) !== target) {
|
|
367
|
+
throw new Error("local installer mutation claim metadata is not canonical");
|
|
368
|
+
}
|
|
369
|
+
return record;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function snapshotInstallMutationClaim(
|
|
373
|
+
directory: string,
|
|
374
|
+
name: string,
|
|
375
|
+
fileSystem: InstallMutationClaimSnapshotFileSystemForTest =
|
|
376
|
+
installMutationClaimSnapshotFileSystem,
|
|
377
|
+
): InstallMutationClaimSnapshot | undefined {
|
|
378
|
+
const parsedName = parseInstallMutationClaimName(name);
|
|
379
|
+
const path = join(directory, name);
|
|
380
|
+
let before: BigIntStats;
|
|
381
|
+
try {
|
|
382
|
+
before = fileSystem.lstat(path);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
if (errorCode(error) === "ENOENT") return undefined;
|
|
385
|
+
throw error;
|
|
386
|
+
}
|
|
387
|
+
requireStableInstallMutationClaimSymlink(
|
|
388
|
+
path,
|
|
389
|
+
before,
|
|
390
|
+
"mutation claim",
|
|
391
|
+
fileSystem,
|
|
392
|
+
);
|
|
393
|
+
let target: string;
|
|
394
|
+
let after: BigIntStats;
|
|
395
|
+
try {
|
|
396
|
+
target = fileSystem.readlink(path);
|
|
397
|
+
after = fileSystem.lstat(path);
|
|
398
|
+
if (
|
|
399
|
+
!sameFileIdentity(before, after)
|
|
400
|
+
|| fileSystem.readlink(path) !== target
|
|
401
|
+
) {
|
|
402
|
+
throw new InstallMutationClaimChangedError(
|
|
403
|
+
"local installer mutation claim changed during inspection",
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
} catch (error) {
|
|
407
|
+
if (errorCode(error) === "ENOENT") return undefined;
|
|
408
|
+
if (errorCode(error) === "EINVAL") {
|
|
409
|
+
throw new InstallMutationClaimChangedError(
|
|
410
|
+
"local installer mutation claim changed during inspection",
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
throw error;
|
|
414
|
+
}
|
|
415
|
+
return Object.freeze({
|
|
416
|
+
path,
|
|
417
|
+
phase: parsedName.phase,
|
|
418
|
+
record: parseInstallMutationClaim(target, parsedName.claimId),
|
|
419
|
+
stats: after,
|
|
420
|
+
target,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function removeExactInstallMutationClaim(
|
|
425
|
+
snapshot: InstallMutationClaimSnapshot,
|
|
426
|
+
): boolean {
|
|
427
|
+
let current: InstallMutationClaimSnapshot | undefined;
|
|
428
|
+
try {
|
|
429
|
+
current = snapshotInstallMutationClaim(
|
|
430
|
+
resolve(snapshot.path, ".."),
|
|
431
|
+
basename(snapshot.path),
|
|
432
|
+
);
|
|
433
|
+
} catch (error) {
|
|
434
|
+
if (error instanceof InstallMutationClaimChangedError) return false;
|
|
435
|
+
throw error;
|
|
436
|
+
}
|
|
437
|
+
if (current === undefined) return false;
|
|
438
|
+
if (
|
|
439
|
+
!sameFileIdentity(snapshot.stats, current.stats)
|
|
440
|
+
|| snapshot.target !== current.target
|
|
441
|
+
) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Claim pathnames are never reused. Moving the exact inode to a fresh,
|
|
446
|
+
// non-claim pathname before unlinking means two stale reclaimers can race
|
|
447
|
+
// without either one ever unlinking a later claimant at the old pathname.
|
|
448
|
+
const retiredPath = join(
|
|
449
|
+
resolve(snapshot.path, ".."),
|
|
450
|
+
`${installMutationRetiredClaimPrefix}${snapshot.record.claimId}-${randomUUID()}`,
|
|
451
|
+
);
|
|
452
|
+
try {
|
|
453
|
+
renameSync(snapshot.path, retiredPath);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
if (errorCode(error) === "ENOENT") return false;
|
|
456
|
+
throw error;
|
|
457
|
+
}
|
|
458
|
+
const retiredStats = lstatSync(retiredPath, { bigint: true });
|
|
459
|
+
if (
|
|
460
|
+
retiredStats.dev !== snapshot.stats.dev
|
|
461
|
+
|| retiredStats.ino !== snapshot.stats.ino
|
|
462
|
+
|| readlinkSync(retiredPath, "utf8") !== snapshot.target
|
|
463
|
+
) {
|
|
464
|
+
throw new Error("local installer mutation claim changed during retirement");
|
|
465
|
+
}
|
|
466
|
+
pauseAfterRetiringInstallMutationClaimForTest();
|
|
467
|
+
const finalStats = lstatSync(retiredPath, { bigint: true });
|
|
468
|
+
if (
|
|
469
|
+
!sameFileIdentity(retiredStats, finalStats)
|
|
470
|
+
|| readlinkSync(retiredPath, "utf8") !== snapshot.target
|
|
471
|
+
) {
|
|
472
|
+
throw new Error("local installer retired mutation claim changed before removal");
|
|
473
|
+
}
|
|
474
|
+
unlinkSync(retiredPath);
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function snapshotRetiredInstallMutationClaim(
|
|
479
|
+
directory: string,
|
|
480
|
+
name: string,
|
|
481
|
+
fileSystem: InstallMutationClaimSnapshotFileSystemForTest =
|
|
482
|
+
installMutationClaimSnapshotFileSystem,
|
|
483
|
+
): RetiredInstallMutationClaimSnapshot | undefined {
|
|
484
|
+
const match = installMutationRetiredClaimNamePattern.exec(name);
|
|
485
|
+
if (match === null) {
|
|
486
|
+
throw new Error("local installer retired mutation claim filename is invalid");
|
|
487
|
+
}
|
|
488
|
+
const claimId = match[1]!;
|
|
489
|
+
const path = join(directory, name);
|
|
490
|
+
let before: BigIntStats;
|
|
491
|
+
try {
|
|
492
|
+
before = fileSystem.lstat(path);
|
|
493
|
+
} catch (error) {
|
|
494
|
+
if (errorCode(error) === "ENOENT") return undefined;
|
|
495
|
+
throw error;
|
|
496
|
+
}
|
|
497
|
+
requireStableInstallMutationClaimSymlink(
|
|
498
|
+
path,
|
|
499
|
+
before,
|
|
500
|
+
"retired mutation claim",
|
|
501
|
+
fileSystem,
|
|
502
|
+
);
|
|
503
|
+
let target: string;
|
|
504
|
+
let after: BigIntStats;
|
|
505
|
+
try {
|
|
506
|
+
target = fileSystem.readlink(path);
|
|
507
|
+
after = fileSystem.lstat(path);
|
|
508
|
+
if (
|
|
509
|
+
!sameFileIdentity(before, after)
|
|
510
|
+
|| fileSystem.readlink(path) !== target
|
|
511
|
+
) {
|
|
512
|
+
throw new InstallMutationClaimChangedError(
|
|
513
|
+
"local installer retired mutation claim changed during inspection",
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
} catch (error) {
|
|
517
|
+
if (errorCode(error) === "ENOENT") return undefined;
|
|
518
|
+
if (errorCode(error) === "EINVAL") {
|
|
519
|
+
throw new InstallMutationClaimChangedError(
|
|
520
|
+
"local installer retired mutation claim changed during inspection",
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
525
|
+
return Object.freeze({
|
|
526
|
+
path,
|
|
527
|
+
record: parseInstallMutationClaim(target, claimId),
|
|
528
|
+
stats: after,
|
|
529
|
+
target,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export function snapshotRetiredInstallMutationClaimForTest(
|
|
534
|
+
directory: string,
|
|
535
|
+
name: string,
|
|
536
|
+
fileSystem: InstallMutationClaimSnapshotFileSystemForTest,
|
|
537
|
+
): "absent" | "changed" | "present" {
|
|
538
|
+
try {
|
|
539
|
+
return snapshotRetiredInstallMutationClaim(directory, name, fileSystem)
|
|
540
|
+
=== undefined
|
|
541
|
+
? "absent"
|
|
542
|
+
: "present";
|
|
543
|
+
} catch (error) {
|
|
544
|
+
if (error instanceof InstallMutationClaimChangedError) return "changed";
|
|
545
|
+
throw error;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function removeExactRetiredInstallMutationClaim(
|
|
550
|
+
snapshot: RetiredInstallMutationClaimSnapshot,
|
|
551
|
+
): boolean {
|
|
552
|
+
let current: RetiredInstallMutationClaimSnapshot | undefined;
|
|
553
|
+
try {
|
|
554
|
+
current = snapshotRetiredInstallMutationClaim(
|
|
555
|
+
resolve(snapshot.path, ".."),
|
|
556
|
+
basename(snapshot.path),
|
|
557
|
+
);
|
|
558
|
+
} catch (error) {
|
|
559
|
+
if (error instanceof InstallMutationClaimChangedError) return false;
|
|
560
|
+
throw error;
|
|
561
|
+
}
|
|
562
|
+
if (current === undefined) return false;
|
|
563
|
+
if (
|
|
564
|
+
!sameFileIdentity(snapshot.stats, current.stats)
|
|
565
|
+
|| snapshot.target !== current.target
|
|
566
|
+
) {
|
|
567
|
+
return false;
|
|
568
|
+
}
|
|
569
|
+
try {
|
|
570
|
+
unlinkSync(snapshot.path);
|
|
571
|
+
} catch (error) {
|
|
572
|
+
if (errorCode(error) === "ENOENT") return false;
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
575
|
+
return true;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function boundedInstallMutationEntryNames(
|
|
579
|
+
directory: string,
|
|
580
|
+
prefix: string,
|
|
581
|
+
): readonly string[] {
|
|
582
|
+
const names: string[] = [];
|
|
583
|
+
const handle = opendirSync(directory);
|
|
584
|
+
try {
|
|
585
|
+
let entryCount = 0;
|
|
586
|
+
for (;;) {
|
|
587
|
+
const entry = handle.readSync();
|
|
588
|
+
if (entry === null) break;
|
|
589
|
+
entryCount += 1;
|
|
590
|
+
if (entryCount > maximumDirectoryEntries) {
|
|
591
|
+
throw new Error(
|
|
592
|
+
`local installer recovery directory exceeds ${maximumDirectoryEntries} entries`,
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
if (entry.name.startsWith(prefix)) names.push(entry.name);
|
|
596
|
+
}
|
|
597
|
+
} finally {
|
|
598
|
+
handle.closeSync();
|
|
599
|
+
}
|
|
600
|
+
return names.sort();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function recoverRetiredInstallMutationClaims(directory: string): void {
|
|
604
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
605
|
+
const names = boundedInstallMutationEntryNames(
|
|
606
|
+
directory,
|
|
607
|
+
installMutationRetiredClaimPrefix,
|
|
608
|
+
);
|
|
609
|
+
let retry = false;
|
|
610
|
+
let removed = false;
|
|
611
|
+
let liveCount = 0;
|
|
612
|
+
for (const name of names) {
|
|
613
|
+
let snapshot: RetiredInstallMutationClaimSnapshot | undefined;
|
|
614
|
+
try {
|
|
615
|
+
snapshot = snapshotRetiredInstallMutationClaim(directory, name);
|
|
616
|
+
} catch (error) {
|
|
617
|
+
if (error instanceof InstallMutationClaimChangedError) {
|
|
618
|
+
retry = true;
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
throw error;
|
|
622
|
+
}
|
|
623
|
+
if (snapshot === undefined) {
|
|
624
|
+
retry = true;
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
const status = processOwnerStatus(snapshot.record.owner);
|
|
628
|
+
if (status === "unknown") {
|
|
629
|
+
throw new Error(
|
|
630
|
+
"local installer retired mutation claim owner identity cannot be determined safely",
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
if (status === "exact-live-owner") {
|
|
634
|
+
liveCount += 1;
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
if (removeExactRetiredInstallMutationClaim(snapshot)) {
|
|
638
|
+
removed = true;
|
|
639
|
+
} else {
|
|
640
|
+
retry = true;
|
|
641
|
+
break;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
if (retry || removed) continue;
|
|
645
|
+
if (liveCount > maximumMutationClaims) {
|
|
646
|
+
throw new Error(
|
|
647
|
+
`local installer live retired mutation claim count exceeds ${maximumMutationClaims}`,
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
throw new Error(
|
|
653
|
+
"local installer retired mutation claims did not reach a stable snapshot",
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function listLiveInstallMutationClaims(
|
|
658
|
+
directory: string,
|
|
659
|
+
): readonly InstallMutationClaimSnapshot[] {
|
|
660
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
661
|
+
recoverRetiredInstallMutationClaims(directory);
|
|
662
|
+
const names = boundedInstallMutationEntryNames(
|
|
663
|
+
directory,
|
|
664
|
+
installMutationClaimPrefix,
|
|
665
|
+
);
|
|
666
|
+
let retry = false;
|
|
667
|
+
let removedStaleClaim = false;
|
|
668
|
+
const live: InstallMutationClaimSnapshot[] = [];
|
|
669
|
+
for (const name of names) {
|
|
670
|
+
let snapshot: InstallMutationClaimSnapshot | undefined;
|
|
671
|
+
try {
|
|
672
|
+
snapshot = snapshotInstallMutationClaim(directory, name);
|
|
673
|
+
} catch (error) {
|
|
674
|
+
if (error instanceof InstallMutationClaimChangedError) {
|
|
675
|
+
retry = true;
|
|
676
|
+
break;
|
|
677
|
+
}
|
|
678
|
+
throw error;
|
|
679
|
+
}
|
|
680
|
+
if (snapshot === undefined) {
|
|
681
|
+
retry = true;
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
const status = processOwnerStatus(snapshot.record.owner);
|
|
685
|
+
if (status === "unknown") {
|
|
686
|
+
throw new Error(
|
|
687
|
+
"local installer mutation claim owner identity cannot be determined safely",
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
if (status === "different-or-dead") {
|
|
691
|
+
if (removeExactInstallMutationClaim(snapshot)) {
|
|
692
|
+
removedStaleClaim = true;
|
|
693
|
+
} else {
|
|
694
|
+
retry = true;
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
continue;
|
|
698
|
+
}
|
|
699
|
+
live.push(snapshot);
|
|
700
|
+
}
|
|
701
|
+
if (retry || removedStaleClaim) continue;
|
|
702
|
+
if (live.length > maximumMutationClaims) {
|
|
703
|
+
throw new Error(
|
|
704
|
+
`local installer live mutation claim count exceeds ${maximumMutationClaims}`,
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
return live;
|
|
708
|
+
}
|
|
709
|
+
throw new Error("local installer mutation claims did not reach a stable snapshot");
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function waitAtInstallMutationClaimTestBarrier(
|
|
713
|
+
readyPath: string,
|
|
714
|
+
releasePath: string,
|
|
715
|
+
label: string,
|
|
716
|
+
): void {
|
|
717
|
+
try {
|
|
718
|
+
writeFileSync(readyPath, `${process.pid}\n`, { flag: "wx", mode: 0o600 });
|
|
719
|
+
} catch (error) {
|
|
720
|
+
if (errorCode(error) !== "EEXIST") throw error;
|
|
721
|
+
}
|
|
722
|
+
const deadline = Date.now() + TEST_BARRIER_TIMEOUT_MS;
|
|
723
|
+
while (!existsSync(releasePath)) {
|
|
724
|
+
if (Date.now() >= deadline) {
|
|
725
|
+
throw new Error(`local installer ${label} test barrier timed out`);
|
|
726
|
+
}
|
|
727
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function pauseAfterWaitingInstallMutationClaimForTest(claimId: string): void {
|
|
732
|
+
if (process.env.NODE_ENV !== "test") return;
|
|
733
|
+
const readyDirectory =
|
|
734
|
+
(process.env.GHOSTGET_TEST_INSTALL_MUTATION_WAITING_READY_DIRECTORY ?? process.env.WRENCH_TEST_INSTALL_MUTATION_WAITING_READY_DIRECTORY);
|
|
735
|
+
const releasePath =
|
|
736
|
+
(process.env.GHOSTGET_TEST_INSTALL_MUTATION_WAITING_RELEASE ?? process.env.WRENCH_TEST_INSTALL_MUTATION_WAITING_RELEASE);
|
|
737
|
+
if (readyDirectory === undefined || releasePath === undefined) return;
|
|
738
|
+
waitAtInstallMutationClaimTestBarrier(
|
|
739
|
+
join(readyDirectory, `${claimId}.ready`),
|
|
740
|
+
releasePath,
|
|
741
|
+
"waiting-claim",
|
|
742
|
+
);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
function pauseAfterHeldInstallMutationClaimForTest(): void {
|
|
746
|
+
if (process.env.NODE_ENV !== "test") return;
|
|
747
|
+
const readyPath = (process.env.GHOSTGET_TEST_INSTALL_MUTATION_HELD_READY ?? process.env.WRENCH_TEST_INSTALL_MUTATION_HELD_READY);
|
|
748
|
+
const releasePath = (process.env.GHOSTGET_TEST_INSTALL_MUTATION_HELD_RELEASE ?? process.env.WRENCH_TEST_INSTALL_MUTATION_HELD_RELEASE);
|
|
749
|
+
if (readyPath === undefined || releasePath === undefined) return;
|
|
750
|
+
waitAtInstallMutationClaimTestBarrier(
|
|
751
|
+
readyPath,
|
|
752
|
+
releasePath,
|
|
753
|
+
"held-claim",
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function pauseBeforeInstallLeaseRemovalForTest(): void {
|
|
758
|
+
if (process.env.NODE_ENV !== "test") return;
|
|
759
|
+
const readyPath = (process.env.GHOSTGET_TEST_INSTALL_RELEASE_READY ?? process.env.WRENCH_TEST_INSTALL_RELEASE_READY);
|
|
760
|
+
const releasePath = (process.env.GHOSTGET_TEST_INSTALL_RELEASE_RELEASE ?? process.env.WRENCH_TEST_INSTALL_RELEASE_RELEASE);
|
|
761
|
+
if (readyPath === undefined || releasePath === undefined) return;
|
|
762
|
+
waitAtInstallMutationClaimTestBarrier(
|
|
763
|
+
readyPath,
|
|
764
|
+
releasePath,
|
|
765
|
+
"lease-release",
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function pauseAfterRetiringInstallMutationClaimForTest(): void {
|
|
770
|
+
if (process.env.NODE_ENV !== "test") return;
|
|
771
|
+
const readyPath = (process.env.GHOSTGET_TEST_INSTALL_MUTATION_RETIRED_READY ?? process.env.WRENCH_TEST_INSTALL_MUTATION_RETIRED_READY);
|
|
772
|
+
const releasePath = (process.env.GHOSTGET_TEST_INSTALL_MUTATION_RETIRED_RELEASE ?? process.env.WRENCH_TEST_INSTALL_MUTATION_RETIRED_RELEASE);
|
|
773
|
+
if (readyPath === undefined || releasePath === undefined) return;
|
|
774
|
+
waitAtInstallMutationClaimTestBarrier(
|
|
775
|
+
readyPath,
|
|
776
|
+
releasePath,
|
|
777
|
+
"retired-claim",
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function pauseBeforeManagedWrapperMutationForTest(): void {
|
|
782
|
+
if (process.env.NODE_ENV !== "test") return;
|
|
783
|
+
const readyPath = (process.env.GHOSTGET_TEST_INSTALL_TARGET_MUTATION_READY ?? process.env.WRENCH_TEST_INSTALL_TARGET_MUTATION_READY);
|
|
784
|
+
const releasePath = (process.env.GHOSTGET_TEST_INSTALL_TARGET_MUTATION_RELEASE ?? process.env.WRENCH_TEST_INSTALL_TARGET_MUTATION_RELEASE);
|
|
785
|
+
if (readyPath === undefined || releasePath === undefined) return;
|
|
786
|
+
waitAtInstallMutationClaimTestBarrier(
|
|
787
|
+
readyPath,
|
|
788
|
+
releasePath,
|
|
789
|
+
"managed-wrapper-mutation",
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function renameOwnedInstallMutationClaim(
|
|
794
|
+
directory: string,
|
|
795
|
+
fromPhase: InstallMutationClaimPhase,
|
|
796
|
+
toPhase: InstallMutationClaimPhase,
|
|
797
|
+
claimId: string,
|
|
798
|
+
expectedTarget: string,
|
|
799
|
+
): string {
|
|
800
|
+
const fromPath = installMutationClaimPath(directory, fromPhase, claimId);
|
|
801
|
+
const before = snapshotInstallMutationClaim(directory, basename(fromPath));
|
|
802
|
+
if (before === undefined || before.target !== expectedTarget) {
|
|
803
|
+
throw new Error("local installer mutation claim changed before promotion");
|
|
804
|
+
}
|
|
805
|
+
const toPath = installMutationClaimPath(directory, toPhase, claimId);
|
|
806
|
+
renameSync(fromPath, toPath);
|
|
807
|
+
const after = snapshotInstallMutationClaim(directory, basename(toPath));
|
|
808
|
+
if (
|
|
809
|
+
after === undefined
|
|
810
|
+
|| after.target !== expectedTarget
|
|
811
|
+
|| after.stats.dev !== before.stats.dev
|
|
812
|
+
|| after.stats.ino !== before.stats.ino
|
|
813
|
+
) {
|
|
814
|
+
throw new Error("local installer mutation claim changed during promotion");
|
|
815
|
+
}
|
|
816
|
+
return toPath;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
function acquireInstallMutationClaim(
|
|
820
|
+
directoryValue: string,
|
|
821
|
+
): HeldInstallMutationClaim | undefined {
|
|
822
|
+
const { directory, stats: rootStats } = validateRecoveryDirectory(directoryValue);
|
|
823
|
+
const claimId = randomUUID();
|
|
824
|
+
const record: InstallMutationClaimRecord = Object.freeze({
|
|
825
|
+
schemaVersion: 1,
|
|
826
|
+
kind: "wrench-install-lease-mutation-claim",
|
|
827
|
+
claimId,
|
|
828
|
+
owner: captureProcessOwnerIdentity(process.pid),
|
|
829
|
+
});
|
|
830
|
+
const target = encodeInstallMutationClaim(record);
|
|
831
|
+
let phase: InstallMutationClaimPhase = "waiting";
|
|
832
|
+
let claimPath = installMutationClaimPath(directory, phase, claimId);
|
|
833
|
+
symlinkSync(target, claimPath);
|
|
834
|
+
let released = false;
|
|
835
|
+
const release = (): void => {
|
|
836
|
+
if (released) return;
|
|
837
|
+
const snapshot = snapshotInstallMutationClaim(directory, basename(claimPath));
|
|
838
|
+
if (snapshot === undefined || snapshot.target !== target) {
|
|
839
|
+
throw new Error("local installer mutation claim changed before release");
|
|
840
|
+
}
|
|
841
|
+
if (!removeExactInstallMutationClaim(snapshot)) {
|
|
842
|
+
throw new Error("local installer mutation claim changed during release");
|
|
843
|
+
}
|
|
844
|
+
released = true;
|
|
845
|
+
};
|
|
846
|
+
try {
|
|
847
|
+
pauseAfterWaitingInstallMutationClaimForTest(claimId);
|
|
848
|
+
const waiting = listLiveInstallMutationClaims(directory);
|
|
849
|
+
if (
|
|
850
|
+
waiting.some((candidate) =>
|
|
851
|
+
candidate.record.claimId !== claimId
|
|
852
|
+
&& (
|
|
853
|
+
candidate.phase !== "waiting"
|
|
854
|
+
|| candidate.record.claimId < claimId
|
|
855
|
+
)
|
|
856
|
+
)
|
|
857
|
+
) {
|
|
858
|
+
release();
|
|
859
|
+
return undefined;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
claimPath = renameOwnedInstallMutationClaim(
|
|
863
|
+
directory,
|
|
864
|
+
phase,
|
|
865
|
+
"candidate",
|
|
866
|
+
claimId,
|
|
867
|
+
target,
|
|
868
|
+
);
|
|
869
|
+
phase = "candidate";
|
|
870
|
+
const candidates = listLiveInstallMutationClaims(directory);
|
|
871
|
+
if (
|
|
872
|
+
candidates.some((candidate) =>
|
|
873
|
+
candidate.record.claimId !== claimId
|
|
874
|
+
&& (
|
|
875
|
+
candidate.phase === "held"
|
|
876
|
+
|| candidate.record.claimId < claimId
|
|
877
|
+
)
|
|
878
|
+
)
|
|
879
|
+
) {
|
|
880
|
+
release();
|
|
881
|
+
return undefined;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
claimPath = renameOwnedInstallMutationClaim(
|
|
885
|
+
directory,
|
|
886
|
+
phase,
|
|
887
|
+
"held",
|
|
888
|
+
claimId,
|
|
889
|
+
target,
|
|
890
|
+
);
|
|
891
|
+
phase = "held";
|
|
892
|
+
const held = listLiveInstallMutationClaims(directory);
|
|
893
|
+
if (
|
|
894
|
+
held.some((candidate) =>
|
|
895
|
+
candidate.record.claimId !== claimId && candidate.phase === "held"
|
|
896
|
+
)
|
|
897
|
+
) {
|
|
898
|
+
throw new Error("local installer mutation arbitration admitted two owners");
|
|
899
|
+
}
|
|
900
|
+
const rootAfter = lstatSync(directory, { bigint: true });
|
|
901
|
+
if (!sameDirectoryObject(rootStats, rootAfter)) {
|
|
902
|
+
throw new Error("local installer recovery directory changed during arbitration");
|
|
903
|
+
}
|
|
904
|
+
pauseAfterHeldInstallMutationClaimForTest();
|
|
905
|
+
return Object.freeze({ directory, rootStats, release });
|
|
906
|
+
} catch (error) {
|
|
907
|
+
if (!released) release();
|
|
908
|
+
throw error;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function snapshotInstallLease(directory: string): InstallLeaseSnapshot | undefined {
|
|
913
|
+
const path = join(directory, installLeaseName);
|
|
914
|
+
let before: BigIntStats;
|
|
915
|
+
try {
|
|
916
|
+
before = lstatSync(path, { bigint: true });
|
|
917
|
+
} catch (error) {
|
|
918
|
+
if (errorCode(error) === "ENOENT") return undefined;
|
|
919
|
+
throw error;
|
|
920
|
+
}
|
|
921
|
+
if (
|
|
922
|
+
!before.isSymbolicLink()
|
|
923
|
+
|| !ownedByCurrentUser(before)
|
|
924
|
+
|| before.nlink !== 1n
|
|
925
|
+
) {
|
|
926
|
+
throw new Error("local installer lease must be an owned exact symbolic lease");
|
|
927
|
+
}
|
|
928
|
+
const target = readlinkSync(path, "utf8");
|
|
929
|
+
const after = lstatSync(path, { bigint: true });
|
|
930
|
+
if (!sameFileIdentity(before, after) || readlinkSync(path, "utf8") !== target) {
|
|
931
|
+
throw new Error("local installer lease changed during inspection");
|
|
932
|
+
}
|
|
933
|
+
return Object.freeze({
|
|
934
|
+
path,
|
|
935
|
+
record: parseInstallLease(target),
|
|
936
|
+
stats: after,
|
|
937
|
+
target,
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function assertLeaseUnchanged(snapshot: InstallLeaseSnapshot): void {
|
|
942
|
+
const current = lstatSync(snapshot.path, { bigint: true });
|
|
943
|
+
if (
|
|
944
|
+
!sameFileIdentity(snapshot.stats, current)
|
|
945
|
+
|| readlinkSync(snapshot.path, "utf8") !== snapshot.target
|
|
946
|
+
) {
|
|
947
|
+
throw new Error("local installer lease changed before removal");
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function leaseIsStale(snapshot: InstallLeaseSnapshot): boolean {
|
|
952
|
+
const status = processOwnerStatus(snapshot.record.owner);
|
|
953
|
+
if (status === "unknown") {
|
|
954
|
+
throw new Error("local installer lease owner identity cannot be determined safely");
|
|
955
|
+
}
|
|
956
|
+
return status === "different-or-dead";
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function snapshotManagedInstallStage(
|
|
960
|
+
directory: string,
|
|
961
|
+
name: string,
|
|
962
|
+
): InstallStageSnapshot {
|
|
963
|
+
const path = join(directory, name);
|
|
964
|
+
const lexical = lstatSync(path, { bigint: true });
|
|
965
|
+
const permissions = lexical.mode & 0o777n;
|
|
966
|
+
const hasAcceptedStageMode = permissions === 0o600n
|
|
967
|
+
|| permissions === 0o700n
|
|
968
|
+
|| (name.startsWith(".oh-install.") && permissions === 0o755n);
|
|
969
|
+
if (
|
|
970
|
+
lexical.isSymbolicLink()
|
|
971
|
+
|| !lexical.isFile()
|
|
972
|
+
|| !ownedByCurrentUser(lexical)
|
|
973
|
+
|| lexical.nlink !== 1n
|
|
974
|
+
|| !hasAcceptedStageMode
|
|
975
|
+
|| lexical.size < 0n
|
|
976
|
+
|| lexical.size > BigInt(maximumInstallStageBytes)
|
|
977
|
+
) {
|
|
978
|
+
throw new Error(
|
|
979
|
+
`abandoned local installer stage ${name} is not an owned private bounded regular file`,
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const descriptor = openSync(
|
|
984
|
+
path,
|
|
985
|
+
constants.O_RDONLY
|
|
986
|
+
| (constants.O_NOFOLLOW ?? 0)
|
|
987
|
+
| (constants.O_NONBLOCK ?? 0),
|
|
988
|
+
);
|
|
989
|
+
try {
|
|
990
|
+
const before = fstatSync(descriptor, { bigint: true });
|
|
991
|
+
if (!sameFileIdentity(lexical, before)) {
|
|
992
|
+
throw new Error(`abandoned local installer stage ${name} changed before inspection`);
|
|
993
|
+
}
|
|
994
|
+
const bytes = readFileSync(descriptor);
|
|
995
|
+
const after = fstatSync(descriptor, { bigint: true });
|
|
996
|
+
const finalLexical = lstatSync(path, { bigint: true });
|
|
997
|
+
if (
|
|
998
|
+
bytes.byteLength > maximumInstallStageBytes
|
|
999
|
+
|| !sameFileIdentity(before, after)
|
|
1000
|
+
|| !sameFileIdentity(after, finalLexical)
|
|
1001
|
+
) {
|
|
1002
|
+
throw new Error(`abandoned local installer stage ${name} changed during inspection`);
|
|
1003
|
+
}
|
|
1004
|
+
let content: string;
|
|
1005
|
+
try {
|
|
1006
|
+
content = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1007
|
+
} catch {
|
|
1008
|
+
throw new Error(`abandoned local installer stage ${name} is not valid UTF-8`);
|
|
1009
|
+
}
|
|
1010
|
+
const marker = expectedMarker(name);
|
|
1011
|
+
const managedHeader = `#!/bin/sh\n${marker}\n`;
|
|
1012
|
+
const isInterruptedManagedPrefix = managedHeader.startsWith(content);
|
|
1013
|
+
if (
|
|
1014
|
+
!isInterruptedManagedPrefix
|
|
1015
|
+
&& !content.split("\n").includes(marker)
|
|
1016
|
+
) {
|
|
1017
|
+
throw new Error(`abandoned local installer stage ${name} is not managed by Ghostget`);
|
|
1018
|
+
}
|
|
1019
|
+
return Object.freeze({
|
|
1020
|
+
bytes: Buffer.from(bytes),
|
|
1021
|
+
name,
|
|
1022
|
+
path,
|
|
1023
|
+
stats: finalLexical,
|
|
1024
|
+
});
|
|
1025
|
+
} finally {
|
|
1026
|
+
closeSync(descriptor);
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function snapshotManagedWrapperIfPresent(
|
|
1031
|
+
directory: string,
|
|
1032
|
+
name: "oh" | "wrench",
|
|
1033
|
+
marker: string,
|
|
1034
|
+
): ManagedWrapperSnapshot | undefined {
|
|
1035
|
+
const path = join(directory, name);
|
|
1036
|
+
let lexical: BigIntStats;
|
|
1037
|
+
try {
|
|
1038
|
+
lexical = lstatSync(path, { bigint: true });
|
|
1039
|
+
} catch (error) {
|
|
1040
|
+
if (errorCode(error) === "ENOENT") return undefined;
|
|
1041
|
+
throw error;
|
|
1042
|
+
}
|
|
1043
|
+
const permissions = lexical.mode & 0o777n;
|
|
1044
|
+
if (
|
|
1045
|
+
lexical.isSymbolicLink()
|
|
1046
|
+
|| !lexical.isFile()
|
|
1047
|
+
|| !ownedByCurrentUser(lexical)
|
|
1048
|
+
|| lexical.nlink !== 1n
|
|
1049
|
+
|| (permissions !== 0o700n && permissions !== 0o755n)
|
|
1050
|
+
|| lexical.size < 0n
|
|
1051
|
+
|| lexical.size > BigInt(maximumInstallStageBytes)
|
|
1052
|
+
) {
|
|
1053
|
+
throw new Error(
|
|
1054
|
+
`managed ${name} wrapper must be an owned, singly linked, bounded executable file`,
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
const descriptor = openSync(
|
|
1058
|
+
path,
|
|
1059
|
+
constants.O_RDONLY
|
|
1060
|
+
| (constants.O_NOFOLLOW ?? 0)
|
|
1061
|
+
| (constants.O_NONBLOCK ?? 0),
|
|
1062
|
+
);
|
|
1063
|
+
try {
|
|
1064
|
+
const before = fstatSync(descriptor, { bigint: true });
|
|
1065
|
+
if (!sameFileIdentity(lexical, before)) {
|
|
1066
|
+
throw new Error(`managed ${name} wrapper changed before inspection`);
|
|
1067
|
+
}
|
|
1068
|
+
const bytes = readFileSync(descriptor);
|
|
1069
|
+
const after = fstatSync(descriptor, { bigint: true });
|
|
1070
|
+
const finalLexical = lstatSync(path, { bigint: true });
|
|
1071
|
+
if (
|
|
1072
|
+
bytes.byteLength > maximumInstallStageBytes
|
|
1073
|
+
|| !sameFileIdentity(before, after)
|
|
1074
|
+
|| !sameFileIdentity(after, finalLexical)
|
|
1075
|
+
) {
|
|
1076
|
+
throw new Error(`managed ${name} wrapper changed during inspection`);
|
|
1077
|
+
}
|
|
1078
|
+
let content: string;
|
|
1079
|
+
try {
|
|
1080
|
+
content = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1081
|
+
} catch {
|
|
1082
|
+
throw new Error(`managed ${name} wrapper is not valid UTF-8`);
|
|
1083
|
+
}
|
|
1084
|
+
if (!content.split("\n").includes(marker)) {
|
|
1085
|
+
throw new Error(`managed ${name} wrapper ownership marker is missing`);
|
|
1086
|
+
}
|
|
1087
|
+
return Object.freeze({
|
|
1088
|
+
bytes: Buffer.from(bytes),
|
|
1089
|
+
path,
|
|
1090
|
+
stats: finalLexical,
|
|
1091
|
+
});
|
|
1092
|
+
} finally {
|
|
1093
|
+
closeSync(descriptor);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
function assertManagedWrapperUnchanged(
|
|
1098
|
+
snapshot: ManagedWrapperSnapshot,
|
|
1099
|
+
): void {
|
|
1100
|
+
const current = lstatSync(snapshot.path, { bigint: true });
|
|
1101
|
+
if (!sameFileIdentity(snapshot.stats, current)) {
|
|
1102
|
+
throw new Error("managed local wrapper changed before mutation");
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
function validateOwnedLiveInstallLease(
|
|
1107
|
+
directory: string,
|
|
1108
|
+
ownerPid: number,
|
|
1109
|
+
token: string,
|
|
1110
|
+
): InstallLeaseSnapshot {
|
|
1111
|
+
if (!Number.isSafeInteger(ownerPid) || ownerPid <= 0 || ownerPid > 2_147_483_647) {
|
|
1112
|
+
throw new Error("local installer lease owner PID is invalid");
|
|
1113
|
+
}
|
|
1114
|
+
if (!installLeaseTokenPattern.test(token)) {
|
|
1115
|
+
throw new Error("local installer lease token is invalid");
|
|
1116
|
+
}
|
|
1117
|
+
const lease = snapshotInstallLease(directory);
|
|
1118
|
+
if (
|
|
1119
|
+
lease === undefined
|
|
1120
|
+
|| lease.record.owner.pid !== ownerPid
|
|
1121
|
+
|| lease.record.token !== token
|
|
1122
|
+
) {
|
|
1123
|
+
throw new Error("local installer lease is not owned by this installer");
|
|
1124
|
+
}
|
|
1125
|
+
if (processOwnerStatus(lease.record.owner) !== "exact-live-owner") {
|
|
1126
|
+
throw new Error("local installer lease owner is no longer live");
|
|
1127
|
+
}
|
|
1128
|
+
return lease;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* Publish the exact staged wrapper inside the helper that owns mutation
|
|
1133
|
+
* arbitration. If the parent shell dies, this helper either completes its one
|
|
1134
|
+
* atomic rename before releasing arbitration or is itself proven dead before a
|
|
1135
|
+
* successor may proceed; no orphaned mv process exists.
|
|
1136
|
+
*/
|
|
1137
|
+
export function publishManagedInstallWrapper(
|
|
1138
|
+
directoryValue: string,
|
|
1139
|
+
ownerPid: number,
|
|
1140
|
+
token: string,
|
|
1141
|
+
stageValue: string,
|
|
1142
|
+
): "already-installed" | "installed" {
|
|
1143
|
+
const claim = acquireInstallMutationClaim(directoryValue);
|
|
1144
|
+
if (claim === undefined) {
|
|
1145
|
+
throw new Error("another Ghostget installer lease mutation helper is active");
|
|
1146
|
+
}
|
|
1147
|
+
try {
|
|
1148
|
+
const lease = validateOwnedLiveInstallLease(
|
|
1149
|
+
claim.directory,
|
|
1150
|
+
ownerPid,
|
|
1151
|
+
token,
|
|
1152
|
+
);
|
|
1153
|
+
if (!stageValue.startsWith("/")) {
|
|
1154
|
+
throw new Error("local installer wrapper stage must be absolute");
|
|
1155
|
+
}
|
|
1156
|
+
const stagePath = resolve(stageValue);
|
|
1157
|
+
const stageName = basename(stagePath);
|
|
1158
|
+
if (
|
|
1159
|
+
stagePath !== join(claim.directory, stageName)
|
|
1160
|
+
|| !stageName.startsWith(".wrench-install.")
|
|
1161
|
+
|| !installStageNamePattern.test(stageName)
|
|
1162
|
+
) {
|
|
1163
|
+
throw new Error("local installer wrapper stage is not an exact Ghostget stage");
|
|
1164
|
+
}
|
|
1165
|
+
const stage = snapshotManagedInstallStage(claim.directory, stageName);
|
|
1166
|
+
const target = snapshotManagedWrapperIfPresent(
|
|
1167
|
+
claim.directory,
|
|
1168
|
+
"wrench",
|
|
1169
|
+
ghostgetManagedMarker,
|
|
1170
|
+
);
|
|
1171
|
+
pauseBeforeManagedWrapperMutationForTest();
|
|
1172
|
+
assertLeaseUnchanged(lease);
|
|
1173
|
+
const rootBeforeMutation = lstatSync(claim.directory, { bigint: true });
|
|
1174
|
+
if (!sameDirectoryObject(claim.rootStats, rootBeforeMutation)) {
|
|
1175
|
+
throw new Error("local installer recovery directory changed before publication");
|
|
1176
|
+
}
|
|
1177
|
+
const stageBeforeMutation = lstatSync(stage.path, { bigint: true });
|
|
1178
|
+
if (!sameFileIdentity(stage.stats, stageBeforeMutation)) {
|
|
1179
|
+
throw new Error("local installer wrapper stage changed before publication");
|
|
1180
|
+
}
|
|
1181
|
+
if (target !== undefined) assertManagedWrapperUnchanged(target);
|
|
1182
|
+
|
|
1183
|
+
if (target !== undefined && target.bytes.equals(stage.bytes)) {
|
|
1184
|
+
unlinkSync(stage.path);
|
|
1185
|
+
return "already-installed";
|
|
1186
|
+
}
|
|
1187
|
+
renameSync(stage.path, join(claim.directory, "wrench"));
|
|
1188
|
+
const published = snapshotManagedWrapperIfPresent(
|
|
1189
|
+
claim.directory,
|
|
1190
|
+
"wrench",
|
|
1191
|
+
ghostgetManagedMarker,
|
|
1192
|
+
);
|
|
1193
|
+
if (
|
|
1194
|
+
published === undefined
|
|
1195
|
+
|| published.stats.dev !== stage.stats.dev
|
|
1196
|
+
|| published.stats.ino !== stage.stats.ino
|
|
1197
|
+
|| !published.bytes.equals(stage.bytes)
|
|
1198
|
+
) {
|
|
1199
|
+
throw new Error("managed Ghostget wrapper changed during atomic publication");
|
|
1200
|
+
}
|
|
1201
|
+
return "installed";
|
|
1202
|
+
} finally {
|
|
1203
|
+
claim.release();
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/** Remove one exact managed wrapper without delegating unlink to a child. */
|
|
1208
|
+
export function removeManagedInstallWrapper(
|
|
1209
|
+
directoryValue: string,
|
|
1210
|
+
ownerPid: number,
|
|
1211
|
+
token: string,
|
|
1212
|
+
name: "oh" | "wrench",
|
|
1213
|
+
): void {
|
|
1214
|
+
const claim = acquireInstallMutationClaim(directoryValue);
|
|
1215
|
+
if (claim === undefined) {
|
|
1216
|
+
throw new Error("another Ghostget installer lease mutation helper is active");
|
|
1217
|
+
}
|
|
1218
|
+
try {
|
|
1219
|
+
const lease = validateOwnedLiveInstallLease(
|
|
1220
|
+
claim.directory,
|
|
1221
|
+
ownerPid,
|
|
1222
|
+
token,
|
|
1223
|
+
);
|
|
1224
|
+
const target = snapshotManagedWrapperIfPresent(
|
|
1225
|
+
claim.directory,
|
|
1226
|
+
name,
|
|
1227
|
+
name === "wrench" ? ghostgetManagedMarker : predecessorManagedMarker,
|
|
1228
|
+
);
|
|
1229
|
+
if (target === undefined) {
|
|
1230
|
+
throw new Error(`managed ${name} wrapper disappeared before removal`);
|
|
1231
|
+
}
|
|
1232
|
+
pauseBeforeManagedWrapperMutationForTest();
|
|
1233
|
+
assertLeaseUnchanged(lease);
|
|
1234
|
+
const rootBeforeMutation = lstatSync(claim.directory, { bigint: true });
|
|
1235
|
+
if (!sameDirectoryObject(claim.rootStats, rootBeforeMutation)) {
|
|
1236
|
+
throw new Error("local installer recovery directory changed before wrapper removal");
|
|
1237
|
+
}
|
|
1238
|
+
assertManagedWrapperUnchanged(target);
|
|
1239
|
+
unlinkSync(target.path);
|
|
1240
|
+
} finally {
|
|
1241
|
+
claim.release();
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
function recoverInstallStagesUnderClaim(directoryValue: string): number {
|
|
1246
|
+
const { directory, stats: rootBefore } = validateRecoveryDirectory(directoryValue);
|
|
1247
|
+
const lease = snapshotInstallLease(directory);
|
|
1248
|
+
let removeLease = false;
|
|
1249
|
+
if (lease !== undefined) {
|
|
1250
|
+
if (!leaseIsStale(lease)) {
|
|
1251
|
+
throw new Error(
|
|
1252
|
+
`another Ghostget installer is active with process ${lease.record.owner.pid}`,
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
removeLease = true;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
const candidateNames: string[] = [];
|
|
1259
|
+
const handle = opendirSync(directory);
|
|
1260
|
+
try {
|
|
1261
|
+
let entryCount = 0;
|
|
1262
|
+
for (;;) {
|
|
1263
|
+
const entry = handle.readSync();
|
|
1264
|
+
if (entry === null) break;
|
|
1265
|
+
entryCount += 1;
|
|
1266
|
+
if (entryCount > maximumDirectoryEntries) {
|
|
1267
|
+
throw new Error(
|
|
1268
|
+
`local installer recovery directory exceeds ${maximumDirectoryEntries} entries`,
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
if (!installStageNamePattern.test(entry.name)) continue;
|
|
1272
|
+
candidateNames.push(entry.name);
|
|
1273
|
+
if (candidateNames.length > maximumInstallStages) {
|
|
1274
|
+
throw new Error(
|
|
1275
|
+
`local installer recovery found more than ${maximumInstallStages} exact stages`,
|
|
1276
|
+
);
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
} finally {
|
|
1280
|
+
handle.closeSync();
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
candidateNames.sort();
|
|
1284
|
+
const snapshots = candidateNames.map((name) =>
|
|
1285
|
+
snapshotManagedInstallStage(directory, name)
|
|
1286
|
+
);
|
|
1287
|
+
const rootAfterValidation = lstatSync(directory, { bigint: true });
|
|
1288
|
+
if (!sameDirectoryObject(rootBefore, rootAfterValidation)) {
|
|
1289
|
+
throw new Error("local installer recovery directory changed during inspection");
|
|
1290
|
+
}
|
|
1291
|
+
if (lease !== undefined) {
|
|
1292
|
+
assertLeaseUnchanged(lease);
|
|
1293
|
+
if (!leaseIsStale(lease)) {
|
|
1294
|
+
throw new Error(
|
|
1295
|
+
`another Ghostget installer is active with process ${lease.record.owner.pid}`,
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
for (const snapshot of snapshots) {
|
|
1300
|
+
const current = lstatSync(snapshot.path, { bigint: true });
|
|
1301
|
+
if (!sameFileIdentity(snapshot.stats, current)) {
|
|
1302
|
+
throw new Error(
|
|
1303
|
+
`abandoned local installer stage ${snapshot.name} changed before removal`,
|
|
1304
|
+
);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
for (const snapshot of snapshots) unlinkSync(snapshot.path);
|
|
1308
|
+
if (removeLease && lease !== undefined) {
|
|
1309
|
+
assertLeaseUnchanged(lease);
|
|
1310
|
+
unlinkSync(lease.path);
|
|
1311
|
+
}
|
|
1312
|
+
return snapshots.length;
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
/**
|
|
1316
|
+
* Acquire the exact installation lease and recover stages whose prior owner is
|
|
1317
|
+
* proven dead. The symbolic lease makes its complete owner record visible in
|
|
1318
|
+
* one atomic filesystem operation, so no empty-marker race exists.
|
|
1319
|
+
*/
|
|
1320
|
+
export function acquireInstallLease(directoryValue: string, ownerPid: number): string {
|
|
1321
|
+
if (!Number.isSafeInteger(ownerPid) || ownerPid <= 0 || ownerPid > 2_147_483_647) {
|
|
1322
|
+
throw new Error("local installer lease owner PID is invalid");
|
|
1323
|
+
}
|
|
1324
|
+
const claim = acquireInstallMutationClaim(directoryValue);
|
|
1325
|
+
if (claim === undefined) {
|
|
1326
|
+
throw new Error("another Ghostget installer lease mutation helper is active");
|
|
1327
|
+
}
|
|
1328
|
+
const { directory, rootStats } = claim;
|
|
1329
|
+
let publishedLease: InstallLeaseSnapshot | undefined;
|
|
1330
|
+
try {
|
|
1331
|
+
const owner = captureProcessOwnerIdentity(ownerPid);
|
|
1332
|
+
if (processOwnerStatus(owner) !== "exact-live-owner") {
|
|
1333
|
+
throw new Error("local installer lease owner is no longer live");
|
|
1334
|
+
}
|
|
1335
|
+
recoverInstallStagesUnderClaim(directory);
|
|
1336
|
+
const token = randomBytes(24).toString("base64url");
|
|
1337
|
+
const record: InstallLeaseRecord = Object.freeze({
|
|
1338
|
+
schemaVersion: 1,
|
|
1339
|
+
owner,
|
|
1340
|
+
token,
|
|
1341
|
+
});
|
|
1342
|
+
const path = join(directory, installLeaseName);
|
|
1343
|
+
symlinkSync(encodeInstallLease(record), path);
|
|
1344
|
+
const rootAfter = lstatSync(directory, { bigint: true });
|
|
1345
|
+
if (!sameDirectoryObject(rootStats, rootAfter)) {
|
|
1346
|
+
throw new Error("local installer recovery directory changed during lease acquisition");
|
|
1347
|
+
}
|
|
1348
|
+
const lease = snapshotInstallLease(directory);
|
|
1349
|
+
if (
|
|
1350
|
+
lease === undefined
|
|
1351
|
+
|| lease.record.owner.pid !== ownerPid
|
|
1352
|
+
|| lease.record.token !== token
|
|
1353
|
+
|| lease.record.owner.bootId !== owner.bootId
|
|
1354
|
+
|| lease.record.owner.processStartId !== owner.processStartId
|
|
1355
|
+
) {
|
|
1356
|
+
throw new Error("local installer lease changed during acquisition");
|
|
1357
|
+
}
|
|
1358
|
+
publishedLease = lease;
|
|
1359
|
+
if (processOwnerStatus(owner) !== "exact-live-owner") {
|
|
1360
|
+
throw new Error("local installer lease owner died during acquisition");
|
|
1361
|
+
}
|
|
1362
|
+
return token;
|
|
1363
|
+
} catch (error) {
|
|
1364
|
+
if (publishedLease !== undefined) {
|
|
1365
|
+
assertLeaseUnchanged(publishedLease);
|
|
1366
|
+
unlinkSync(publishedLease.path);
|
|
1367
|
+
}
|
|
1368
|
+
throw error;
|
|
1369
|
+
} finally {
|
|
1370
|
+
claim.release();
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
/** Release only the exact lease still owned by this live installer process. */
|
|
1375
|
+
export function releaseInstallLease(
|
|
1376
|
+
directoryValue: string,
|
|
1377
|
+
ownerPid: number,
|
|
1378
|
+
token: string,
|
|
1379
|
+
): void {
|
|
1380
|
+
if (!Number.isSafeInteger(ownerPid) || ownerPid <= 0 || ownerPid > 2_147_483_647) {
|
|
1381
|
+
throw new Error("local installer lease owner PID is invalid");
|
|
1382
|
+
}
|
|
1383
|
+
if (!installLeaseTokenPattern.test(token)) {
|
|
1384
|
+
throw new Error("local installer lease release token is invalid");
|
|
1385
|
+
}
|
|
1386
|
+
const claim = acquireInstallMutationClaim(directoryValue);
|
|
1387
|
+
if (claim === undefined) {
|
|
1388
|
+
throw new Error("another Ghostget installer lease mutation helper is active");
|
|
1389
|
+
}
|
|
1390
|
+
try {
|
|
1391
|
+
const lease = snapshotInstallLease(claim.directory);
|
|
1392
|
+
if (
|
|
1393
|
+
lease === undefined
|
|
1394
|
+
|| lease.record.owner.pid !== ownerPid
|
|
1395
|
+
|| lease.record.token !== token
|
|
1396
|
+
) {
|
|
1397
|
+
throw new Error("local installer lease is not owned by this installer");
|
|
1398
|
+
}
|
|
1399
|
+
if (processOwnerStatus(lease.record.owner) !== "exact-live-owner") {
|
|
1400
|
+
throw new Error("local installer lease release owner is no longer live");
|
|
1401
|
+
}
|
|
1402
|
+
pauseBeforeInstallLeaseRemovalForTest();
|
|
1403
|
+
assertLeaseUnchanged(lease);
|
|
1404
|
+
if (processOwnerStatus(lease.record.owner) !== "exact-live-owner") {
|
|
1405
|
+
throw new Error("local installer lease release owner died before removal");
|
|
1406
|
+
}
|
|
1407
|
+
const rootBeforeRemoval = lstatSync(claim.directory, { bigint: true });
|
|
1408
|
+
if (!sameDirectoryObject(claim.rootStats, rootBeforeRemoval)) {
|
|
1409
|
+
throw new Error("local installer recovery directory changed before lease release");
|
|
1410
|
+
}
|
|
1411
|
+
unlinkSync(lease.path);
|
|
1412
|
+
} finally {
|
|
1413
|
+
claim.release();
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
/**
|
|
1418
|
+
* Remove only exact, inode-stable stages left by a killed Ghostget or Oh local
|
|
1419
|
+
* installer. A live exact installer lease always prevents stage removal.
|
|
1420
|
+
*/
|
|
1421
|
+
export function recoverAbandonedInstallStages(directoryValue: string): number {
|
|
1422
|
+
const claim = acquireInstallMutationClaim(directoryValue);
|
|
1423
|
+
if (claim === undefined) {
|
|
1424
|
+
throw new Error("another Ghostget installer lease mutation helper is active");
|
|
1425
|
+
}
|
|
1426
|
+
try {
|
|
1427
|
+
return recoverInstallStagesUnderClaim(claim.directory);
|
|
1428
|
+
} finally {
|
|
1429
|
+
claim.release();
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
function parseOwnerPid(value: string): number {
|
|
1434
|
+
if (!/^[1-9]\d{0,9}$/u.test(value)) {
|
|
1435
|
+
throw new Error("local installer lease owner PID must be an exact positive integer");
|
|
1436
|
+
}
|
|
1437
|
+
return Number(value);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
function parseManagedWrapperName(value: string): "oh" | "wrench" {
|
|
1441
|
+
if (value !== "oh" && value !== "wrench") {
|
|
1442
|
+
throw new Error("managed local wrapper name must be exactly oh or wrench");
|
|
1443
|
+
}
|
|
1444
|
+
return value;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
if (import.meta.main) {
|
|
1448
|
+
try {
|
|
1449
|
+
const arguments_ = process.argv.slice(2);
|
|
1450
|
+
if (arguments_.length === 1) {
|
|
1451
|
+
recoverAbandonedInstallStages(arguments_[0] ?? "");
|
|
1452
|
+
} else if (arguments_.length === 3 && arguments_[0] === "acquire") {
|
|
1453
|
+
const token = acquireInstallLease(
|
|
1454
|
+
arguments_[1] ?? "",
|
|
1455
|
+
parseOwnerPid(arguments_[2] ?? ""),
|
|
1456
|
+
);
|
|
1457
|
+
process.stdout.write(`${token}\n`);
|
|
1458
|
+
} else if (arguments_.length === 4 && arguments_[0] === "release") {
|
|
1459
|
+
releaseInstallLease(
|
|
1460
|
+
arguments_[1] ?? "",
|
|
1461
|
+
parseOwnerPid(arguments_[2] ?? ""),
|
|
1462
|
+
arguments_[3] ?? "",
|
|
1463
|
+
);
|
|
1464
|
+
} else if (arguments_.length === 5 && arguments_[0] === "publish") {
|
|
1465
|
+
const status = publishManagedInstallWrapper(
|
|
1466
|
+
arguments_[1] ?? "",
|
|
1467
|
+
parseOwnerPid(arguments_[2] ?? ""),
|
|
1468
|
+
arguments_[3] ?? "",
|
|
1469
|
+
arguments_[4] ?? "",
|
|
1470
|
+
);
|
|
1471
|
+
process.stdout.write(`${status}\n`);
|
|
1472
|
+
} else if (arguments_.length === 5 && arguments_[0] === "remove") {
|
|
1473
|
+
removeManagedInstallWrapper(
|
|
1474
|
+
arguments_[1] ?? "",
|
|
1475
|
+
parseOwnerPid(arguments_[2] ?? ""),
|
|
1476
|
+
arguments_[3] ?? "",
|
|
1477
|
+
parseManagedWrapperName(arguments_[4] ?? ""),
|
|
1478
|
+
);
|
|
1479
|
+
process.stdout.write("removed\n");
|
|
1480
|
+
} else {
|
|
1481
|
+
throw new Error(
|
|
1482
|
+
"expected recovery directory or exact acquire/release/publish/remove arguments",
|
|
1483
|
+
);
|
|
1484
|
+
}
|
|
1485
|
+
} catch (error) {
|
|
1486
|
+
process.stderr.write(
|
|
1487
|
+
`ghostget installer: ${error instanceof Error ? error.message : "unknown stage recovery failure"}\n`,
|
|
1488
|
+
);
|
|
1489
|
+
process.exitCode = 1;
|
|
1490
|
+
}
|
|
1491
|
+
}
|