@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,931 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { lstat, realpath } from "node:fs/promises";
|
|
3
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { canonicalJson, sha256 } from "./canonical-json";
|
|
6
|
+
import {
|
|
7
|
+
captureProcessOwnerIdentity,
|
|
8
|
+
currentProcessStartIdentity,
|
|
9
|
+
processOwnerStatus,
|
|
10
|
+
type ProcessOwnerIdentity,
|
|
11
|
+
type ProcessOwnerStatus,
|
|
12
|
+
} from "./process-identity";
|
|
13
|
+
import {
|
|
14
|
+
createPrivateJsonIfAbsent,
|
|
15
|
+
ensurePrivateDirectory,
|
|
16
|
+
readPrivateStateFileIfPresent,
|
|
17
|
+
removePrivateDirectoryTree,
|
|
18
|
+
removePrivateStateFileIfUnchanged,
|
|
19
|
+
snapshotPrivateStateDirectory,
|
|
20
|
+
readPrivateStateFilesBatch,
|
|
21
|
+
ghostgetStateHome,
|
|
22
|
+
writePrivateJsonIfUnchanged,
|
|
23
|
+
} from "./storage";
|
|
24
|
+
|
|
25
|
+
const CLAIM_KIND = "beeper-message-like-me-directory-lease";
|
|
26
|
+
const CLAIM_SCHEMA_VERSION = 2;
|
|
27
|
+
const EXPORT_ADMISSION_KIND = "beeper-message-like-me-export-admission";
|
|
28
|
+
const EXPORT_ADMISSION_SCHEMA_VERSION = 2;
|
|
29
|
+
const EXPORT_ADMISSION_FILE = "active.json";
|
|
30
|
+
const MAX_EXPORT_ADMISSION_ACQUIRE_ATTEMPTS = 8;
|
|
31
|
+
const MAX_CLAIMS = 64;
|
|
32
|
+
const MAX_CLAIM_BYTES = 16 * 1024;
|
|
33
|
+
const UUID_PATTERN =
|
|
34
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
|
|
35
|
+
const DIGEST_PATTERN = /^[a-f0-9]{64}$/u;
|
|
36
|
+
|
|
37
|
+
type LeaseRole = "raw-working" | "bundle-stage";
|
|
38
|
+
type RawPhase = "preparing" | "launching" | "running" | "settled";
|
|
39
|
+
|
|
40
|
+
type LegacyExportAdmissionClaim = Readonly<{
|
|
41
|
+
schemaVersion: 1;
|
|
42
|
+
kind: typeof EXPORT_ADMISSION_KIND;
|
|
43
|
+
id: string;
|
|
44
|
+
owner: ProcessOwnerIdentity;
|
|
45
|
+
}>;
|
|
46
|
+
|
|
47
|
+
export type BeeperMessageLikeMeExportAdmissionPhase =
|
|
48
|
+
| "parent-owned"
|
|
49
|
+
| "helper-launching"
|
|
50
|
+
| "helper-active"
|
|
51
|
+
| "cleanup-unsafe";
|
|
52
|
+
|
|
53
|
+
type LegacyExportAdmissionClaimV2 = Readonly<{
|
|
54
|
+
schemaVersion: 2;
|
|
55
|
+
kind: typeof EXPORT_ADMISSION_KIND;
|
|
56
|
+
id: string;
|
|
57
|
+
owner: ProcessOwnerIdentity;
|
|
58
|
+
helperOwner: ProcessOwnerIdentity | null;
|
|
59
|
+
phase: BeeperMessageLikeMeExportAdmissionPhase;
|
|
60
|
+
}>;
|
|
61
|
+
|
|
62
|
+
type ExportAdmissionClaimV2 = Readonly<{
|
|
63
|
+
schemaVersion: 2;
|
|
64
|
+
kind: typeof EXPORT_ADMISSION_KIND;
|
|
65
|
+
id: string;
|
|
66
|
+
owner: ProcessOwnerIdentity;
|
|
67
|
+
helperOwner: ProcessOwnerIdentity | null;
|
|
68
|
+
phase: BeeperMessageLikeMeExportAdmissionPhase;
|
|
69
|
+
/** Rotated on every lifecycle write so a stale controller cannot win an ABA CAS. */
|
|
70
|
+
revision: string;
|
|
71
|
+
}>;
|
|
72
|
+
|
|
73
|
+
type ExportAdmissionClaim =
|
|
74
|
+
| LegacyExportAdmissionClaim
|
|
75
|
+
| LegacyExportAdmissionClaimV2
|
|
76
|
+
| ExportAdmissionClaimV2;
|
|
77
|
+
|
|
78
|
+
type ExportAdmissionSnapshot = Readonly<{
|
|
79
|
+
claim: ExportAdmissionClaim;
|
|
80
|
+
contentSha256: string;
|
|
81
|
+
}>;
|
|
82
|
+
|
|
83
|
+
export type BeeperMessageLikeMeExportAdmission = {
|
|
84
|
+
readonly claimPath: string;
|
|
85
|
+
readonly environment: Readonly<Record<string, string | undefined>>;
|
|
86
|
+
readonly claimId: string;
|
|
87
|
+
claim: ExportAdmissionClaimV2;
|
|
88
|
+
contentSha256: string;
|
|
89
|
+
released: boolean;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
type LeaseDirectoryIdentity = Readonly<{
|
|
93
|
+
device: string;
|
|
94
|
+
inode: string;
|
|
95
|
+
/** Stable across rename and distinct across inode reuse. */
|
|
96
|
+
birthtimeNs: string;
|
|
97
|
+
}>;
|
|
98
|
+
|
|
99
|
+
type DirectoryLeaseClaim = Readonly<{
|
|
100
|
+
schemaVersion: 2;
|
|
101
|
+
kind: typeof CLAIM_KIND;
|
|
102
|
+
id: string;
|
|
103
|
+
role: LeaseRole;
|
|
104
|
+
path: string;
|
|
105
|
+
parentPath: string;
|
|
106
|
+
parentIdentity: LeaseDirectoryIdentity;
|
|
107
|
+
directoryIdentity: LeaseDirectoryIdentity;
|
|
108
|
+
outputRoot: string | null;
|
|
109
|
+
owner: ProcessOwnerIdentity;
|
|
110
|
+
childOwner: ProcessOwnerIdentity | null;
|
|
111
|
+
phase: RawPhase;
|
|
112
|
+
createdAtMs: number;
|
|
113
|
+
recoverAfterMs: number;
|
|
114
|
+
}>;
|
|
115
|
+
|
|
116
|
+
export type BeeperMessageLikeMeDirectoryLease = {
|
|
117
|
+
readonly claimPath: string;
|
|
118
|
+
readonly environment: Readonly<Record<string, string | undefined>>;
|
|
119
|
+
claim: DirectoryLeaseClaim;
|
|
120
|
+
contentSha256: string;
|
|
121
|
+
released: boolean;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export type BeeperMessageLikeMeRecoveryReport = Readonly<{
|
|
125
|
+
recovered: number;
|
|
126
|
+
published: number;
|
|
127
|
+
active: number;
|
|
128
|
+
indeterminate: number;
|
|
129
|
+
}>;
|
|
130
|
+
|
|
131
|
+
function fail(message: string): never {
|
|
132
|
+
throw new Error(`Beeper Message Like Me recovery: ${message}`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isErrno(error: unknown, code: string): boolean {
|
|
136
|
+
return typeof error === "object" && error !== null && "code" in error
|
|
137
|
+
&& (error as { readonly code?: unknown }).code === code;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function record(value: unknown, label: string): Readonly<Record<string, unknown>> {
|
|
141
|
+
if (
|
|
142
|
+
typeof value !== "object"
|
|
143
|
+
|| value === null
|
|
144
|
+
|| Array.isArray(value)
|
|
145
|
+
|| Object.getPrototypeOf(value) !== Object.prototype
|
|
146
|
+
) return fail(`${label} must be a plain object`);
|
|
147
|
+
return value as Readonly<Record<string, unknown>>;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function exactKeys(
|
|
151
|
+
value: Readonly<Record<string, unknown>>,
|
|
152
|
+
keys: readonly string[],
|
|
153
|
+
label: string,
|
|
154
|
+
): void {
|
|
155
|
+
const actual = Object.keys(value).sort();
|
|
156
|
+
const expected = [...keys].sort();
|
|
157
|
+
if (
|
|
158
|
+
actual.length !== expected.length
|
|
159
|
+
|| actual.some((key, index) => key !== expected[index])
|
|
160
|
+
) fail(`${label} has an unsupported shape`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function boundedPath(value: unknown, label: string): string {
|
|
164
|
+
if (
|
|
165
|
+
typeof value !== "string"
|
|
166
|
+
|| !isAbsolute(value)
|
|
167
|
+
|| resolve(value) !== value
|
|
168
|
+
|| value.length > 4_096
|
|
169
|
+
|| /[\0\r\n]/u.test(value)
|
|
170
|
+
) return fail(`${label} is invalid`);
|
|
171
|
+
return value;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function identity(value: unknown, label: string): LeaseDirectoryIdentity {
|
|
175
|
+
const source = record(value, label);
|
|
176
|
+
exactKeys(source, ["birthtimeNs", "device", "inode"], label);
|
|
177
|
+
if (
|
|
178
|
+
typeof source.device !== "string"
|
|
179
|
+
|| !/^\d{1,40}$/u.test(source.device)
|
|
180
|
+
|| typeof source.inode !== "string"
|
|
181
|
+
|| !/^\d{1,40}$/u.test(source.inode)
|
|
182
|
+
|| typeof source.birthtimeNs !== "string"
|
|
183
|
+
|| !/^[1-9]\d{0,39}$/u.test(source.birthtimeNs)
|
|
184
|
+
) return fail(`${label} is invalid`);
|
|
185
|
+
return Object.freeze({
|
|
186
|
+
device: source.device,
|
|
187
|
+
inode: source.inode,
|
|
188
|
+
birthtimeNs: source.birthtimeNs,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function owner(value: unknown, label: string): ProcessOwnerIdentity {
|
|
193
|
+
const source = record(value, label);
|
|
194
|
+
exactKeys(source, ["bootId", "pid", "processStartId"], label);
|
|
195
|
+
if (
|
|
196
|
+
typeof source.pid !== "number"
|
|
197
|
+
|| !Number.isSafeInteger(source.pid)
|
|
198
|
+
|| source.pid < 1
|
|
199
|
+
|| source.pid > 2_147_483_647
|
|
200
|
+
|| typeof source.bootId !== "string"
|
|
201
|
+
|| !DIGEST_PATTERN.test(source.bootId)
|
|
202
|
+
|| typeof source.processStartId !== "string"
|
|
203
|
+
|| !DIGEST_PATTERN.test(source.processStartId)
|
|
204
|
+
) return fail(`${label} is invalid`);
|
|
205
|
+
return Object.freeze({
|
|
206
|
+
pid: source.pid,
|
|
207
|
+
bootId: source.bootId,
|
|
208
|
+
processStartId: source.processStartId,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function parseExportAdmission(value: unknown): ExportAdmissionClaim {
|
|
213
|
+
const source = record(value, "export admission");
|
|
214
|
+
const legacy = source.schemaVersion === 1;
|
|
215
|
+
const currentV2 = source.schemaVersion === 2 && Object.hasOwn(source, "revision");
|
|
216
|
+
exactKeys(
|
|
217
|
+
source,
|
|
218
|
+
legacy
|
|
219
|
+
? ["id", "kind", "owner", "schemaVersion"]
|
|
220
|
+
: currentV2
|
|
221
|
+
? ["helperOwner", "id", "kind", "owner", "phase", "revision", "schemaVersion"]
|
|
222
|
+
: ["helperOwner", "id", "kind", "owner", "phase", "schemaVersion"],
|
|
223
|
+
"export admission",
|
|
224
|
+
);
|
|
225
|
+
if (
|
|
226
|
+
(source.schemaVersion !== 1 && source.schemaVersion !== EXPORT_ADMISSION_SCHEMA_VERSION)
|
|
227
|
+
|| source.kind !== EXPORT_ADMISSION_KIND
|
|
228
|
+
|| typeof source.id !== "string"
|
|
229
|
+
|| !UUID_PATTERN.test(source.id)
|
|
230
|
+
) return fail("export admission identity is invalid");
|
|
231
|
+
if (legacy) {
|
|
232
|
+
return Object.freeze({
|
|
233
|
+
schemaVersion: 1 as const,
|
|
234
|
+
kind: EXPORT_ADMISSION_KIND,
|
|
235
|
+
id: source.id,
|
|
236
|
+
owner: owner(source.owner, "export admission owner"),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if (
|
|
240
|
+
!["parent-owned", "helper-launching", "helper-active", "cleanup-unsafe"].includes(
|
|
241
|
+
typeof source.phase === "string" ? source.phase : "",
|
|
242
|
+
)
|
|
243
|
+
|| (source.phase === "helper-active" && source.helperOwner === null)
|
|
244
|
+
|| ((source.phase === "parent-owned" || source.phase === "helper-launching")
|
|
245
|
+
&& source.helperOwner !== null)
|
|
246
|
+
) return fail("export admission lifecycle is invalid");
|
|
247
|
+
const parsed = {
|
|
248
|
+
schemaVersion: 2 as const,
|
|
249
|
+
kind: EXPORT_ADMISSION_KIND,
|
|
250
|
+
id: source.id,
|
|
251
|
+
owner: owner(source.owner, "export admission owner"),
|
|
252
|
+
helperOwner: source.helperOwner === null
|
|
253
|
+
? null
|
|
254
|
+
: owner(source.helperOwner, "export admission helper owner"),
|
|
255
|
+
phase: source.phase as BeeperMessageLikeMeExportAdmissionPhase,
|
|
256
|
+
} as const;
|
|
257
|
+
if (!currentV2) return Object.freeze(parsed);
|
|
258
|
+
if (typeof source.revision !== "string" || !UUID_PATTERN.test(source.revision)) {
|
|
259
|
+
return fail("export admission revision is invalid");
|
|
260
|
+
}
|
|
261
|
+
return Object.freeze({ ...parsed, revision: source.revision });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function milliseconds(value: unknown, label: string): number {
|
|
265
|
+
if (
|
|
266
|
+
typeof value !== "number"
|
|
267
|
+
|| !Number.isSafeInteger(value)
|
|
268
|
+
|| value < 0
|
|
269
|
+
) return fail(`${label} is invalid`);
|
|
270
|
+
return value;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function parseClaim(value: unknown): DirectoryLeaseClaim {
|
|
274
|
+
const source = record(value, "directory lease");
|
|
275
|
+
exactKeys(source, [
|
|
276
|
+
"childOwner",
|
|
277
|
+
"createdAtMs",
|
|
278
|
+
"directoryIdentity",
|
|
279
|
+
"id",
|
|
280
|
+
"kind",
|
|
281
|
+
"outputRoot",
|
|
282
|
+
"owner",
|
|
283
|
+
"parentIdentity",
|
|
284
|
+
"parentPath",
|
|
285
|
+
"path",
|
|
286
|
+
"phase",
|
|
287
|
+
"recoverAfterMs",
|
|
288
|
+
"role",
|
|
289
|
+
"schemaVersion",
|
|
290
|
+
], "directory lease");
|
|
291
|
+
if (
|
|
292
|
+
source.schemaVersion !== CLAIM_SCHEMA_VERSION
|
|
293
|
+
|| source.kind !== CLAIM_KIND
|
|
294
|
+
|| typeof source.id !== "string"
|
|
295
|
+
|| !UUID_PATTERN.test(source.id)
|
|
296
|
+
|| (source.role !== "raw-working" && source.role !== "bundle-stage")
|
|
297
|
+
|| !["preparing", "launching", "running", "settled"].includes(
|
|
298
|
+
typeof source.phase === "string" ? source.phase : "",
|
|
299
|
+
)
|
|
300
|
+
) return fail("directory lease identity is invalid");
|
|
301
|
+
const path = boundedPath(source.path, "directory lease path");
|
|
302
|
+
const parentPath = boundedPath(source.parentPath, "directory lease parent");
|
|
303
|
+
const outputRoot = source.outputRoot === null
|
|
304
|
+
? null
|
|
305
|
+
: boundedPath(source.outputRoot, "directory lease output");
|
|
306
|
+
const createdAtMs = milliseconds(source.createdAtMs, "directory lease creation time");
|
|
307
|
+
const recoverAfterMs = milliseconds(
|
|
308
|
+
source.recoverAfterMs,
|
|
309
|
+
"directory lease recovery time",
|
|
310
|
+
);
|
|
311
|
+
if (
|
|
312
|
+
dirname(path) !== parentPath
|
|
313
|
+
|| recoverAfterMs < createdAtMs
|
|
314
|
+
|| (source.role === "bundle-stage") !== (outputRoot !== null)
|
|
315
|
+
|| (outputRoot !== null && dirname(outputRoot) !== parentPath)
|
|
316
|
+
|| (source.role === "bundle-stage" && source.phase !== "preparing")
|
|
317
|
+
|| (source.role === "bundle-stage" && source.childOwner !== null)
|
|
318
|
+
|| (source.phase === "running") !== (source.childOwner !== null)
|
|
319
|
+
) return fail("directory lease coordinates are inconsistent");
|
|
320
|
+
return Object.freeze({
|
|
321
|
+
schemaVersion: 2,
|
|
322
|
+
kind: CLAIM_KIND,
|
|
323
|
+
id: source.id,
|
|
324
|
+
role: source.role,
|
|
325
|
+
path,
|
|
326
|
+
parentPath,
|
|
327
|
+
parentIdentity: identity(source.parentIdentity, "directory lease parent identity"),
|
|
328
|
+
directoryIdentity: identity(
|
|
329
|
+
source.directoryIdentity,
|
|
330
|
+
"directory lease directory identity",
|
|
331
|
+
),
|
|
332
|
+
outputRoot,
|
|
333
|
+
owner: owner(source.owner, "directory lease owner"),
|
|
334
|
+
childOwner: source.childOwner === null
|
|
335
|
+
? null
|
|
336
|
+
: owner(source.childOwner, "directory lease child owner"),
|
|
337
|
+
phase: source.phase as RawPhase,
|
|
338
|
+
createdAtMs,
|
|
339
|
+
recoverAfterMs,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function claimBytes(claim: DirectoryLeaseClaim): string {
|
|
344
|
+
return `${canonicalJson(claim)}\n`;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function claimSha256(claim: DirectoryLeaseClaim): string {
|
|
348
|
+
return sha256(claimBytes(claim));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function leaseRoot(
|
|
352
|
+
environment: Readonly<Record<string, string | undefined>>,
|
|
353
|
+
): string {
|
|
354
|
+
return join(
|
|
355
|
+
ghostgetStateHome(environment),
|
|
356
|
+
"recovery",
|
|
357
|
+
"beeper-message-like-me-directory-leases",
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function exportAdmissionRoot(
|
|
362
|
+
environment: Readonly<Record<string, string | undefined>>,
|
|
363
|
+
): string {
|
|
364
|
+
return join(
|
|
365
|
+
ghostgetStateHome(environment),
|
|
366
|
+
"recovery",
|
|
367
|
+
"beeper-message-like-me-export-admission",
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function exportAdmissionBytes(claim: ExportAdmissionClaim): string {
|
|
372
|
+
return `${canonicalJson(claim)}\n`;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function readExportAdmission(
|
|
376
|
+
root: string,
|
|
377
|
+
environment: Readonly<Record<string, string | undefined>>,
|
|
378
|
+
): ExportAdmissionSnapshot | null {
|
|
379
|
+
const content = readPrivateStateFileIfPresent(
|
|
380
|
+
join(root, EXPORT_ADMISSION_FILE),
|
|
381
|
+
MAX_CLAIM_BYTES,
|
|
382
|
+
"Beeper Message Like Me export admission",
|
|
383
|
+
environment,
|
|
384
|
+
);
|
|
385
|
+
if (content === null) return null;
|
|
386
|
+
let decoded: unknown;
|
|
387
|
+
try {
|
|
388
|
+
decoded = JSON.parse(content) as unknown;
|
|
389
|
+
} catch {
|
|
390
|
+
return fail("export admission is not valid JSON");
|
|
391
|
+
}
|
|
392
|
+
const claim = parseExportAdmission(decoded);
|
|
393
|
+
if (content !== exportAdmissionBytes(claim)) {
|
|
394
|
+
return fail("export admission is not canonical");
|
|
395
|
+
}
|
|
396
|
+
return Object.freeze({ claim, contentSha256: sha256(content) });
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function stateMutationCreateContention(error: unknown): boolean {
|
|
400
|
+
return error instanceof Error
|
|
401
|
+
&& error.message.includes("state file mutation is already active");
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function acquiredExportAdmission(
|
|
405
|
+
root: string,
|
|
406
|
+
environment: Readonly<Record<string, string | undefined>>,
|
|
407
|
+
snapshot: ExportAdmissionSnapshot,
|
|
408
|
+
): BeeperMessageLikeMeExportAdmission {
|
|
409
|
+
if (snapshot.claim.schemaVersion !== 2 || !("revision" in snapshot.claim)) {
|
|
410
|
+
return fail("a legacy export admission cannot be acquired as current ownership");
|
|
411
|
+
}
|
|
412
|
+
return {
|
|
413
|
+
claimPath: join(root, EXPORT_ADMISSION_FILE),
|
|
414
|
+
environment,
|
|
415
|
+
claimId: snapshot.claim.id,
|
|
416
|
+
claim: snapshot.claim,
|
|
417
|
+
contentSha256: snapshot.contentSha256,
|
|
418
|
+
released: false,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function exportAdmissionDisposition(
|
|
423
|
+
claim: ExportAdmissionClaim,
|
|
424
|
+
inspectOwner: (owner: ProcessOwnerIdentity) => ProcessOwnerStatus,
|
|
425
|
+
currentBootId: string,
|
|
426
|
+
): "active" | "indeterminate" | "recoverable" {
|
|
427
|
+
const parent = inspectOwner(claim.owner);
|
|
428
|
+
if (parent === "exact-live-owner") return "active";
|
|
429
|
+
if (parent === "unknown") return "indeterminate";
|
|
430
|
+
if (claim.schemaVersion === 1) {
|
|
431
|
+
// The released v1 admission recorded only the parent even though its
|
|
432
|
+
// detached per-page helper could outlive that parent. Only a kernel reboot
|
|
433
|
+
// proves that an unrecorded legacy helper cannot still be running.
|
|
434
|
+
return claim.owner.bootId === currentBootId ? "indeterminate" : "recoverable";
|
|
435
|
+
}
|
|
436
|
+
if (
|
|
437
|
+
claim.helperOwner !== null
|
|
438
|
+
&& claim.helperOwner.bootId !== claim.owner.bootId
|
|
439
|
+
) return "indeterminate";
|
|
440
|
+
if (claim.phase === "cleanup-unsafe") {
|
|
441
|
+
// cleanup-unsafe means the recorded helper is not a complete ownership
|
|
442
|
+
// proof: a descendant or process-group member may have survived after the
|
|
443
|
+
// recorded child exited. Only a reboot proves every such process is gone.
|
|
444
|
+
return claim.owner.bootId === currentBootId ? "indeterminate" : "recoverable";
|
|
445
|
+
}
|
|
446
|
+
if (claim.phase === "parent-owned") return "recoverable";
|
|
447
|
+
// Only a reboot proves that an unjoined helper, its streams, and its process
|
|
448
|
+
// group cannot still retain private work after the supervising parent exits.
|
|
449
|
+
if (claim.owner.bootId !== currentBootId) return "recoverable";
|
|
450
|
+
if (claim.helperOwner === null) return "indeterminate";
|
|
451
|
+
const helper = inspectOwner(claim.helperOwner);
|
|
452
|
+
return helper === "exact-live-owner"
|
|
453
|
+
? "active"
|
|
454
|
+
: "indeterminate";
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function acquireBeeperMessageLikeMeExportAdmission(options: Readonly<{
|
|
458
|
+
environment?: Readonly<Record<string, string | undefined>>;
|
|
459
|
+
/** Test-only process-liveness seam. */
|
|
460
|
+
inspectOwnerForTest?: (owner: ProcessOwnerIdentity) => ProcessOwnerStatus;
|
|
461
|
+
/** Test-only exact kernel boot identity seam. */
|
|
462
|
+
currentBootIdForTest?: string;
|
|
463
|
+
}> = {}): BeeperMessageLikeMeExportAdmission {
|
|
464
|
+
if (
|
|
465
|
+
(options.inspectOwnerForTest !== undefined || options.currentBootIdForTest !== undefined)
|
|
466
|
+
&& process.env.NODE_ENV !== "test"
|
|
467
|
+
) return fail("export admission liveness injection is available only in tests");
|
|
468
|
+
const environment = options.environment ?? process.env;
|
|
469
|
+
const inspectOwner = options.inspectOwnerForTest ?? processOwnerStatus;
|
|
470
|
+
const currentBootId = options.currentBootIdForTest
|
|
471
|
+
?? currentProcessStartIdentity().bootId;
|
|
472
|
+
if (!DIGEST_PATTERN.test(currentBootId)) return fail("current boot identity is invalid");
|
|
473
|
+
const root = exportAdmissionRoot(environment);
|
|
474
|
+
try {
|
|
475
|
+
ensurePrivateDirectory(root);
|
|
476
|
+
for (
|
|
477
|
+
let attempt = 0;
|
|
478
|
+
attempt < MAX_EXPORT_ADMISSION_ACQUIRE_ATTEMPTS;
|
|
479
|
+
attempt += 1
|
|
480
|
+
) {
|
|
481
|
+
const observed = readExportAdmission(root, environment);
|
|
482
|
+
if (observed !== null) {
|
|
483
|
+
const disposition = exportAdmissionDisposition(
|
|
484
|
+
observed.claim,
|
|
485
|
+
inspectOwner,
|
|
486
|
+
currentBootId,
|
|
487
|
+
);
|
|
488
|
+
if (disposition === "active") {
|
|
489
|
+
return fail("another export is active");
|
|
490
|
+
}
|
|
491
|
+
if (disposition === "indeterminate") {
|
|
492
|
+
return fail("prior export owner cannot be inspected safely");
|
|
493
|
+
}
|
|
494
|
+
if (!removePrivateStateFileIfUnchanged(
|
|
495
|
+
join(root, EXPORT_ADMISSION_FILE),
|
|
496
|
+
{ expectedCurrentContentSha256: observed.contentSha256 },
|
|
497
|
+
environment,
|
|
498
|
+
)) continue;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const processIdentity = currentProcessStartIdentity();
|
|
502
|
+
const claim = Object.freeze({
|
|
503
|
+
schemaVersion: 2 as const,
|
|
504
|
+
kind: EXPORT_ADMISSION_KIND,
|
|
505
|
+
id: randomUUID(),
|
|
506
|
+
owner: Object.freeze({ pid: process.pid, ...processIdentity }),
|
|
507
|
+
helperOwner: null,
|
|
508
|
+
phase: "parent-owned" as const,
|
|
509
|
+
revision: randomUUID(),
|
|
510
|
+
});
|
|
511
|
+
const snapshot = Object.freeze({
|
|
512
|
+
claim,
|
|
513
|
+
contentSha256: sha256(exportAdmissionBytes(claim)),
|
|
514
|
+
});
|
|
515
|
+
let created = false;
|
|
516
|
+
try {
|
|
517
|
+
created = createPrivateJsonIfAbsent(
|
|
518
|
+
join(root, EXPORT_ADMISSION_FILE),
|
|
519
|
+
claim,
|
|
520
|
+
{ environment },
|
|
521
|
+
).created;
|
|
522
|
+
} catch (error) {
|
|
523
|
+
const committed = readExportAdmission(root, environment);
|
|
524
|
+
if (
|
|
525
|
+
committed !== null
|
|
526
|
+
&& committed.contentSha256 === snapshot.contentSha256
|
|
527
|
+
) return acquiredExportAdmission(root, environment, snapshot);
|
|
528
|
+
if (!stateMutationCreateContention(error)) throw error;
|
|
529
|
+
}
|
|
530
|
+
if (created) return acquiredExportAdmission(root, environment, snapshot);
|
|
531
|
+
}
|
|
532
|
+
return fail("export admission could not be acquired after bounded contention");
|
|
533
|
+
} catch (error) {
|
|
534
|
+
if (
|
|
535
|
+
error instanceof Error
|
|
536
|
+
&& error.message.startsWith("Beeper Message Like Me recovery:")
|
|
537
|
+
) throw error;
|
|
538
|
+
return fail("export admission could not be acquired safely");
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function updateExportAdmission(
|
|
543
|
+
admission: BeeperMessageLikeMeExportAdmission,
|
|
544
|
+
phase: BeeperMessageLikeMeExportAdmissionPhase,
|
|
545
|
+
helperOwner: ProcessOwnerIdentity | null,
|
|
546
|
+
): void {
|
|
547
|
+
if (admission.released) return fail("released export admission cannot change lifecycle");
|
|
548
|
+
const current = admission.claim.phase;
|
|
549
|
+
const valid =
|
|
550
|
+
(phase === "helper-launching" && current === "parent-owned" && helperOwner === null)
|
|
551
|
+
|| (phase === "helper-active" && current === "helper-launching" && helperOwner !== null)
|
|
552
|
+
|| (phase === "parent-owned"
|
|
553
|
+
&& (current === "helper-launching" || current === "helper-active")
|
|
554
|
+
&& helperOwner === null)
|
|
555
|
+
|| (phase === "cleanup-unsafe"
|
|
556
|
+
&& current !== "parent-owned"
|
|
557
|
+
&& (helperOwner === null || current === "helper-active"));
|
|
558
|
+
if (!valid) return fail("export admission lifecycle transition is invalid");
|
|
559
|
+
const next = Object.freeze({
|
|
560
|
+
...admission.claim,
|
|
561
|
+
phase,
|
|
562
|
+
helperOwner,
|
|
563
|
+
revision: randomUUID(),
|
|
564
|
+
});
|
|
565
|
+
try {
|
|
566
|
+
if (!writePrivateJsonIfUnchanged(admission.claimPath, next, {
|
|
567
|
+
expectedCurrentContentSha256: admission.contentSha256,
|
|
568
|
+
})) return fail("export admission changed before lifecycle update");
|
|
569
|
+
admission.claim = next;
|
|
570
|
+
admission.contentSha256 = sha256(exportAdmissionBytes(next));
|
|
571
|
+
} catch (error) {
|
|
572
|
+
if (error instanceof Error && error.message.startsWith("Beeper Message Like Me recovery:")) {
|
|
573
|
+
throw error;
|
|
574
|
+
}
|
|
575
|
+
return fail("export admission lifecycle could not be updated safely");
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
export function beginBeeperMessageLikeMeHelperLaunch(
|
|
580
|
+
admission: BeeperMessageLikeMeExportAdmission,
|
|
581
|
+
): void {
|
|
582
|
+
updateExportAdmission(admission, "helper-launching", null);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
export function bindBeeperMessageLikeMeHelperOwner(
|
|
586
|
+
admission: BeeperMessageLikeMeExportAdmission,
|
|
587
|
+
childPid: number,
|
|
588
|
+
): void {
|
|
589
|
+
if (!Number.isSafeInteger(childPid) || childPid < 1 || childPid > 2_147_483_647) {
|
|
590
|
+
return fail("export admission helper PID is invalid");
|
|
591
|
+
}
|
|
592
|
+
updateExportAdmission(admission, "helper-active", captureProcessOwnerIdentity(childPid));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
export function settleBeeperMessageLikeMeHelper(
|
|
596
|
+
admission: BeeperMessageLikeMeExportAdmission,
|
|
597
|
+
): void {
|
|
598
|
+
updateExportAdmission(admission, "parent-owned", null);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
export function markBeeperMessageLikeMeHelperCleanupUnsafe(
|
|
602
|
+
admission: BeeperMessageLikeMeExportAdmission,
|
|
603
|
+
): void {
|
|
604
|
+
updateExportAdmission(admission, "cleanup-unsafe", admission.claim.helperOwner);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
export function releaseBeeperMessageLikeMeExportAdmission(
|
|
608
|
+
admission: BeeperMessageLikeMeExportAdmission,
|
|
609
|
+
): void {
|
|
610
|
+
if (admission.released) return;
|
|
611
|
+
if (admission.claim.phase !== "parent-owned" || admission.claim.helperOwner !== null) {
|
|
612
|
+
return fail("export admission cannot release an unsettled helper lifecycle");
|
|
613
|
+
}
|
|
614
|
+
try {
|
|
615
|
+
if (!removePrivateStateFileIfUnchanged(admission.claimPath, {
|
|
616
|
+
expectedCurrentContentSha256: admission.contentSha256,
|
|
617
|
+
}, admission.environment)) return fail("export admission changed before release");
|
|
618
|
+
admission.released = true;
|
|
619
|
+
} catch (error) {
|
|
620
|
+
if (
|
|
621
|
+
error instanceof Error
|
|
622
|
+
&& error.message.startsWith("Beeper Message Like Me recovery:")
|
|
623
|
+
) throw error;
|
|
624
|
+
return fail("export admission could not be released safely");
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
async function physicalDirectoryIdentity(
|
|
629
|
+
path: string,
|
|
630
|
+
privateMode: boolean,
|
|
631
|
+
): Promise<LeaseDirectoryIdentity> {
|
|
632
|
+
const canonical = await realpath(path);
|
|
633
|
+
const metadata = await lstat(path, { bigint: true });
|
|
634
|
+
const uid = process.getuid?.();
|
|
635
|
+
if (
|
|
636
|
+
canonical !== path
|
|
637
|
+
|| !metadata.isDirectory()
|
|
638
|
+
|| metadata.isSymbolicLink()
|
|
639
|
+
|| uid === undefined
|
|
640
|
+
|| metadata.uid !== BigInt(uid)
|
|
641
|
+
|| metadata.birthtimeNs <= 0n
|
|
642
|
+
|| (privateMode
|
|
643
|
+
? (metadata.mode & 0o777n) !== 0o700n
|
|
644
|
+
: (metadata.mode & 0o022n) !== 0n)
|
|
645
|
+
) return fail("directory lease target is not an owned physical directory");
|
|
646
|
+
return Object.freeze({
|
|
647
|
+
device: metadata.dev.toString(),
|
|
648
|
+
inode: metadata.ino.toString(),
|
|
649
|
+
birthtimeNs: metadata.birthtimeNs.toString(),
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function sameIdentity(
|
|
654
|
+
left: LeaseDirectoryIdentity,
|
|
655
|
+
right: LeaseDirectoryIdentity,
|
|
656
|
+
): boolean {
|
|
657
|
+
return left.device === right.device
|
|
658
|
+
&& left.inode === right.inode
|
|
659
|
+
&& left.birthtimeNs === right.birthtimeNs;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
export async function createBeeperMessageLikeMeDirectoryLease(request: Readonly<{
|
|
663
|
+
role: LeaseRole;
|
|
664
|
+
path: string;
|
|
665
|
+
outputRoot?: string;
|
|
666
|
+
recoverAfterMs: number;
|
|
667
|
+
environment?: Readonly<Record<string, string | undefined>>;
|
|
668
|
+
nowMs?: number;
|
|
669
|
+
}>): Promise<BeeperMessageLikeMeDirectoryLease> {
|
|
670
|
+
const environment = request.environment ?? process.env;
|
|
671
|
+
const path = boundedPath(request.path, "directory lease path");
|
|
672
|
+
const parentPath = dirname(path);
|
|
673
|
+
const outputRoot = request.outputRoot === undefined
|
|
674
|
+
? null
|
|
675
|
+
: boundedPath(request.outputRoot, "directory lease output");
|
|
676
|
+
const nowMs = milliseconds(request.nowMs ?? Date.now(), "directory lease creation time");
|
|
677
|
+
if (
|
|
678
|
+
!Number.isSafeInteger(request.recoverAfterMs)
|
|
679
|
+
|| request.recoverAfterMs < nowMs
|
|
680
|
+
|| (request.role === "bundle-stage") !== (outputRoot !== null)
|
|
681
|
+
|| (outputRoot !== null && dirname(outputRoot) !== parentPath)
|
|
682
|
+
) return fail("directory lease request is invalid");
|
|
683
|
+
const processIdentity = currentProcessStartIdentity();
|
|
684
|
+
const claim = Object.freeze({
|
|
685
|
+
schemaVersion: 2 as const,
|
|
686
|
+
kind: CLAIM_KIND,
|
|
687
|
+
id: randomUUID(),
|
|
688
|
+
role: request.role,
|
|
689
|
+
path,
|
|
690
|
+
parentPath,
|
|
691
|
+
parentIdentity: await physicalDirectoryIdentity(parentPath, false),
|
|
692
|
+
directoryIdentity: await physicalDirectoryIdentity(path, true),
|
|
693
|
+
outputRoot,
|
|
694
|
+
owner: Object.freeze({ pid: process.pid, ...processIdentity }),
|
|
695
|
+
childOwner: null,
|
|
696
|
+
phase: "preparing" as const,
|
|
697
|
+
createdAtMs: nowMs,
|
|
698
|
+
recoverAfterMs: request.recoverAfterMs,
|
|
699
|
+
});
|
|
700
|
+
const root = leaseRoot(environment);
|
|
701
|
+
try {
|
|
702
|
+
ensurePrivateDirectory(root);
|
|
703
|
+
const claimPath = join(root, `${claim.id}.json`);
|
|
704
|
+
if (!createPrivateJsonIfAbsent(claimPath, claim, { environment }).created) {
|
|
705
|
+
return fail("directory lease ID was already present");
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
claimPath,
|
|
709
|
+
environment,
|
|
710
|
+
claim,
|
|
711
|
+
contentSha256: claimSha256(claim),
|
|
712
|
+
released: false,
|
|
713
|
+
};
|
|
714
|
+
} catch (error) {
|
|
715
|
+
if (
|
|
716
|
+
error instanceof Error
|
|
717
|
+
&& error.message.startsWith("Beeper Message Like Me recovery:")
|
|
718
|
+
) throw error;
|
|
719
|
+
return fail("directory lease could not be created safely");
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export function updateBeeperMessageLikeMeDirectoryLease(
|
|
724
|
+
lease: BeeperMessageLikeMeDirectoryLease,
|
|
725
|
+
phase: RawPhase,
|
|
726
|
+
childPid?: number,
|
|
727
|
+
): void {
|
|
728
|
+
if (lease.released || lease.claim.role !== "raw-working") {
|
|
729
|
+
return fail("only an active raw directory lease can change phase");
|
|
730
|
+
}
|
|
731
|
+
const currentPhase = lease.claim.phase;
|
|
732
|
+
const transitionIsValid =
|
|
733
|
+
(phase === "launching"
|
|
734
|
+
&& (currentPhase === "preparing" || currentPhase === "settled"))
|
|
735
|
+
|| (phase === "running" && currentPhase === "launching")
|
|
736
|
+
|| (
|
|
737
|
+
phase === "settled"
|
|
738
|
+
&& (currentPhase === "launching" || currentPhase === "running")
|
|
739
|
+
);
|
|
740
|
+
if (!transitionIsValid) {
|
|
741
|
+
return fail("raw directory lease lifecycle transition is invalid");
|
|
742
|
+
}
|
|
743
|
+
let childOwner: ProcessOwnerIdentity | null = null;
|
|
744
|
+
if (phase === "running") {
|
|
745
|
+
if (
|
|
746
|
+
childPid === undefined
|
|
747
|
+
|| !Number.isSafeInteger(childPid)
|
|
748
|
+
|| childPid < 1
|
|
749
|
+
|| childPid > 2_147_483_647
|
|
750
|
+
) return fail("running directory lease requires a child process identity");
|
|
751
|
+
childOwner = captureProcessOwnerIdentity(childPid);
|
|
752
|
+
} else if (childPid !== undefined) {
|
|
753
|
+
return fail("only a running directory lease accepts a child process identity");
|
|
754
|
+
}
|
|
755
|
+
const next = Object.freeze({ ...lease.claim, phase, childOwner });
|
|
756
|
+
try {
|
|
757
|
+
if (!writePrivateJsonIfUnchanged(lease.claimPath, next, {
|
|
758
|
+
expectedCurrentContentSha256: lease.contentSha256,
|
|
759
|
+
})) return fail("directory lease changed before its lifecycle update");
|
|
760
|
+
lease.claim = next;
|
|
761
|
+
lease.contentSha256 = claimSha256(next);
|
|
762
|
+
} catch (error) {
|
|
763
|
+
if (
|
|
764
|
+
error instanceof Error
|
|
765
|
+
&& error.message.startsWith("Beeper Message Like Me recovery:")
|
|
766
|
+
) throw error;
|
|
767
|
+
return fail("directory lease lifecycle could not be updated safely");
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
export function releaseBeeperMessageLikeMeDirectoryLease(
|
|
772
|
+
lease: BeeperMessageLikeMeDirectoryLease,
|
|
773
|
+
): void {
|
|
774
|
+
if (lease.released) return;
|
|
775
|
+
try {
|
|
776
|
+
if (!removePrivateStateFileIfUnchanged(lease.claimPath, {
|
|
777
|
+
expectedCurrentContentSha256: lease.contentSha256,
|
|
778
|
+
}, lease.environment)) return fail("directory lease changed before release");
|
|
779
|
+
lease.released = true;
|
|
780
|
+
} catch (error) {
|
|
781
|
+
if (
|
|
782
|
+
error instanceof Error
|
|
783
|
+
&& error.message.startsWith("Beeper Message Like Me recovery:")
|
|
784
|
+
) throw error;
|
|
785
|
+
return fail("directory lease could not be released safely");
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
async function identityIfPresent(path: string): Promise<LeaseDirectoryIdentity | null> {
|
|
790
|
+
try {
|
|
791
|
+
return await physicalDirectoryIdentity(path, true);
|
|
792
|
+
} catch (error) {
|
|
793
|
+
if (isErrno(error, "ENOENT")) return null;
|
|
794
|
+
throw error;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function rawLeaseRecoveryDisposition(
|
|
799
|
+
claim: DirectoryLeaseClaim,
|
|
800
|
+
nowMs: number,
|
|
801
|
+
inspectOwner: (owner: ProcessOwnerIdentity) => ProcessOwnerStatus,
|
|
802
|
+
): "active" | "indeterminate" | "recoverable" {
|
|
803
|
+
if (claim.phase === "preparing" || claim.phase === "settled") {
|
|
804
|
+
return "recoverable";
|
|
805
|
+
}
|
|
806
|
+
if (claim.phase === "running" && claim.childOwner !== null) {
|
|
807
|
+
const childStatus = inspectOwner(claim.childOwner);
|
|
808
|
+
return childStatus === "exact-live-owner"
|
|
809
|
+
? "active"
|
|
810
|
+
: childStatus === "unknown"
|
|
811
|
+
? "indeterminate"
|
|
812
|
+
: "recoverable";
|
|
813
|
+
}
|
|
814
|
+
return nowMs >= claim.recoverAfterMs ? "recoverable" : "indeterminate";
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
export async function recoverBeeperMessageLikeMeDirectoryLeases(options: Readonly<{
|
|
818
|
+
environment?: Readonly<Record<string, string | undefined>>;
|
|
819
|
+
nowMs?: number;
|
|
820
|
+
inspectOwner?: (owner: ProcessOwnerIdentity) => ProcessOwnerStatus;
|
|
821
|
+
}> = {}): Promise<BeeperMessageLikeMeRecoveryReport> {
|
|
822
|
+
const environment = options.environment ?? process.env;
|
|
823
|
+
const nowMs = milliseconds(options.nowMs ?? Date.now(), "directory recovery time");
|
|
824
|
+
const inspectOwner = options.inspectOwner ?? processOwnerStatus;
|
|
825
|
+
const root = leaseRoot(environment);
|
|
826
|
+
try {
|
|
827
|
+
ensurePrivateDirectory(root);
|
|
828
|
+
const snapshot = snapshotPrivateStateDirectory(
|
|
829
|
+
root,
|
|
830
|
+
environment,
|
|
831
|
+
undefined,
|
|
832
|
+
{ recoverOrphanedMutationClaims: true },
|
|
833
|
+
);
|
|
834
|
+
if (snapshot.identity === null || snapshot.entries.length > MAX_CLAIMS) {
|
|
835
|
+
return fail("directory lease collection exceeded its reviewed bound");
|
|
836
|
+
}
|
|
837
|
+
const names = snapshot.entries.map((entry) => {
|
|
838
|
+
if (
|
|
839
|
+
entry.kind !== "file"
|
|
840
|
+
|| !UUID_PATTERN.test(entry.name.replace(/\.json$/u, ""))
|
|
841
|
+
|| !entry.name.endsWith(".json")
|
|
842
|
+
) return fail("directory lease collection contains an unsupported entry");
|
|
843
|
+
return entry.name;
|
|
844
|
+
}).sort();
|
|
845
|
+
const files = readPrivateStateFilesBatch(root, names, {
|
|
846
|
+
maximumBytesPerFile: MAX_CLAIM_BYTES,
|
|
847
|
+
maximumTotalBytes: MAX_CLAIM_BYTES * Math.max(1, names.length),
|
|
848
|
+
environment,
|
|
849
|
+
expectedDirectoryIdentity: snapshot.identity,
|
|
850
|
+
});
|
|
851
|
+
let recovered = 0;
|
|
852
|
+
let published = 0;
|
|
853
|
+
let active = 0;
|
|
854
|
+
let indeterminate = 0;
|
|
855
|
+
for (const file of files) {
|
|
856
|
+
if (file.status !== "present") {
|
|
857
|
+
return fail("directory lease changed during recovery inspection");
|
|
858
|
+
}
|
|
859
|
+
let decoded: unknown;
|
|
860
|
+
try {
|
|
861
|
+
decoded = JSON.parse(file.content) as unknown;
|
|
862
|
+
} catch {
|
|
863
|
+
return fail("directory lease is not valid JSON");
|
|
864
|
+
}
|
|
865
|
+
const claim = parseClaim(decoded);
|
|
866
|
+
if (
|
|
867
|
+
file.name !== `${claim.id}.json`
|
|
868
|
+
|| file.content !== claimBytes(claim)
|
|
869
|
+
) return fail("directory lease is not canonical");
|
|
870
|
+
const claimPath = join(root, file.name);
|
|
871
|
+
const ownerStatus = inspectOwner(claim.owner);
|
|
872
|
+
if (ownerStatus === "exact-live-owner") {
|
|
873
|
+
active += 1;
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
if (ownerStatus === "unknown") {
|
|
877
|
+
indeterminate += 1;
|
|
878
|
+
continue;
|
|
879
|
+
}
|
|
880
|
+
if (claim.role === "raw-working") {
|
|
881
|
+
const disposition = rawLeaseRecoveryDisposition(
|
|
882
|
+
claim,
|
|
883
|
+
nowMs,
|
|
884
|
+
inspectOwner,
|
|
885
|
+
);
|
|
886
|
+
if (disposition === "active") {
|
|
887
|
+
active += 1;
|
|
888
|
+
continue;
|
|
889
|
+
}
|
|
890
|
+
if (disposition === "indeterminate") {
|
|
891
|
+
indeterminate += 1;
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
const parentIdentity = await physicalDirectoryIdentity(claim.parentPath, false);
|
|
896
|
+
if (!sameIdentity(parentIdentity, claim.parentIdentity)) {
|
|
897
|
+
return fail("directory lease parent changed before recovery");
|
|
898
|
+
}
|
|
899
|
+
const current = await identityIfPresent(claim.path);
|
|
900
|
+
if (current !== null && !sameIdentity(current, claim.directoryIdentity)) {
|
|
901
|
+
return fail("directory lease target changed before recovery");
|
|
902
|
+
}
|
|
903
|
+
if (current !== null) {
|
|
904
|
+
removePrivateDirectoryTree(claim.path, claim.directoryIdentity);
|
|
905
|
+
recovered += 1;
|
|
906
|
+
} else {
|
|
907
|
+
// Completes an interrupted identity-bound helper quarantine when present.
|
|
908
|
+
if (removePrivateDirectoryTree(claim.path, claim.directoryIdentity)) {
|
|
909
|
+
recovered += 1;
|
|
910
|
+
} else if (claim.outputRoot !== null) {
|
|
911
|
+
const outputIdentity = await identityIfPresent(claim.outputRoot);
|
|
912
|
+
if (
|
|
913
|
+
outputIdentity !== null
|
|
914
|
+
&& !sameIdentity(outputIdentity, claim.directoryIdentity)
|
|
915
|
+
) return fail("directory lease output changed before recovery");
|
|
916
|
+
if (outputIdentity !== null) published += 1;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
if (!removePrivateStateFileIfUnchanged(claimPath, {
|
|
920
|
+
expectedCurrentContentSha256: sha256(file.content),
|
|
921
|
+
}, environment)) return fail("directory lease changed before recovery release");
|
|
922
|
+
}
|
|
923
|
+
return Object.freeze({ recovered, published, active, indeterminate });
|
|
924
|
+
} catch (error) {
|
|
925
|
+
if (
|
|
926
|
+
error instanceof Error
|
|
927
|
+
&& error.message.startsWith("Beeper Message Like Me recovery:")
|
|
928
|
+
) throw error;
|
|
929
|
+
return fail("directory lease recovery failed safely");
|
|
930
|
+
}
|
|
931
|
+
}
|