@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,1656 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
closeSync,
|
|
5
|
+
constants,
|
|
6
|
+
fchmodSync,
|
|
7
|
+
fstatSync,
|
|
8
|
+
fsyncSync,
|
|
9
|
+
linkSync,
|
|
10
|
+
lstatSync,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
openSync,
|
|
13
|
+
opendirSync,
|
|
14
|
+
readdirSync,
|
|
15
|
+
readSync,
|
|
16
|
+
renameSync,
|
|
17
|
+
rmSync,
|
|
18
|
+
unlinkSync,
|
|
19
|
+
writeFileSync,
|
|
20
|
+
type BigIntStats,
|
|
21
|
+
} from "node:fs";
|
|
22
|
+
|
|
23
|
+
const MAX_REQUEST_BYTES = 4 * 1024 * 1024;
|
|
24
|
+
const MAX_READ_BYTES = 128 * 1024 * 1024;
|
|
25
|
+
const MAX_WRITE_BYTES = 2 * 1024 * 1024;
|
|
26
|
+
const writeTemporaryFaultForTest = process.env.NODE_ENV === "test"
|
|
27
|
+
? (process.env.GHOSTGET_TEST_WRITE_TEMP_FAULT ?? process.env.WRENCH_TEST_WRITE_TEMP_FAULT)
|
|
28
|
+
: undefined;
|
|
29
|
+
const pathMutationFaultForTest = process.env.NODE_ENV === "test"
|
|
30
|
+
? (process.env.GHOSTGET_TEST_PATH_MUTATION_FAULT ?? process.env.WRENCH_TEST_PATH_MUTATION_FAULT)
|
|
31
|
+
: undefined;
|
|
32
|
+
const removeDirectoryFaultForTest = process.env.NODE_ENV === "test"
|
|
33
|
+
? (process.env.GHOSTGET_TEST_REMOVE_DIRECTORY_FAULT ?? process.env.WRENCH_TEST_REMOVE_DIRECTORY_FAULT)
|
|
34
|
+
: undefined;
|
|
35
|
+
const removeQuarantineScanMaximum = (() => {
|
|
36
|
+
const testValue = process.env.NODE_ENV === "test"
|
|
37
|
+
? (process.env.GHOSTGET_TEST_REMOVE_QUARANTINE_SCAN_MAXIMUM ?? process.env.WRENCH_TEST_REMOVE_QUARANTINE_SCAN_MAXIMUM)
|
|
38
|
+
: undefined;
|
|
39
|
+
if (testValue === undefined) return 100_000;
|
|
40
|
+
if (!/^[1-9][0-9]{0,5}$/u.test(testValue)) {
|
|
41
|
+
throw new Error("test recursive-removal scan bound is invalid");
|
|
42
|
+
}
|
|
43
|
+
return Number(testValue);
|
|
44
|
+
})();
|
|
45
|
+
const writeTemporaryNamePattern =
|
|
46
|
+
/^\.io-write-([1-9][0-9]{0,9})-([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\.tmp$/u;
|
|
47
|
+
const pathMutationStageNamePattern =
|
|
48
|
+
/^\.io-path-mutation-stage-([a-f0-9]{64})-([1-9][0-9]{0,9})-([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\.tmp$/u;
|
|
49
|
+
const removeQuarantineNamePattern =
|
|
50
|
+
/^\.io-remove-([1-9][0-9]{0,9})-([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\.quarantine$/u;
|
|
51
|
+
|
|
52
|
+
type Identity = { readonly device: string; readonly inode: string };
|
|
53
|
+
type DirectoryExpectation = Identity | null;
|
|
54
|
+
type FileExpectation = {
|
|
55
|
+
readonly identity: Identity;
|
|
56
|
+
readonly size: string;
|
|
57
|
+
readonly mtimeNs: string;
|
|
58
|
+
readonly ctimeNs: string;
|
|
59
|
+
};
|
|
60
|
+
type DirectoryEntry = {
|
|
61
|
+
readonly name: string;
|
|
62
|
+
readonly kind: "file" | "directory" | "symbolic-link" | "other";
|
|
63
|
+
readonly identity: Identity;
|
|
64
|
+
readonly size: string;
|
|
65
|
+
readonly mtimeNs: string;
|
|
66
|
+
readonly ctimeNs: string;
|
|
67
|
+
};
|
|
68
|
+
type TreeEntry = Omit<DirectoryEntry, "name"> & {
|
|
69
|
+
readonly path: string;
|
|
70
|
+
};
|
|
71
|
+
type PathMutationClaim = {
|
|
72
|
+
readonly kind: "io-path-mutation-claim";
|
|
73
|
+
readonly schemaVersion: 1;
|
|
74
|
+
readonly targetSha256: string;
|
|
75
|
+
readonly requestId: string;
|
|
76
|
+
readonly pid: number;
|
|
77
|
+
};
|
|
78
|
+
type PathMutationClaimSnapshot = {
|
|
79
|
+
readonly claim: PathMutationClaim;
|
|
80
|
+
readonly content: Buffer;
|
|
81
|
+
readonly stats: BigIntStats;
|
|
82
|
+
};
|
|
83
|
+
type BatchReadFile = {
|
|
84
|
+
readonly segments: readonly string[];
|
|
85
|
+
readonly directoryExpectations: readonly Identity[];
|
|
86
|
+
readonly maximumBytes: number;
|
|
87
|
+
readonly fileExpectation: FileExpectation;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
type Request = {
|
|
91
|
+
readonly schemaVersion: 1;
|
|
92
|
+
readonly requestId: string;
|
|
93
|
+
readonly expected: Identity;
|
|
94
|
+
readonly operation:
|
|
95
|
+
| {
|
|
96
|
+
readonly kind: "ensure-directories";
|
|
97
|
+
readonly segments: readonly string[];
|
|
98
|
+
readonly directoryExpectations: readonly DirectoryExpectation[];
|
|
99
|
+
readonly requireFinalPrivate: boolean;
|
|
100
|
+
}
|
|
101
|
+
| {
|
|
102
|
+
readonly kind: "read-file";
|
|
103
|
+
readonly segments: readonly string[];
|
|
104
|
+
readonly directoryExpectations: readonly Identity[];
|
|
105
|
+
readonly maximumBytes: number;
|
|
106
|
+
readonly fileExpectation?: FileExpectation;
|
|
107
|
+
}
|
|
108
|
+
| {
|
|
109
|
+
readonly kind: "list-directory";
|
|
110
|
+
readonly segments: readonly string[];
|
|
111
|
+
readonly directoryExpectations: readonly Identity[];
|
|
112
|
+
readonly maximumEntries: number;
|
|
113
|
+
}
|
|
114
|
+
| {
|
|
115
|
+
readonly kind: "snapshot-tree";
|
|
116
|
+
readonly maximumEntries: number;
|
|
117
|
+
readonly maximumDirectories: number;
|
|
118
|
+
readonly maximumDepth: number;
|
|
119
|
+
readonly maximumPathBytes: number;
|
|
120
|
+
}
|
|
121
|
+
| {
|
|
122
|
+
readonly kind: "batch-read-files";
|
|
123
|
+
readonly files: readonly BatchReadFile[];
|
|
124
|
+
readonly maximumTotalBytes: number;
|
|
125
|
+
}
|
|
126
|
+
| {
|
|
127
|
+
readonly kind: "write-file";
|
|
128
|
+
readonly segments: readonly string[];
|
|
129
|
+
readonly directoryExpectations: readonly DirectoryExpectation[];
|
|
130
|
+
readonly content: string;
|
|
131
|
+
readonly createOnly: boolean;
|
|
132
|
+
readonly expectedContentSha256: string | null;
|
|
133
|
+
readonly maximumExpectedContentBytes: number;
|
|
134
|
+
}
|
|
135
|
+
| {
|
|
136
|
+
readonly kind: "remove-directory-tree";
|
|
137
|
+
readonly segments: readonly string[];
|
|
138
|
+
readonly directoryExpectations: readonly DirectoryExpectation[];
|
|
139
|
+
readonly expectedTargetBirthtimeNs: string | null;
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
type Response = {
|
|
144
|
+
readonly ok: true;
|
|
145
|
+
readonly identity: Identity;
|
|
146
|
+
readonly created?: boolean;
|
|
147
|
+
readonly removed?: boolean;
|
|
148
|
+
readonly contentBase64?: string;
|
|
149
|
+
readonly entries?: readonly DirectoryEntry[];
|
|
150
|
+
readonly targetIdentity?: Identity;
|
|
151
|
+
readonly treeEntries?: readonly TreeEntry[];
|
|
152
|
+
readonly fileContentsBase64?: readonly string[];
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
function hasCode(error: unknown, code: string): boolean {
|
|
156
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function ownedByCurrentUser(stats: { readonly uid: number | bigint }): boolean {
|
|
160
|
+
const currentUid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
161
|
+
return currentUid === undefined || stats.uid === (typeof stats.uid === "bigint" ? BigInt(currentUid) : currentUid);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function identity(stats: { readonly dev: number | bigint; readonly ino: number | bigint }): Identity {
|
|
165
|
+
return { device: stats.dev.toString(), inode: stats.ino.toString() };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function directoryEntry(name: string, stats: BigIntStats): DirectoryEntry {
|
|
169
|
+
return {
|
|
170
|
+
name,
|
|
171
|
+
kind: stats.isFile()
|
|
172
|
+
? "file"
|
|
173
|
+
: stats.isDirectory()
|
|
174
|
+
? "directory"
|
|
175
|
+
: stats.isSymbolicLink()
|
|
176
|
+
? "symbolic-link"
|
|
177
|
+
: "other",
|
|
178
|
+
identity: identity(stats),
|
|
179
|
+
size: stats.size.toString(),
|
|
180
|
+
mtimeNs: stats.mtimeNs.toString(),
|
|
181
|
+
ctimeNs: stats.ctimeNs.toString(),
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function sameIdentity(left: Identity, right: Identity): boolean {
|
|
186
|
+
return left.device === right.device && left.inode === right.inode;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function matchesFileExpectation(
|
|
190
|
+
stats: BigIntStats,
|
|
191
|
+
expected: FileExpectation,
|
|
192
|
+
): boolean {
|
|
193
|
+
return sameIdentity(identity(stats), expected.identity)
|
|
194
|
+
&& stats.size.toString() === expected.size
|
|
195
|
+
&& stats.mtimeNs.toString() === expected.mtimeNs
|
|
196
|
+
&& stats.ctimeNs.toString() === expected.ctimeNs;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function sameFileSnapshot(left: BigIntStats, right: BigIntStats): boolean {
|
|
200
|
+
return left.dev === right.dev
|
|
201
|
+
&& left.ino === right.ino
|
|
202
|
+
&& left.size === right.size
|
|
203
|
+
&& left.mtimeNs === right.mtimeNs
|
|
204
|
+
&& left.ctimeNs === right.ctimeNs;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function hasPrivateDirectoryMode(stats: { readonly mode: number | bigint }): boolean {
|
|
208
|
+
return typeof stats.mode === "bigint"
|
|
209
|
+
? (stats.mode & 0o777n) === 0o700n
|
|
210
|
+
: (stats.mode & 0o777) === 0o700;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function readBoundedStdin(): string {
|
|
214
|
+
const chunks: Buffer[] = [];
|
|
215
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
216
|
+
let total = 0;
|
|
217
|
+
for (;;) {
|
|
218
|
+
const count = readSync(0, buffer, 0, buffer.byteLength, null);
|
|
219
|
+
if (count === 0) break;
|
|
220
|
+
total += count;
|
|
221
|
+
if (total > MAX_REQUEST_BYTES) throw new Error("request exceeds its byte bound");
|
|
222
|
+
chunks.push(Buffer.from(buffer.subarray(0, count)));
|
|
223
|
+
}
|
|
224
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks, total));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function readDescriptorBounded(descriptor: number, maximumBytes: number): Buffer {
|
|
228
|
+
const chunks: Buffer[] = [];
|
|
229
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, maximumBytes + 1));
|
|
230
|
+
let total = 0;
|
|
231
|
+
for (;;) {
|
|
232
|
+
const remaining = maximumBytes + 1 - total;
|
|
233
|
+
const count = readSync(descriptor, buffer, 0, Math.min(buffer.byteLength, remaining), null);
|
|
234
|
+
if (count === 0) return Buffer.concat(chunks, total);
|
|
235
|
+
total += count;
|
|
236
|
+
if (total > maximumBytes) throw new Error("file grew beyond its byte bound");
|
|
237
|
+
chunks.push(Buffer.from(buffer.subarray(0, count)));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
242
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function exactKeys(value: Record<string, unknown>, expected: readonly string[]): boolean {
|
|
246
|
+
const actual = Object.keys(value).sort();
|
|
247
|
+
const sorted = [...expected].sort();
|
|
248
|
+
return actual.length === sorted.length && actual.every((key, index) => key === sorted[index]);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function parseIdentity(value: unknown): Identity {
|
|
252
|
+
if (
|
|
253
|
+
!isRecord(value)
|
|
254
|
+
|| !exactKeys(value, ["device", "inode"])
|
|
255
|
+
|| typeof value.device !== "string"
|
|
256
|
+
|| !/^\d{1,40}$/u.test(value.device)
|
|
257
|
+
|| typeof value.inode !== "string"
|
|
258
|
+
|| !/^\d{1,40}$/u.test(value.inode)
|
|
259
|
+
) throw new Error("expected directory identity is invalid");
|
|
260
|
+
return { device: value.device, inode: value.inode };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function parseFileExpectation(value: unknown): FileExpectation {
|
|
264
|
+
if (
|
|
265
|
+
!isRecord(value)
|
|
266
|
+
|| !exactKeys(value, ["identity", "size", "mtimeNs", "ctimeNs"])
|
|
267
|
+
|| typeof value.size !== "string"
|
|
268
|
+
|| !/^[0-9]{1,40}$/u.test(value.size)
|
|
269
|
+
|| typeof value.mtimeNs !== "string"
|
|
270
|
+
|| !/^-?[0-9]{1,40}$/u.test(value.mtimeNs)
|
|
271
|
+
|| typeof value.ctimeNs !== "string"
|
|
272
|
+
|| !/^-?[0-9]{1,40}$/u.test(value.ctimeNs)
|
|
273
|
+
) throw new Error("expected file identity is invalid");
|
|
274
|
+
return {
|
|
275
|
+
identity: parseIdentity(value.identity),
|
|
276
|
+
size: value.size,
|
|
277
|
+
mtimeNs: value.mtimeNs,
|
|
278
|
+
ctimeNs: value.ctimeNs,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function parseSegments(value: unknown, allowEmpty = false): readonly string[] {
|
|
283
|
+
if (!Array.isArray(value) || (!allowEmpty && value.length === 0) || value.length > 64) {
|
|
284
|
+
throw new Error("path segment count is invalid");
|
|
285
|
+
}
|
|
286
|
+
return value.map((candidate) => {
|
|
287
|
+
if (
|
|
288
|
+
typeof candidate !== "string"
|
|
289
|
+
|| candidate === ""
|
|
290
|
+
|| candidate === "."
|
|
291
|
+
|| candidate === ".."
|
|
292
|
+
|| candidate.includes("/")
|
|
293
|
+
|| candidate.includes("\\")
|
|
294
|
+
|| candidate.includes("\u0000")
|
|
295
|
+
|| Buffer.byteLength(candidate, "utf8") > 255
|
|
296
|
+
) throw new Error("path segment is invalid");
|
|
297
|
+
return candidate;
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function parseDirectoryExpectations(
|
|
302
|
+
value: unknown,
|
|
303
|
+
expectedLength: number,
|
|
304
|
+
): readonly DirectoryExpectation[] {
|
|
305
|
+
if (!Array.isArray(value) || value.length !== expectedLength) {
|
|
306
|
+
throw new Error("directory expectation count does not match the path");
|
|
307
|
+
}
|
|
308
|
+
let sawAbsent = false;
|
|
309
|
+
return value.map((candidate) => {
|
|
310
|
+
if (candidate === null) {
|
|
311
|
+
sawAbsent = true;
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
if (sawAbsent) throw new Error("an existing directory cannot follow an absent directory expectation");
|
|
315
|
+
return parseIdentity(candidate);
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function parseExistingDirectoryExpectations(value: unknown, expectedLength: number): readonly Identity[] {
|
|
320
|
+
const parsed = parseDirectoryExpectations(value, expectedLength);
|
|
321
|
+
return parsed.map((candidate) => {
|
|
322
|
+
if (candidate === null) throw new Error("this operation cannot create an absent directory");
|
|
323
|
+
return candidate;
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function parseBatchReadFile(value: unknown): BatchReadFile {
|
|
328
|
+
if (
|
|
329
|
+
!isRecord(value)
|
|
330
|
+
|| !exactKeys(value, [
|
|
331
|
+
"segments",
|
|
332
|
+
"directoryExpectations",
|
|
333
|
+
"maximumBytes",
|
|
334
|
+
"fileExpectation",
|
|
335
|
+
])
|
|
336
|
+
|| typeof value.maximumBytes !== "number"
|
|
337
|
+
|| !Number.isSafeInteger(value.maximumBytes)
|
|
338
|
+
|| value.maximumBytes < 0
|
|
339
|
+
|| value.maximumBytes > MAX_READ_BYTES
|
|
340
|
+
) throw new Error("batch read file request is invalid");
|
|
341
|
+
const segments = parseSegments(value.segments);
|
|
342
|
+
const fileExpectation = parseFileExpectation(value.fileExpectation);
|
|
343
|
+
if (BigInt(fileExpectation.size) > BigInt(value.maximumBytes)) {
|
|
344
|
+
throw new Error("batch read file expectation exceeds its byte bound");
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
segments,
|
|
348
|
+
directoryExpectations: parseExistingDirectoryExpectations(
|
|
349
|
+
value.directoryExpectations,
|
|
350
|
+
segments.length - 1,
|
|
351
|
+
),
|
|
352
|
+
maximumBytes: value.maximumBytes,
|
|
353
|
+
fileExpectation,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function parseRequest(value: unknown): Request {
|
|
358
|
+
if (
|
|
359
|
+
!isRecord(value)
|
|
360
|
+
|| !exactKeys(value, ["schemaVersion", "requestId", "expected", "operation"])
|
|
361
|
+
|| value.schemaVersion !== 1
|
|
362
|
+
|| typeof value.requestId !== "string"
|
|
363
|
+
|| !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(value.requestId)
|
|
364
|
+
|| !isRecord(value.operation)
|
|
365
|
+
|| typeof value.operation.kind !== "string"
|
|
366
|
+
) throw new Error("request envelope is invalid");
|
|
367
|
+
const expected = parseIdentity(value.expected);
|
|
368
|
+
const operation = value.operation;
|
|
369
|
+
if (
|
|
370
|
+
operation.kind === "ensure-directories"
|
|
371
|
+
&& exactKeys(operation, ["kind", "segments", "directoryExpectations", "requireFinalPrivate"])
|
|
372
|
+
&& typeof operation.requireFinalPrivate === "boolean"
|
|
373
|
+
) {
|
|
374
|
+
const segments = parseSegments(operation.segments, true);
|
|
375
|
+
return {
|
|
376
|
+
schemaVersion: 1,
|
|
377
|
+
requestId: value.requestId,
|
|
378
|
+
expected,
|
|
379
|
+
operation: {
|
|
380
|
+
kind: "ensure-directories",
|
|
381
|
+
segments,
|
|
382
|
+
directoryExpectations: parseDirectoryExpectations(operation.directoryExpectations, segments.length),
|
|
383
|
+
requireFinalPrivate: operation.requireFinalPrivate,
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
if (
|
|
388
|
+
operation.kind === "read-file"
|
|
389
|
+
&& (
|
|
390
|
+
exactKeys(operation, ["kind", "segments", "directoryExpectations", "maximumBytes"])
|
|
391
|
+
|| exactKeys(operation, [
|
|
392
|
+
"kind",
|
|
393
|
+
"segments",
|
|
394
|
+
"directoryExpectations",
|
|
395
|
+
"maximumBytes",
|
|
396
|
+
"fileExpectation",
|
|
397
|
+
])
|
|
398
|
+
)
|
|
399
|
+
&& typeof operation.maximumBytes === "number"
|
|
400
|
+
&& Number.isSafeInteger(operation.maximumBytes)
|
|
401
|
+
&& operation.maximumBytes >= 0
|
|
402
|
+
&& operation.maximumBytes <= MAX_READ_BYTES
|
|
403
|
+
) {
|
|
404
|
+
const segments = parseSegments(operation.segments);
|
|
405
|
+
return {
|
|
406
|
+
schemaVersion: 1,
|
|
407
|
+
requestId: value.requestId,
|
|
408
|
+
expected,
|
|
409
|
+
operation: {
|
|
410
|
+
kind: "read-file",
|
|
411
|
+
segments,
|
|
412
|
+
directoryExpectations: parseExistingDirectoryExpectations(
|
|
413
|
+
operation.directoryExpectations,
|
|
414
|
+
segments.length - 1,
|
|
415
|
+
),
|
|
416
|
+
maximumBytes: operation.maximumBytes,
|
|
417
|
+
...("fileExpectation" in operation
|
|
418
|
+
? { fileExpectation: parseFileExpectation(operation.fileExpectation) }
|
|
419
|
+
: {}),
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
if (
|
|
424
|
+
operation.kind === "list-directory"
|
|
425
|
+
&& exactKeys(operation, ["kind", "segments", "directoryExpectations", "maximumEntries"])
|
|
426
|
+
&& typeof operation.maximumEntries === "number"
|
|
427
|
+
&& Number.isSafeInteger(operation.maximumEntries)
|
|
428
|
+
&& operation.maximumEntries >= 1
|
|
429
|
+
&& operation.maximumEntries <= 10_000
|
|
430
|
+
) {
|
|
431
|
+
const segments = parseSegments(operation.segments, true);
|
|
432
|
+
return {
|
|
433
|
+
schemaVersion: 1,
|
|
434
|
+
requestId: value.requestId,
|
|
435
|
+
expected,
|
|
436
|
+
operation: {
|
|
437
|
+
kind: "list-directory",
|
|
438
|
+
segments,
|
|
439
|
+
directoryExpectations: parseExistingDirectoryExpectations(
|
|
440
|
+
operation.directoryExpectations,
|
|
441
|
+
segments.length,
|
|
442
|
+
),
|
|
443
|
+
maximumEntries: operation.maximumEntries,
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
if (
|
|
448
|
+
operation.kind === "snapshot-tree"
|
|
449
|
+
&& exactKeys(operation, [
|
|
450
|
+
"kind",
|
|
451
|
+
"maximumEntries",
|
|
452
|
+
"maximumDirectories",
|
|
453
|
+
"maximumDepth",
|
|
454
|
+
"maximumPathBytes",
|
|
455
|
+
])
|
|
456
|
+
&& typeof operation.maximumEntries === "number"
|
|
457
|
+
&& Number.isSafeInteger(operation.maximumEntries)
|
|
458
|
+
&& operation.maximumEntries >= 1
|
|
459
|
+
&& operation.maximumEntries <= 10_000
|
|
460
|
+
&& typeof operation.maximumDirectories === "number"
|
|
461
|
+
&& Number.isSafeInteger(operation.maximumDirectories)
|
|
462
|
+
&& operation.maximumDirectories >= 0
|
|
463
|
+
&& operation.maximumDirectories <= operation.maximumEntries
|
|
464
|
+
&& typeof operation.maximumDepth === "number"
|
|
465
|
+
&& Number.isSafeInteger(operation.maximumDepth)
|
|
466
|
+
&& operation.maximumDepth >= 1
|
|
467
|
+
&& operation.maximumDepth <= 64
|
|
468
|
+
&& typeof operation.maximumPathBytes === "number"
|
|
469
|
+
&& Number.isSafeInteger(operation.maximumPathBytes)
|
|
470
|
+
&& operation.maximumPathBytes >= 1
|
|
471
|
+
&& operation.maximumPathBytes <= 4_096
|
|
472
|
+
) {
|
|
473
|
+
return {
|
|
474
|
+
schemaVersion: 1,
|
|
475
|
+
requestId: value.requestId,
|
|
476
|
+
expected,
|
|
477
|
+
operation: {
|
|
478
|
+
kind: "snapshot-tree",
|
|
479
|
+
maximumEntries: operation.maximumEntries,
|
|
480
|
+
maximumDirectories: operation.maximumDirectories,
|
|
481
|
+
maximumDepth: operation.maximumDepth,
|
|
482
|
+
maximumPathBytes: operation.maximumPathBytes,
|
|
483
|
+
},
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
if (
|
|
487
|
+
operation.kind === "batch-read-files"
|
|
488
|
+
&& exactKeys(operation, ["kind", "files", "maximumTotalBytes"])
|
|
489
|
+
&& Array.isArray(operation.files)
|
|
490
|
+
&& operation.files.length >= 1
|
|
491
|
+
&& operation.files.length <= 1_000
|
|
492
|
+
&& typeof operation.maximumTotalBytes === "number"
|
|
493
|
+
&& Number.isSafeInteger(operation.maximumTotalBytes)
|
|
494
|
+
&& operation.maximumTotalBytes >= 1
|
|
495
|
+
&& operation.maximumTotalBytes <= MAX_READ_BYTES
|
|
496
|
+
) {
|
|
497
|
+
const files = operation.files.map(parseBatchReadFile);
|
|
498
|
+
const paths = files.map((file) => file.segments.join("/"));
|
|
499
|
+
if (new Set(paths).size !== paths.length) {
|
|
500
|
+
throw new Error("batch read file paths must be unique");
|
|
501
|
+
}
|
|
502
|
+
const expectedTotal = files.reduce(
|
|
503
|
+
(total, file) => total + BigInt(file.fileExpectation.size),
|
|
504
|
+
0n,
|
|
505
|
+
);
|
|
506
|
+
if (expectedTotal > BigInt(operation.maximumTotalBytes)) {
|
|
507
|
+
throw new Error("batch read file expectations exceed the total byte bound");
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
schemaVersion: 1,
|
|
511
|
+
requestId: value.requestId,
|
|
512
|
+
expected,
|
|
513
|
+
operation: {
|
|
514
|
+
kind: "batch-read-files",
|
|
515
|
+
files,
|
|
516
|
+
maximumTotalBytes: operation.maximumTotalBytes,
|
|
517
|
+
},
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
if (
|
|
521
|
+
operation.kind === "write-file"
|
|
522
|
+
&& (
|
|
523
|
+
exactKeys(operation, ["kind", "segments", "directoryExpectations", "content", "createOnly"])
|
|
524
|
+
|| exactKeys(operation, [
|
|
525
|
+
"kind", "segments", "directoryExpectations", "content", "createOnly",
|
|
526
|
+
"expectedContentSha256", "maximumExpectedContentBytes",
|
|
527
|
+
])
|
|
528
|
+
)
|
|
529
|
+
&& typeof operation.content === "string"
|
|
530
|
+
&& Buffer.byteLength(operation.content, "utf8") <= MAX_WRITE_BYTES
|
|
531
|
+
&& typeof operation.createOnly === "boolean"
|
|
532
|
+
&& (
|
|
533
|
+
operation.expectedContentSha256 === undefined
|
|
534
|
+
|| operation.expectedContentSha256 === null
|
|
535
|
+
|| typeof operation.expectedContentSha256 === "string"
|
|
536
|
+
&& /^[a-f0-9]{64}$/u.test(operation.expectedContentSha256)
|
|
537
|
+
)
|
|
538
|
+
&& (
|
|
539
|
+
operation.maximumExpectedContentBytes === undefined
|
|
540
|
+
|| typeof operation.maximumExpectedContentBytes === "number"
|
|
541
|
+
&& Number.isSafeInteger(operation.maximumExpectedContentBytes)
|
|
542
|
+
&& operation.maximumExpectedContentBytes >= 0
|
|
543
|
+
&& operation.maximumExpectedContentBytes <= MAX_READ_BYTES
|
|
544
|
+
)
|
|
545
|
+
&& (!operation.createOnly || operation.expectedContentSha256 == null)
|
|
546
|
+
) {
|
|
547
|
+
const segments = parseSegments(operation.segments);
|
|
548
|
+
return {
|
|
549
|
+
schemaVersion: 1,
|
|
550
|
+
requestId: value.requestId,
|
|
551
|
+
expected,
|
|
552
|
+
operation: {
|
|
553
|
+
kind: "write-file",
|
|
554
|
+
segments,
|
|
555
|
+
directoryExpectations: parseDirectoryExpectations(operation.directoryExpectations, segments.length - 1),
|
|
556
|
+
content: operation.content,
|
|
557
|
+
createOnly: operation.createOnly,
|
|
558
|
+
expectedContentSha256: operation.expectedContentSha256 ?? null,
|
|
559
|
+
maximumExpectedContentBytes:
|
|
560
|
+
operation.maximumExpectedContentBytes ?? MAX_WRITE_BYTES,
|
|
561
|
+
},
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
if (
|
|
565
|
+
operation.kind === "remove-directory-tree"
|
|
566
|
+
&& (
|
|
567
|
+
exactKeys(operation, ["kind", "segments", "directoryExpectations"])
|
|
568
|
+
|| exactKeys(operation, [
|
|
569
|
+
"kind",
|
|
570
|
+
"segments",
|
|
571
|
+
"directoryExpectations",
|
|
572
|
+
"expectedTargetBirthtimeNs",
|
|
573
|
+
])
|
|
574
|
+
)
|
|
575
|
+
) {
|
|
576
|
+
const segments = parseSegments(operation.segments);
|
|
577
|
+
if (segments.length < 2) throw new Error("recursive removal target is too broad");
|
|
578
|
+
const expectedTargetBirthtimeNs = "expectedTargetBirthtimeNs" in operation
|
|
579
|
+
? operation.expectedTargetBirthtimeNs
|
|
580
|
+
: null;
|
|
581
|
+
if (
|
|
582
|
+
expectedTargetBirthtimeNs !== null
|
|
583
|
+
&& (
|
|
584
|
+
typeof expectedTargetBirthtimeNs !== "string"
|
|
585
|
+
|| !/^[1-9]\d{0,39}$/u.test(expectedTargetBirthtimeNs)
|
|
586
|
+
)
|
|
587
|
+
) throw new Error("recursive removal target birth time is invalid");
|
|
588
|
+
return {
|
|
589
|
+
schemaVersion: 1,
|
|
590
|
+
requestId: value.requestId,
|
|
591
|
+
expected,
|
|
592
|
+
operation: {
|
|
593
|
+
kind: "remove-directory-tree",
|
|
594
|
+
segments,
|
|
595
|
+
directoryExpectations: parseDirectoryExpectations(operation.directoryExpectations, segments.length),
|
|
596
|
+
expectedTargetBirthtimeNs,
|
|
597
|
+
},
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
throw new Error("request operation is invalid");
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function openBoundDirectory(path: string): number {
|
|
604
|
+
const descriptor = openSync(
|
|
605
|
+
path,
|
|
606
|
+
constants.O_RDONLY
|
|
607
|
+
| ("O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0)
|
|
608
|
+
| ("O_DIRECTORY" in constants ? constants.O_DIRECTORY : 0),
|
|
609
|
+
);
|
|
610
|
+
const stats = fstatSync(descriptor);
|
|
611
|
+
if (!stats.isDirectory()) {
|
|
612
|
+
closeSync(descriptor);
|
|
613
|
+
throw new Error("bound path is not a real directory");
|
|
614
|
+
}
|
|
615
|
+
return descriptor;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function syncDirectory(path: string): void {
|
|
619
|
+
if (process.platform === "win32") return;
|
|
620
|
+
const descriptor = openBoundDirectory(path);
|
|
621
|
+
try {
|
|
622
|
+
fsyncSync(descriptor);
|
|
623
|
+
} finally {
|
|
624
|
+
closeSync(descriptor);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function processIsDefinitelyMissing(pid: number): boolean {
|
|
629
|
+
try {
|
|
630
|
+
process.kill(pid, 0);
|
|
631
|
+
return false;
|
|
632
|
+
} catch (error) {
|
|
633
|
+
return (
|
|
634
|
+
typeof error === "object"
|
|
635
|
+
&& error !== null
|
|
636
|
+
&& "code" in error
|
|
637
|
+
&& error.code === "ESRCH"
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function recoverDefinitelyOrphanedWriteTemporaries(): void {
|
|
643
|
+
let removed = false;
|
|
644
|
+
for (const name of readdirSync(".")) {
|
|
645
|
+
const pidText = writeTemporaryNamePattern.exec(name)?.[1];
|
|
646
|
+
if (pidText === undefined) continue;
|
|
647
|
+
const pid = Number(pidText);
|
|
648
|
+
if (
|
|
649
|
+
!Number.isSafeInteger(pid)
|
|
650
|
+
|| pid < 1
|
|
651
|
+
|| pid > 2_147_483_647
|
|
652
|
+
|| !processIsDefinitelyMissing(pid)
|
|
653
|
+
) continue;
|
|
654
|
+
let stats: ReturnType<typeof lstatSync>;
|
|
655
|
+
try {
|
|
656
|
+
stats = lstatSync(name, { bigint: true });
|
|
657
|
+
} catch (error) {
|
|
658
|
+
if (hasCode(error, "ENOENT")) continue;
|
|
659
|
+
throw error;
|
|
660
|
+
}
|
|
661
|
+
if (
|
|
662
|
+
stats.isSymbolicLink()
|
|
663
|
+
|| !stats.isFile()
|
|
664
|
+
|| !ownedByCurrentUser(stats)
|
|
665
|
+
|| (stats.mode & 0o077n) !== 0n
|
|
666
|
+
) continue;
|
|
667
|
+
try {
|
|
668
|
+
unlinkSync(name);
|
|
669
|
+
removed = true;
|
|
670
|
+
} catch (error) {
|
|
671
|
+
if (!hasCode(error, "ENOENT")) throw error;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (removed) syncDirectory(".");
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function pauseAfterWriteTemporaryForTest(): void {
|
|
678
|
+
if (writeTemporaryFaultForTest !== "pause-after-temp-fsync") return;
|
|
679
|
+
const waitState = new Int32Array(new SharedArrayBuffer(4));
|
|
680
|
+
Atomics.wait(waitState, 0, 0, 60_000);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function pauseAfterRemoveQuarantineForTest(): void {
|
|
684
|
+
if (removeDirectoryFaultForTest !== "pause-after-quarantine-fsync") return;
|
|
685
|
+
const waitState = new Int32Array(new SharedArrayBuffer(4));
|
|
686
|
+
Atomics.wait(waitState, 0, 0, 60_000);
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function assertExpectedCwd(expected: Identity): Identity {
|
|
690
|
+
const descriptor = openBoundDirectory(".");
|
|
691
|
+
try {
|
|
692
|
+
const actual = identity(fstatSync(descriptor, { bigint: true }));
|
|
693
|
+
if (!sameIdentity(actual, expected)) throw new Error("bound directory identity does not match");
|
|
694
|
+
return actual;
|
|
695
|
+
} finally {
|
|
696
|
+
closeSync(descriptor);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function assertPrivateCurrentDirectory(): void {
|
|
701
|
+
const descriptor = openBoundDirectory(".");
|
|
702
|
+
try {
|
|
703
|
+
const stats = fstatSync(descriptor, { bigint: true });
|
|
704
|
+
if (!ownedByCurrentUser(stats) || !hasPrivateDirectoryMode(stats)) {
|
|
705
|
+
throw new Error("preexisting private directory must be current-user-owned with mode 0700");
|
|
706
|
+
}
|
|
707
|
+
} finally {
|
|
708
|
+
closeSync(descriptor);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function enterExpectedDirectory(segment: string, expected: Identity): void {
|
|
713
|
+
const before = lstatSync(segment, { bigint: true });
|
|
714
|
+
if (before.isSymbolicLink() || !before.isDirectory() || !sameIdentity(identity(before), expected)) {
|
|
715
|
+
throw new Error("directory path no longer matches its validated identity");
|
|
716
|
+
}
|
|
717
|
+
process.chdir(segment);
|
|
718
|
+
const descriptor = openBoundDirectory(".");
|
|
719
|
+
try {
|
|
720
|
+
const after = fstatSync(descriptor, { bigint: true });
|
|
721
|
+
if (!sameIdentity(identity(after), expected)) {
|
|
722
|
+
throw new Error("directory changed identity while being bound");
|
|
723
|
+
}
|
|
724
|
+
} finally {
|
|
725
|
+
closeSync(descriptor);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function createAndEnterDirectory(segment: string): void {
|
|
730
|
+
try {
|
|
731
|
+
mkdirSync(segment, { mode: 0o700 });
|
|
732
|
+
} catch (error) {
|
|
733
|
+
if (hasCode(error, "EEXIST")) throw new Error("an absent directory appeared while being created");
|
|
734
|
+
throw error;
|
|
735
|
+
}
|
|
736
|
+
const created = lstatSync(segment, { bigint: true });
|
|
737
|
+
if (created.isSymbolicLink() || !created.isDirectory() || !ownedByCurrentUser(created)) {
|
|
738
|
+
throw new Error("new directory is not a current-user-owned real directory");
|
|
739
|
+
}
|
|
740
|
+
const expected = identity(created);
|
|
741
|
+
syncDirectory(".");
|
|
742
|
+
process.chdir(segment);
|
|
743
|
+
const descriptor = openBoundDirectory(".");
|
|
744
|
+
try {
|
|
745
|
+
const bound = fstatSync(descriptor, { bigint: true });
|
|
746
|
+
if (!sameIdentity(identity(bound), expected) || !ownedByCurrentUser(bound)) {
|
|
747
|
+
throw new Error("new directory changed identity before it was bound");
|
|
748
|
+
}
|
|
749
|
+
fchmodSync(descriptor, 0o700);
|
|
750
|
+
fsyncSync(descriptor);
|
|
751
|
+
} finally {
|
|
752
|
+
closeSync(descriptor);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function traverseDirectories(
|
|
757
|
+
segments: readonly string[],
|
|
758
|
+
expectations: readonly DirectoryExpectation[],
|
|
759
|
+
requireFinalPrivate: boolean,
|
|
760
|
+
createAbsent = true,
|
|
761
|
+
): boolean {
|
|
762
|
+
for (const [index, segment] of segments.entries()) {
|
|
763
|
+
const expected = expectations[index];
|
|
764
|
+
if (expected === undefined) throw new Error("directory expectation is missing");
|
|
765
|
+
if (expected === null) {
|
|
766
|
+
if (!createAbsent) {
|
|
767
|
+
try {
|
|
768
|
+
lstatSync(segment);
|
|
769
|
+
} catch (error) {
|
|
770
|
+
if (hasCode(error, "ENOENT")) return false;
|
|
771
|
+
throw error;
|
|
772
|
+
}
|
|
773
|
+
throw new Error("directory appeared where absence was expected");
|
|
774
|
+
}
|
|
775
|
+
createAndEnterDirectory(segment);
|
|
776
|
+
}
|
|
777
|
+
else enterExpectedDirectory(segment, expected);
|
|
778
|
+
}
|
|
779
|
+
if (requireFinalPrivate) assertPrivateCurrentDirectory();
|
|
780
|
+
return true;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function ensureDirectories(
|
|
784
|
+
expected: Identity,
|
|
785
|
+
segments: readonly string[],
|
|
786
|
+
expectations: readonly DirectoryExpectation[],
|
|
787
|
+
requireFinalPrivate: boolean,
|
|
788
|
+
): Response {
|
|
789
|
+
const actual = assertExpectedCwd(expected);
|
|
790
|
+
traverseDirectories(segments, expectations, requireFinalPrivate);
|
|
791
|
+
return { ok: true, identity: actual };
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function readBoundLeaf(
|
|
795
|
+
leaf: string,
|
|
796
|
+
maximumBytes: number,
|
|
797
|
+
fileExpectation?: FileExpectation,
|
|
798
|
+
): Buffer {
|
|
799
|
+
const pathStats = lstatSync(leaf, { bigint: true });
|
|
800
|
+
if (
|
|
801
|
+
pathStats.isSymbolicLink()
|
|
802
|
+
|| !pathStats.isFile()
|
|
803
|
+
|| (fileExpectation !== undefined
|
|
804
|
+
&& !matchesFileExpectation(pathStats, fileExpectation))
|
|
805
|
+
) {
|
|
806
|
+
throw new Error("read target no longer matches its validated file identity");
|
|
807
|
+
}
|
|
808
|
+
const descriptor = openSync(
|
|
809
|
+
leaf,
|
|
810
|
+
constants.O_RDONLY
|
|
811
|
+
| ("O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0)
|
|
812
|
+
| ("O_NONBLOCK" in constants ? constants.O_NONBLOCK : 0),
|
|
813
|
+
);
|
|
814
|
+
try {
|
|
815
|
+
const stats = fstatSync(descriptor, { bigint: true });
|
|
816
|
+
if (
|
|
817
|
+
!stats.isFile()
|
|
818
|
+
|| stats.size > BigInt(maximumBytes)
|
|
819
|
+
|| (fileExpectation !== undefined
|
|
820
|
+
&& !matchesFileExpectation(stats, fileExpectation))
|
|
821
|
+
) {
|
|
822
|
+
throw new Error("read target is not a bounded regular file");
|
|
823
|
+
}
|
|
824
|
+
const content = readDescriptorBounded(descriptor, maximumBytes);
|
|
825
|
+
const after = fstatSync(descriptor, { bigint: true });
|
|
826
|
+
if (!sameFileSnapshot(stats, after)) {
|
|
827
|
+
throw new Error("read target changed while it was being read");
|
|
828
|
+
}
|
|
829
|
+
return content;
|
|
830
|
+
} finally {
|
|
831
|
+
closeSync(descriptor);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function pathMutationTargetSha256(leaf: string): string {
|
|
836
|
+
return createHash("sha256")
|
|
837
|
+
.update("io-path-mutation", "utf8")
|
|
838
|
+
.update("\0", "utf8")
|
|
839
|
+
.update(leaf, "utf8")
|
|
840
|
+
.digest("hex");
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function pathMutationLockName(targetSha256: string): string {
|
|
844
|
+
return `.io-path-mutation-${targetSha256}.lock`;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function renderPathMutationClaim(claim: PathMutationClaim): string {
|
|
848
|
+
return `${JSON.stringify(claim)}\n`;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
function parsePathMutationClaim(
|
|
852
|
+
content: Buffer,
|
|
853
|
+
targetSha256: string,
|
|
854
|
+
): PathMutationClaim {
|
|
855
|
+
let value: unknown;
|
|
856
|
+
try {
|
|
857
|
+
value = JSON.parse(new TextDecoder("utf-8", {
|
|
858
|
+
fatal: true,
|
|
859
|
+
ignoreBOM: true,
|
|
860
|
+
}).decode(content)) as unknown;
|
|
861
|
+
} catch (error) {
|
|
862
|
+
throw new Error("path mutation claim is not canonical JSON", { cause: error });
|
|
863
|
+
}
|
|
864
|
+
if (
|
|
865
|
+
!isRecord(value)
|
|
866
|
+
|| !exactKeys(value, [
|
|
867
|
+
"kind",
|
|
868
|
+
"schemaVersion",
|
|
869
|
+
"targetSha256",
|
|
870
|
+
"requestId",
|
|
871
|
+
"pid",
|
|
872
|
+
])
|
|
873
|
+
|| value.kind !== "io-path-mutation-claim"
|
|
874
|
+
|| value.schemaVersion !== 1
|
|
875
|
+
|| value.targetSha256 !== targetSha256
|
|
876
|
+
|| typeof value.requestId !== "string"
|
|
877
|
+
|| !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u
|
|
878
|
+
.test(value.requestId)
|
|
879
|
+
|| typeof value.pid !== "number"
|
|
880
|
+
|| !Number.isSafeInteger(value.pid)
|
|
881
|
+
|| value.pid < 1
|
|
882
|
+
|| value.pid > 2_147_483_647
|
|
883
|
+
) throw new Error("path mutation claim is invalid");
|
|
884
|
+
const claim: PathMutationClaim = {
|
|
885
|
+
kind: "io-path-mutation-claim",
|
|
886
|
+
schemaVersion: 1,
|
|
887
|
+
targetSha256,
|
|
888
|
+
requestId: value.requestId,
|
|
889
|
+
pid: value.pid,
|
|
890
|
+
};
|
|
891
|
+
if (renderPathMutationClaim(claim) !== content.toString("utf8")) {
|
|
892
|
+
throw new Error("path mutation claim is not canonical JSON");
|
|
893
|
+
}
|
|
894
|
+
return claim;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function readPathMutationClaimSnapshot(
|
|
898
|
+
name: string,
|
|
899
|
+
targetSha256: string,
|
|
900
|
+
): PathMutationClaimSnapshot | null {
|
|
901
|
+
let before: BigIntStats;
|
|
902
|
+
try {
|
|
903
|
+
before = lstatSync(name, { bigint: true });
|
|
904
|
+
} catch (error) {
|
|
905
|
+
if (hasCode(error, "ENOENT")) return null;
|
|
906
|
+
throw error;
|
|
907
|
+
}
|
|
908
|
+
if (
|
|
909
|
+
before.isSymbolicLink()
|
|
910
|
+
|| !before.isFile()
|
|
911
|
+
|| !ownedByCurrentUser(before)
|
|
912
|
+
|| (before.mode & 0o077n) !== 0n
|
|
913
|
+
|| (before.nlink !== 1n && before.nlink !== 2n)
|
|
914
|
+
) throw new Error("path mutation claim is not one private owned file");
|
|
915
|
+
const content = readBoundLeaf(name, 4 * 1024);
|
|
916
|
+
const after = lstatSync(name, { bigint: true });
|
|
917
|
+
if (
|
|
918
|
+
!sameFileSnapshot(before, after)
|
|
919
|
+
|| before.mode !== after.mode
|
|
920
|
+
|| before.uid !== after.uid
|
|
921
|
+
|| before.gid !== after.gid
|
|
922
|
+
|| before.nlink !== after.nlink
|
|
923
|
+
) throw new Error("path mutation claim changed while it was read");
|
|
924
|
+
return {
|
|
925
|
+
claim: parsePathMutationClaim(content, targetSha256),
|
|
926
|
+
content,
|
|
927
|
+
stats: after,
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
function samePathMutationClaimSnapshot(
|
|
932
|
+
left: PathMutationClaimSnapshot,
|
|
933
|
+
right: PathMutationClaimSnapshot,
|
|
934
|
+
): boolean {
|
|
935
|
+
return left.content.equals(right.content)
|
|
936
|
+
&& sameFileSnapshot(left.stats, right.stats)
|
|
937
|
+
&& left.stats.mode === right.stats.mode
|
|
938
|
+
&& left.stats.uid === right.stats.uid
|
|
939
|
+
&& left.stats.gid === right.stats.gid
|
|
940
|
+
&& left.stats.nlink === right.stats.nlink;
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
function recoverDefinitelyOrphanedPathMutationStages(): void {
|
|
944
|
+
let removed = false;
|
|
945
|
+
for (const name of readdirSync(".")) {
|
|
946
|
+
const pidText = pathMutationStageNamePattern.exec(name)?.[2];
|
|
947
|
+
if (pidText === undefined) continue;
|
|
948
|
+
const pid = Number(pidText);
|
|
949
|
+
if (
|
|
950
|
+
!Number.isSafeInteger(pid)
|
|
951
|
+
|| pid < 1
|
|
952
|
+
|| pid > 2_147_483_647
|
|
953
|
+
|| !processIsDefinitelyMissing(pid)
|
|
954
|
+
) continue;
|
|
955
|
+
let stats: BigIntStats;
|
|
956
|
+
try {
|
|
957
|
+
stats = lstatSync(name, { bigint: true });
|
|
958
|
+
} catch (error) {
|
|
959
|
+
if (hasCode(error, "ENOENT")) continue;
|
|
960
|
+
throw error;
|
|
961
|
+
}
|
|
962
|
+
if (
|
|
963
|
+
stats.isSymbolicLink()
|
|
964
|
+
|| !stats.isFile()
|
|
965
|
+
|| !ownedByCurrentUser(stats)
|
|
966
|
+
|| (stats.mode & 0o077n) !== 0n
|
|
967
|
+
) continue;
|
|
968
|
+
try {
|
|
969
|
+
unlinkSync(name);
|
|
970
|
+
removed = true;
|
|
971
|
+
} catch (error) {
|
|
972
|
+
if (!hasCode(error, "ENOENT")) throw error;
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
if (removed) syncDirectory(".");
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function recoverDefinitelyOrphanedPathMutationClaim(
|
|
979
|
+
lockName: string,
|
|
980
|
+
targetSha256: string,
|
|
981
|
+
): boolean {
|
|
982
|
+
const first = readPathMutationClaimSnapshot(lockName, targetSha256);
|
|
983
|
+
if (first === null) return true;
|
|
984
|
+
if (!processIsDefinitelyMissing(first.claim.pid)) return false;
|
|
985
|
+
const second = readPathMutationClaimSnapshot(lockName, targetSha256);
|
|
986
|
+
if (second === null) return true;
|
|
987
|
+
if (
|
|
988
|
+
!samePathMutationClaimSnapshot(first, second)
|
|
989
|
+
|| !processIsDefinitelyMissing(second.claim.pid)
|
|
990
|
+
) return false;
|
|
991
|
+
const quarantine =
|
|
992
|
+
`.io-path-mutation-recovery-${targetSha256}-${process.pid}-${crypto.randomUUID()}.tmp`;
|
|
993
|
+
try {
|
|
994
|
+
renameSync(lockName, quarantine);
|
|
995
|
+
} catch (error) {
|
|
996
|
+
if (hasCode(error, "ENOENT")) return true;
|
|
997
|
+
throw error;
|
|
998
|
+
}
|
|
999
|
+
const moved = lstatSync(quarantine, { bigint: true });
|
|
1000
|
+
if (!sameIdentity(identity(moved), identity(second.stats))) {
|
|
1001
|
+
throw new Error("path mutation claim changed while it was quarantined");
|
|
1002
|
+
}
|
|
1003
|
+
unlinkSync(quarantine);
|
|
1004
|
+
syncDirectory(".");
|
|
1005
|
+
recoverDefinitelyOrphanedPathMutationStages();
|
|
1006
|
+
return true;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function pauseAfterPathMutationClaimForTest(
|
|
1010
|
+
targetSha256: string,
|
|
1011
|
+
requestId: string,
|
|
1012
|
+
): void {
|
|
1013
|
+
if (pathMutationFaultForTest !== "pause-after-claim") return;
|
|
1014
|
+
const prefix = `.wrench-test-path-mutation-${targetSha256}-${requestId}`;
|
|
1015
|
+
const readyName = `${prefix}-ready`;
|
|
1016
|
+
const releaseName = `${prefix}-release`;
|
|
1017
|
+
const descriptor = openSync(
|
|
1018
|
+
readyName,
|
|
1019
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL,
|
|
1020
|
+
0o600,
|
|
1021
|
+
);
|
|
1022
|
+
try {
|
|
1023
|
+
fchmodSync(descriptor, 0o600);
|
|
1024
|
+
fsyncSync(descriptor);
|
|
1025
|
+
} finally {
|
|
1026
|
+
closeSync(descriptor);
|
|
1027
|
+
}
|
|
1028
|
+
syncDirectory(".");
|
|
1029
|
+
const deadline = Date.now() + 60_000;
|
|
1030
|
+
for (;;) {
|
|
1031
|
+
try {
|
|
1032
|
+
const release = lstatSync(releaseName);
|
|
1033
|
+
if (
|
|
1034
|
+
release.isSymbolicLink()
|
|
1035
|
+
|| !release.isFile()
|
|
1036
|
+
|| !ownedByCurrentUser(release)
|
|
1037
|
+
|| (release.mode & 0o077) !== 0
|
|
1038
|
+
) throw new Error("path mutation test release is not one private file");
|
|
1039
|
+
break;
|
|
1040
|
+
} catch (error) {
|
|
1041
|
+
if (!hasCode(error, "ENOENT")) throw error;
|
|
1042
|
+
}
|
|
1043
|
+
if (Date.now() >= deadline) {
|
|
1044
|
+
throw new Error("path mutation overlap test timed out");
|
|
1045
|
+
}
|
|
1046
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
1047
|
+
}
|
|
1048
|
+
unlinkSync(readyName);
|
|
1049
|
+
unlinkSync(releaseName);
|
|
1050
|
+
syncDirectory(".");
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function acquirePathMutationClaim(
|
|
1054
|
+
leaf: string,
|
|
1055
|
+
requestId: string,
|
|
1056
|
+
): (() => void) | null {
|
|
1057
|
+
const targetSha256 = pathMutationTargetSha256(leaf);
|
|
1058
|
+
const lockName = pathMutationLockName(targetSha256);
|
|
1059
|
+
recoverDefinitelyOrphanedPathMutationStages();
|
|
1060
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1061
|
+
const claim: PathMutationClaim = {
|
|
1062
|
+
kind: "io-path-mutation-claim",
|
|
1063
|
+
schemaVersion: 1,
|
|
1064
|
+
targetSha256,
|
|
1065
|
+
requestId,
|
|
1066
|
+
pid: process.pid,
|
|
1067
|
+
};
|
|
1068
|
+
const stage =
|
|
1069
|
+
`.io-path-mutation-stage-${targetSha256}-${process.pid}-${crypto.randomUUID()}.tmp`;
|
|
1070
|
+
let descriptor: number | null = null;
|
|
1071
|
+
let stageExists = false;
|
|
1072
|
+
try {
|
|
1073
|
+
descriptor = openSync(
|
|
1074
|
+
stage,
|
|
1075
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL
|
|
1076
|
+
| ("O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0),
|
|
1077
|
+
0o600,
|
|
1078
|
+
);
|
|
1079
|
+
stageExists = true;
|
|
1080
|
+
writeFileSync(descriptor, renderPathMutationClaim(claim), "utf8");
|
|
1081
|
+
fchmodSync(descriptor, 0o600);
|
|
1082
|
+
fsyncSync(descriptor);
|
|
1083
|
+
closeSync(descriptor);
|
|
1084
|
+
descriptor = null;
|
|
1085
|
+
try {
|
|
1086
|
+
linkSync(stage, lockName);
|
|
1087
|
+
} catch (error) {
|
|
1088
|
+
if (!hasCode(error, "EEXIST")) throw error;
|
|
1089
|
+
unlinkSync(stage);
|
|
1090
|
+
stageExists = false;
|
|
1091
|
+
syncDirectory(".");
|
|
1092
|
+
if (recoverDefinitelyOrphanedPathMutationClaim(lockName, targetSha256)) {
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
unlinkSync(stage);
|
|
1098
|
+
stageExists = false;
|
|
1099
|
+
syncDirectory(".");
|
|
1100
|
+
pauseAfterPathMutationClaimForTest(targetSha256, requestId);
|
|
1101
|
+
return () => {
|
|
1102
|
+
const held = readPathMutationClaimSnapshot(lockName, targetSha256);
|
|
1103
|
+
if (
|
|
1104
|
+
held === null
|
|
1105
|
+
|| held.claim.pid !== process.pid
|
|
1106
|
+
|| held.claim.requestId !== requestId
|
|
1107
|
+
) throw new Error("path mutation claim changed before release");
|
|
1108
|
+
const releaseName =
|
|
1109
|
+
`.io-path-mutation-release-${targetSha256}-${process.pid}-${crypto.randomUUID()}.tmp`;
|
|
1110
|
+
renameSync(lockName, releaseName);
|
|
1111
|
+
const moved = lstatSync(releaseName, { bigint: true });
|
|
1112
|
+
if (!sameIdentity(identity(moved), identity(held.stats))) {
|
|
1113
|
+
throw new Error("path mutation claim changed while it was released");
|
|
1114
|
+
}
|
|
1115
|
+
unlinkSync(releaseName);
|
|
1116
|
+
syncDirectory(".");
|
|
1117
|
+
};
|
|
1118
|
+
} finally {
|
|
1119
|
+
if (descriptor !== null) closeSync(descriptor);
|
|
1120
|
+
if (stageExists) {
|
|
1121
|
+
try {
|
|
1122
|
+
unlinkSync(stage);
|
|
1123
|
+
syncDirectory(".");
|
|
1124
|
+
} catch {
|
|
1125
|
+
// A dead helper's private stage is recoverable by the next writer.
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
return null;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function readFile(
|
|
1134
|
+
expected: Identity,
|
|
1135
|
+
segments: readonly string[],
|
|
1136
|
+
expectations: readonly Identity[],
|
|
1137
|
+
maximumBytes: number,
|
|
1138
|
+
fileExpectation?: FileExpectation,
|
|
1139
|
+
): Response {
|
|
1140
|
+
const actual = assertExpectedCwd(expected);
|
|
1141
|
+
traverseDirectories(segments.slice(0, -1), expectations, false);
|
|
1142
|
+
const leaf = segments.at(-1);
|
|
1143
|
+
if (leaf === undefined) throw new Error("file path is missing its leaf");
|
|
1144
|
+
const content = readBoundLeaf(leaf, maximumBytes, fileExpectation);
|
|
1145
|
+
return { ok: true, identity: actual, contentBase64: content.toString("base64") };
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function listDirectory(
|
|
1149
|
+
expected: Identity,
|
|
1150
|
+
segments: readonly string[],
|
|
1151
|
+
expectations: readonly Identity[],
|
|
1152
|
+
maximumEntries: number,
|
|
1153
|
+
): Response {
|
|
1154
|
+
const actual = assertExpectedCwd(expected);
|
|
1155
|
+
traverseDirectories(segments, expectations, false);
|
|
1156
|
+
const targetDescriptor = openBoundDirectory(".");
|
|
1157
|
+
let targetIdentity: Identity;
|
|
1158
|
+
try {
|
|
1159
|
+
targetIdentity = identity(fstatSync(targetDescriptor, { bigint: true }));
|
|
1160
|
+
} finally {
|
|
1161
|
+
closeSync(targetDescriptor);
|
|
1162
|
+
}
|
|
1163
|
+
return {
|
|
1164
|
+
ok: true,
|
|
1165
|
+
identity: actual,
|
|
1166
|
+
targetIdentity,
|
|
1167
|
+
entries: readCurrentDirectoryEntries(maximumEntries),
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function readCurrentDirectoryEntries(
|
|
1172
|
+
maximumEntries: number,
|
|
1173
|
+
): readonly DirectoryEntry[] {
|
|
1174
|
+
recoverDefinitelyOrphanedWriteTemporaries();
|
|
1175
|
+
const entries: DirectoryEntry[] = [];
|
|
1176
|
+
const directory = opendirSync(".");
|
|
1177
|
+
try {
|
|
1178
|
+
for (;;) {
|
|
1179
|
+
const entry = directory.readSync();
|
|
1180
|
+
if (entry === null) break;
|
|
1181
|
+
if (entries.length >= maximumEntries) {
|
|
1182
|
+
throw new Error(`directory exceeds its ${maximumEntries} entry bound`);
|
|
1183
|
+
}
|
|
1184
|
+
entries.push(directoryEntry(entry.name, lstatSync(entry.name, { bigint: true })));
|
|
1185
|
+
}
|
|
1186
|
+
} finally {
|
|
1187
|
+
directory.closeSync();
|
|
1188
|
+
}
|
|
1189
|
+
entries.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
|
|
1190
|
+
return entries;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function sameDirectoryEntrySnapshot(
|
|
1194
|
+
stats: BigIntStats,
|
|
1195
|
+
expected: DirectoryEntry,
|
|
1196
|
+
): boolean {
|
|
1197
|
+
return sameIdentity(identity(stats), expected.identity)
|
|
1198
|
+
&& stats.size.toString() === expected.size
|
|
1199
|
+
&& stats.mtimeNs.toString() === expected.mtimeNs
|
|
1200
|
+
&& stats.ctimeNs.toString() === expected.ctimeNs;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function snapshotTree(
|
|
1204
|
+
expected: Identity,
|
|
1205
|
+
maximumEntries: number,
|
|
1206
|
+
maximumDirectories: number,
|
|
1207
|
+
maximumDepth: number,
|
|
1208
|
+
maximumPathBytes: number,
|
|
1209
|
+
): Response {
|
|
1210
|
+
const actual = assertExpectedCwd(expected);
|
|
1211
|
+
const treeEntries: TreeEntry[] = [];
|
|
1212
|
+
let directories = 0;
|
|
1213
|
+
const visit = (
|
|
1214
|
+
expectedCurrent: Identity,
|
|
1215
|
+
parentSegments: readonly string[],
|
|
1216
|
+
): void => {
|
|
1217
|
+
assertExpectedCwd(expectedCurrent);
|
|
1218
|
+
const remainingEntries = maximumEntries - treeEntries.length;
|
|
1219
|
+
const currentEntries = readCurrentDirectoryEntries(
|
|
1220
|
+
Math.max(1, remainingEntries),
|
|
1221
|
+
);
|
|
1222
|
+
for (const entry of currentEntries) {
|
|
1223
|
+
if (treeEntries.length >= maximumEntries) {
|
|
1224
|
+
throw new Error(`directory tree exceeds its ${maximumEntries} entry bound`);
|
|
1225
|
+
}
|
|
1226
|
+
const segments = [...parentSegments, entry.name];
|
|
1227
|
+
const path = segments.join("/");
|
|
1228
|
+
if (Buffer.byteLength(path, "utf8") > maximumPathBytes) {
|
|
1229
|
+
throw new Error(`directory tree path exceeds its ${maximumPathBytes} byte bound`);
|
|
1230
|
+
}
|
|
1231
|
+
treeEntries.push({
|
|
1232
|
+
path,
|
|
1233
|
+
kind: entry.kind,
|
|
1234
|
+
identity: entry.identity,
|
|
1235
|
+
size: entry.size,
|
|
1236
|
+
mtimeNs: entry.mtimeNs,
|
|
1237
|
+
ctimeNs: entry.ctimeNs,
|
|
1238
|
+
});
|
|
1239
|
+
if (entry.kind !== "directory") continue;
|
|
1240
|
+
directories += 1;
|
|
1241
|
+
if (directories > maximumDirectories) {
|
|
1242
|
+
throw new Error(
|
|
1243
|
+
`directory tree exceeds its ${maximumDirectories} directory bound`,
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
if (segments.length > maximumDepth) {
|
|
1247
|
+
throw new Error(`directory tree exceeds its ${maximumDepth} level depth bound`);
|
|
1248
|
+
}
|
|
1249
|
+
enterExpectedDirectory(entry.name, entry.identity);
|
|
1250
|
+
visit(entry.identity, segments);
|
|
1251
|
+
process.chdir("..");
|
|
1252
|
+
assertExpectedCwd(expectedCurrent);
|
|
1253
|
+
const after = lstatSync(entry.name, { bigint: true });
|
|
1254
|
+
if (
|
|
1255
|
+
after.isSymbolicLink()
|
|
1256
|
+
|| !after.isDirectory()
|
|
1257
|
+
|| !sameDirectoryEntrySnapshot(after, entry)
|
|
1258
|
+
) {
|
|
1259
|
+
throw new Error(
|
|
1260
|
+
`directory ${path} changed while its tree was being snapshotted`,
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
visit(expected, []);
|
|
1266
|
+
treeEntries.sort((left, right) =>
|
|
1267
|
+
left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
1268
|
+
return {
|
|
1269
|
+
ok: true,
|
|
1270
|
+
identity: actual,
|
|
1271
|
+
treeEntries,
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
function batchReadFiles(
|
|
1276
|
+
expected: Identity,
|
|
1277
|
+
files: readonly BatchReadFile[],
|
|
1278
|
+
maximumTotalBytes: number,
|
|
1279
|
+
): Response {
|
|
1280
|
+
const actual = assertExpectedCwd(expected);
|
|
1281
|
+
const fileContentsBase64: string[] = [];
|
|
1282
|
+
let totalBytes = 0;
|
|
1283
|
+
for (const file of files) {
|
|
1284
|
+
assertExpectedCwd(expected);
|
|
1285
|
+
const parentSegments = file.segments.slice(0, -1);
|
|
1286
|
+
traverseDirectories(parentSegments, file.directoryExpectations, false);
|
|
1287
|
+
const leaf = file.segments.at(-1);
|
|
1288
|
+
if (leaf === undefined) throw new Error("batch read file path is missing its leaf");
|
|
1289
|
+
const content = readBoundLeaf(
|
|
1290
|
+
leaf,
|
|
1291
|
+
file.maximumBytes,
|
|
1292
|
+
file.fileExpectation,
|
|
1293
|
+
);
|
|
1294
|
+
totalBytes += content.byteLength;
|
|
1295
|
+
if (totalBytes > maximumTotalBytes) {
|
|
1296
|
+
throw new Error(`batch read exceeds its ${maximumTotalBytes} total byte bound`);
|
|
1297
|
+
}
|
|
1298
|
+
fileContentsBase64.push(content.toString("base64"));
|
|
1299
|
+
for (
|
|
1300
|
+
let index = file.directoryExpectations.length - 1;
|
|
1301
|
+
index >= 0;
|
|
1302
|
+
index -= 1
|
|
1303
|
+
) {
|
|
1304
|
+
process.chdir("..");
|
|
1305
|
+
const parentExpected = index === 0
|
|
1306
|
+
? expected
|
|
1307
|
+
: file.directoryExpectations[index - 1];
|
|
1308
|
+
if (parentExpected === undefined) {
|
|
1309
|
+
throw new Error("batch read directory expectation is missing");
|
|
1310
|
+
}
|
|
1311
|
+
assertExpectedCwd(parentExpected);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
assertExpectedCwd(expected);
|
|
1315
|
+
return {
|
|
1316
|
+
ok: true,
|
|
1317
|
+
identity: actual,
|
|
1318
|
+
fileContentsBase64,
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function assertReplaceableLeaf(leaf: string): void {
|
|
1323
|
+
let stats: ReturnType<typeof lstatSync>;
|
|
1324
|
+
try {
|
|
1325
|
+
stats = lstatSync(leaf);
|
|
1326
|
+
} catch (error) {
|
|
1327
|
+
if (hasCode(error, "ENOENT")) return;
|
|
1328
|
+
throw error;
|
|
1329
|
+
}
|
|
1330
|
+
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
1331
|
+
throw new Error("write target must be an absent path or a real regular file");
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function writeFile(
|
|
1336
|
+
expected: Identity,
|
|
1337
|
+
segments: readonly string[],
|
|
1338
|
+
expectations: readonly DirectoryExpectation[],
|
|
1339
|
+
content: string,
|
|
1340
|
+
createOnly: boolean,
|
|
1341
|
+
expectedContentSha256: string | null,
|
|
1342
|
+
maximumExpectedContentBytes: number,
|
|
1343
|
+
requestId: string,
|
|
1344
|
+
): Response {
|
|
1345
|
+
const actual = assertExpectedCwd(expected);
|
|
1346
|
+
traverseDirectories(segments.slice(0, -1), expectations, false);
|
|
1347
|
+
const leaf = segments.at(-1);
|
|
1348
|
+
if (leaf === undefined) throw new Error("file path is missing its leaf");
|
|
1349
|
+
if (!createOnly) assertReplaceableLeaf(leaf);
|
|
1350
|
+
recoverDefinitelyOrphanedWriteTemporaries();
|
|
1351
|
+
|
|
1352
|
+
const temporary = `.io-write-${process.pid}-${crypto.randomUUID()}.tmp`;
|
|
1353
|
+
let descriptor: number | null = null;
|
|
1354
|
+
let temporaryExists = false;
|
|
1355
|
+
let releaseMutation: (() => void) | null = null;
|
|
1356
|
+
let mutationDurable = false;
|
|
1357
|
+
try {
|
|
1358
|
+
descriptor = openSync(
|
|
1359
|
+
temporary,
|
|
1360
|
+
constants.O_WRONLY
|
|
1361
|
+
| constants.O_CREAT
|
|
1362
|
+
| constants.O_EXCL
|
|
1363
|
+
| ("O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0),
|
|
1364
|
+
0o600,
|
|
1365
|
+
);
|
|
1366
|
+
temporaryExists = true;
|
|
1367
|
+
writeFileSync(descriptor, content, "utf8");
|
|
1368
|
+
fchmodSync(descriptor, 0o600);
|
|
1369
|
+
fsyncSync(descriptor);
|
|
1370
|
+
closeSync(descriptor);
|
|
1371
|
+
descriptor = null;
|
|
1372
|
+
pauseAfterWriteTemporaryForTest();
|
|
1373
|
+
releaseMutation = acquirePathMutationClaim(leaf, requestId);
|
|
1374
|
+
if (releaseMutation === null) {
|
|
1375
|
+
if (expectedContentSha256 !== null) {
|
|
1376
|
+
throw new Error("file content no longer matches the expected hash");
|
|
1377
|
+
}
|
|
1378
|
+
throw new Error("file mutation is already active");
|
|
1379
|
+
}
|
|
1380
|
+
if (createOnly) {
|
|
1381
|
+
try {
|
|
1382
|
+
linkSync(temporary, leaf);
|
|
1383
|
+
} catch (error) {
|
|
1384
|
+
if (!hasCode(error, "EEXIST")) throw error;
|
|
1385
|
+
unlinkSync(temporary);
|
|
1386
|
+
temporaryExists = false;
|
|
1387
|
+
syncDirectory(".");
|
|
1388
|
+
return { ok: true, identity: actual, created: false };
|
|
1389
|
+
}
|
|
1390
|
+
unlinkSync(temporary);
|
|
1391
|
+
temporaryExists = false;
|
|
1392
|
+
} else {
|
|
1393
|
+
if (expectedContentSha256 !== null) {
|
|
1394
|
+
let current: Buffer;
|
|
1395
|
+
try {
|
|
1396
|
+
current = readBoundLeaf(leaf, maximumExpectedContentBytes);
|
|
1397
|
+
} catch (error) {
|
|
1398
|
+
if (hasCode(error, "ENOENT")) {
|
|
1399
|
+
throw new Error("file content no longer matches the expected hash");
|
|
1400
|
+
}
|
|
1401
|
+
throw error;
|
|
1402
|
+
}
|
|
1403
|
+
if (
|
|
1404
|
+
createHash("sha256").update(current).digest("hex")
|
|
1405
|
+
!== expectedContentSha256
|
|
1406
|
+
) {
|
|
1407
|
+
throw new Error("file content no longer matches the expected hash");
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
renameSync(temporary, leaf);
|
|
1411
|
+
temporaryExists = false;
|
|
1412
|
+
}
|
|
1413
|
+
syncDirectory(".");
|
|
1414
|
+
mutationDurable = true;
|
|
1415
|
+
return { ok: true, identity: actual, created: true };
|
|
1416
|
+
} finally {
|
|
1417
|
+
if (releaseMutation !== null) {
|
|
1418
|
+
try {
|
|
1419
|
+
releaseMutation();
|
|
1420
|
+
} catch (error) {
|
|
1421
|
+
if (!mutationDurable) throw error;
|
|
1422
|
+
// The target rename and directory sync are authoritative. A dead
|
|
1423
|
+
// helper's exact private claim is recoverable by the next writer.
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
if (descriptor !== null) closeSync(descriptor);
|
|
1427
|
+
if (temporaryExists) {
|
|
1428
|
+
try {
|
|
1429
|
+
unlinkSync(temporary);
|
|
1430
|
+
syncDirectory(".");
|
|
1431
|
+
} catch {
|
|
1432
|
+
// The staged file is private and remains inside the helper's bound cwd.
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
function removeDirectoryTree(
|
|
1439
|
+
expected: Identity,
|
|
1440
|
+
segments: readonly string[],
|
|
1441
|
+
expectations: readonly DirectoryExpectation[],
|
|
1442
|
+
expectedTargetBirthtimeNs: string | null,
|
|
1443
|
+
): Response {
|
|
1444
|
+
const actual = assertExpectedCwd(expected);
|
|
1445
|
+
const targetExpected = expectations.at(-1);
|
|
1446
|
+
const parentSegments = segments.slice(0, -1);
|
|
1447
|
+
const parentExpectations = expectations.slice(0, -1);
|
|
1448
|
+
if (!traverseDirectories(parentSegments, parentExpectations, false, false)) {
|
|
1449
|
+
if (targetExpected !== null && targetExpected !== undefined) {
|
|
1450
|
+
throw new Error("recursive removal parent is absent for an identity-bound target");
|
|
1451
|
+
}
|
|
1452
|
+
return { ok: true, identity: actual, removed: false };
|
|
1453
|
+
}
|
|
1454
|
+
const parentExpected = parentExpectations.at(-1);
|
|
1455
|
+
const name = segments.at(-1);
|
|
1456
|
+
if (
|
|
1457
|
+
targetExpected === undefined
|
|
1458
|
+
|| parentExpected === undefined
|
|
1459
|
+
|| parentExpected === null
|
|
1460
|
+
|| name === undefined
|
|
1461
|
+
) {
|
|
1462
|
+
throw new Error("recursive removal identity chain is incomplete");
|
|
1463
|
+
}
|
|
1464
|
+
const parentDescriptor = openBoundDirectory(".");
|
|
1465
|
+
try {
|
|
1466
|
+
if (!sameIdentity(identity(fstatSync(parentDescriptor, { bigint: true })), parentExpected)) {
|
|
1467
|
+
throw new Error("recursive removal parent changed identity");
|
|
1468
|
+
}
|
|
1469
|
+
} finally {
|
|
1470
|
+
closeSync(parentDescriptor);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
let removalSource = name;
|
|
1474
|
+
let target: BigIntStats | null;
|
|
1475
|
+
try {
|
|
1476
|
+
target = lstatSync(name, { bigint: true });
|
|
1477
|
+
} catch (error) {
|
|
1478
|
+
if (!hasCode(error, "ENOENT")) throw error;
|
|
1479
|
+
target = null;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
if (target === null) {
|
|
1483
|
+
if (targetExpected === null) {
|
|
1484
|
+
return { ok: true, identity: actual, removed: false };
|
|
1485
|
+
}
|
|
1486
|
+
const directory = opendirSync(".");
|
|
1487
|
+
try {
|
|
1488
|
+
for (let entriesRead = 0;; entriesRead += 1) {
|
|
1489
|
+
const entry = directory.readSync();
|
|
1490
|
+
if (entry === null) break;
|
|
1491
|
+
if (entriesRead >= removeQuarantineScanMaximum) {
|
|
1492
|
+
throw new Error(
|
|
1493
|
+
`recursive removal recovery exceeds its ${removeQuarantineScanMaximum} entry bound`,
|
|
1494
|
+
);
|
|
1495
|
+
}
|
|
1496
|
+
const pidText = removeQuarantineNamePattern.exec(entry.name)?.[1];
|
|
1497
|
+
if (pidText === undefined) continue;
|
|
1498
|
+
let candidate: BigIntStats;
|
|
1499
|
+
try {
|
|
1500
|
+
candidate = lstatSync(entry.name, { bigint: true });
|
|
1501
|
+
} catch (error) {
|
|
1502
|
+
if (hasCode(error, "ENOENT")) continue;
|
|
1503
|
+
throw error;
|
|
1504
|
+
}
|
|
1505
|
+
if (
|
|
1506
|
+
!sameIdentity(identity(candidate), targetExpected)
|
|
1507
|
+
|| (
|
|
1508
|
+
expectedTargetBirthtimeNs !== null
|
|
1509
|
+
&& candidate.birthtimeNs.toString() !== expectedTargetBirthtimeNs
|
|
1510
|
+
)
|
|
1511
|
+
) continue;
|
|
1512
|
+
if (
|
|
1513
|
+
candidate.isSymbolicLink()
|
|
1514
|
+
|| !candidate.isDirectory()
|
|
1515
|
+
|| !ownedByCurrentUser(candidate)
|
|
1516
|
+
|| !hasPrivateDirectoryMode(candidate)
|
|
1517
|
+
) {
|
|
1518
|
+
throw new Error("recursive removal quarantine is not a private current-user directory");
|
|
1519
|
+
}
|
|
1520
|
+
const pid = Number(pidText);
|
|
1521
|
+
if (
|
|
1522
|
+
!Number.isSafeInteger(pid)
|
|
1523
|
+
|| pid < 1
|
|
1524
|
+
|| pid > 2_147_483_647
|
|
1525
|
+
|| !processIsDefinitelyMissing(pid)
|
|
1526
|
+
) {
|
|
1527
|
+
throw new Error("recursive removal quarantine owner is live or cannot be proven dead");
|
|
1528
|
+
}
|
|
1529
|
+
if (removalSource !== name) {
|
|
1530
|
+
throw new Error("recursive removal identity has multiple quarantine paths");
|
|
1531
|
+
}
|
|
1532
|
+
removalSource = entry.name;
|
|
1533
|
+
}
|
|
1534
|
+
} finally {
|
|
1535
|
+
directory.closeSync();
|
|
1536
|
+
}
|
|
1537
|
+
if (removalSource === name) {
|
|
1538
|
+
return { ok: true, identity: actual, removed: false };
|
|
1539
|
+
}
|
|
1540
|
+
} else {
|
|
1541
|
+
if (targetExpected === null) {
|
|
1542
|
+
throw new Error("recursive removal target appeared where absence was expected");
|
|
1543
|
+
}
|
|
1544
|
+
if (
|
|
1545
|
+
target.isSymbolicLink()
|
|
1546
|
+
|| !target.isDirectory()
|
|
1547
|
+
|| !ownedByCurrentUser(target)
|
|
1548
|
+
|| !hasPrivateDirectoryMode(target)
|
|
1549
|
+
|| !sameIdentity(identity(target), targetExpected)
|
|
1550
|
+
|| (
|
|
1551
|
+
expectedTargetBirthtimeNs !== null
|
|
1552
|
+
&& target.birthtimeNs.toString() !== expectedTargetBirthtimeNs
|
|
1553
|
+
)
|
|
1554
|
+
) throw new Error("recursive removal target changed identity");
|
|
1555
|
+
}
|
|
1556
|
+
|
|
1557
|
+
if (targetExpected === null) {
|
|
1558
|
+
throw new Error("recursive removal target identity is unavailable");
|
|
1559
|
+
}
|
|
1560
|
+
const quarantine = `.io-remove-${process.pid}-${crypto.randomUUID()}.quarantine`;
|
|
1561
|
+
renameSync(removalSource, quarantine);
|
|
1562
|
+
const moved = lstatSync(quarantine, { bigint: true });
|
|
1563
|
+
if (
|
|
1564
|
+
!moved.isDirectory()
|
|
1565
|
+
|| moved.isSymbolicLink()
|
|
1566
|
+
|| !ownedByCurrentUser(moved)
|
|
1567
|
+
|| !hasPrivateDirectoryMode(moved)
|
|
1568
|
+
|| !sameIdentity(identity(moved), targetExpected)
|
|
1569
|
+
|| (
|
|
1570
|
+
expectedTargetBirthtimeNs !== null
|
|
1571
|
+
&& moved.birthtimeNs.toString() !== expectedTargetBirthtimeNs
|
|
1572
|
+
)
|
|
1573
|
+
) throw new Error("recursive removal quarantine changed identity");
|
|
1574
|
+
syncDirectory(".");
|
|
1575
|
+
pauseAfterRemoveQuarantineForTest();
|
|
1576
|
+
rmSync(quarantine, { recursive: true, force: false, maxRetries: 0 });
|
|
1577
|
+
syncDirectory(".");
|
|
1578
|
+
return { ok: true, identity: actual, removed: true };
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function execute(request: Request): Response {
|
|
1582
|
+
const operation = request.operation;
|
|
1583
|
+
if (operation.kind === "ensure-directories") {
|
|
1584
|
+
return ensureDirectories(
|
|
1585
|
+
request.expected,
|
|
1586
|
+
operation.segments,
|
|
1587
|
+
operation.directoryExpectations,
|
|
1588
|
+
operation.requireFinalPrivate,
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1591
|
+
if (operation.kind === "read-file") {
|
|
1592
|
+
return readFile(
|
|
1593
|
+
request.expected,
|
|
1594
|
+
operation.segments,
|
|
1595
|
+
operation.directoryExpectations,
|
|
1596
|
+
operation.maximumBytes,
|
|
1597
|
+
operation.fileExpectation,
|
|
1598
|
+
);
|
|
1599
|
+
}
|
|
1600
|
+
if (operation.kind === "list-directory") {
|
|
1601
|
+
return listDirectory(
|
|
1602
|
+
request.expected,
|
|
1603
|
+
operation.segments,
|
|
1604
|
+
operation.directoryExpectations,
|
|
1605
|
+
operation.maximumEntries,
|
|
1606
|
+
);
|
|
1607
|
+
}
|
|
1608
|
+
if (operation.kind === "snapshot-tree") {
|
|
1609
|
+
return snapshotTree(
|
|
1610
|
+
request.expected,
|
|
1611
|
+
operation.maximumEntries,
|
|
1612
|
+
operation.maximumDirectories,
|
|
1613
|
+
operation.maximumDepth,
|
|
1614
|
+
operation.maximumPathBytes,
|
|
1615
|
+
);
|
|
1616
|
+
}
|
|
1617
|
+
if (operation.kind === "batch-read-files") {
|
|
1618
|
+
return batchReadFiles(
|
|
1619
|
+
request.expected,
|
|
1620
|
+
operation.files,
|
|
1621
|
+
operation.maximumTotalBytes,
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
if (operation.kind === "write-file") {
|
|
1625
|
+
return writeFile(
|
|
1626
|
+
request.expected,
|
|
1627
|
+
operation.segments,
|
|
1628
|
+
operation.directoryExpectations,
|
|
1629
|
+
operation.content,
|
|
1630
|
+
operation.createOnly,
|
|
1631
|
+
operation.expectedContentSha256,
|
|
1632
|
+
operation.maximumExpectedContentBytes,
|
|
1633
|
+
request.requestId,
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
return removeDirectoryTree(
|
|
1637
|
+
request.expected,
|
|
1638
|
+
operation.segments,
|
|
1639
|
+
operation.directoryExpectations,
|
|
1640
|
+
operation.expectedTargetBirthtimeNs,
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
function main(): void {
|
|
1645
|
+
const request = parseRequest(JSON.parse(readBoundedStdin()) as unknown);
|
|
1646
|
+
process.stdout.write(`${JSON.stringify(execute(request))}\n`);
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
if (import.meta.main) {
|
|
1650
|
+
try {
|
|
1651
|
+
main();
|
|
1652
|
+
} catch (error) {
|
|
1653
|
+
process.stderr.write(`path helper: ${error instanceof Error ? error.message : "unknown failure"}\n`);
|
|
1654
|
+
process.exitCode = 1;
|
|
1655
|
+
}
|
|
1656
|
+
}
|