@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
package/src/har.ts
ADDED
|
@@ -0,0 +1,743 @@
|
|
|
1
|
+
import {
|
|
2
|
+
chmodSync,
|
|
3
|
+
closeSync,
|
|
4
|
+
constants,
|
|
5
|
+
copyFileSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
fsyncSync,
|
|
8
|
+
lstatSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
openSync,
|
|
11
|
+
opendirSync,
|
|
12
|
+
readdirSync,
|
|
13
|
+
renameSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
} from "node:fs";
|
|
16
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
canonicalJson,
|
|
20
|
+
isReviewedTemplateProtectedHostname,
|
|
21
|
+
sha256,
|
|
22
|
+
GHOSTGET_MANIFEST_SCHEMA_VERSION,
|
|
23
|
+
GHOSTGET_REVIEWED_TEMPLATE_MANIFEST_SCHEMA_VERSION,
|
|
24
|
+
GHOSTGET_WEB_SESSION_MANIFEST_SCHEMA_VERSION,
|
|
25
|
+
type GhostgetManifest,
|
|
26
|
+
} from "./model";
|
|
27
|
+
import {
|
|
28
|
+
platformSurfaceIds,
|
|
29
|
+
socialPlatformCatalog,
|
|
30
|
+
type PlatformSurfaceId,
|
|
31
|
+
} from "./platform-catalog";
|
|
32
|
+
import type { ProviderPluginRegistry } from "./provider-plugin-registry";
|
|
33
|
+
import { readRegularFile, writePrivateJson, type PrivateDirectoryIdentity } from "./storage";
|
|
34
|
+
|
|
35
|
+
export const MAX_HAR_BYTES = 128 * 1024 * 1024;
|
|
36
|
+
export const MAX_HAR_ENTRIES = 20_000;
|
|
37
|
+
const MAX_SCAFFOLD_ENTRIES = 10_000;
|
|
38
|
+
const MAX_SCAFFOLD_BYTES = 256 * 1024 * 1024;
|
|
39
|
+
const MAX_SCAFFOLD_DEPTH = 32;
|
|
40
|
+
const MAX_TRANSACTION_ARTIFACTS = 1_000;
|
|
41
|
+
|
|
42
|
+
export type JsonShape =
|
|
43
|
+
| "null"
|
|
44
|
+
| "boolean"
|
|
45
|
+
| "number"
|
|
46
|
+
| "string"
|
|
47
|
+
| { readonly array: JsonShape | "empty" }
|
|
48
|
+
| { readonly object: Readonly<Record<string, JsonShape>>; readonly truncated?: boolean };
|
|
49
|
+
|
|
50
|
+
export type HarCandidate = {
|
|
51
|
+
readonly method: string;
|
|
52
|
+
readonly origin: string;
|
|
53
|
+
readonly pathTemplate: string;
|
|
54
|
+
readonly firstParty: boolean;
|
|
55
|
+
readonly reviewRequired: true;
|
|
56
|
+
readonly sampleCount: number;
|
|
57
|
+
readonly statuses: readonly number[];
|
|
58
|
+
readonly mimeTypes: readonly string[];
|
|
59
|
+
readonly queryKeys: readonly string[];
|
|
60
|
+
readonly requestHeaderNames: readonly string[];
|
|
61
|
+
readonly requestBodyKind: "none" | "json" | "form" | "text";
|
|
62
|
+
readonly requestShape: JsonShape | null;
|
|
63
|
+
readonly responseShape: JsonShape | null;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type HarAnalysis = {
|
|
67
|
+
readonly schemaVersion: 1;
|
|
68
|
+
readonly adapterId: string;
|
|
69
|
+
readonly targetOrigin: string;
|
|
70
|
+
readonly browserDomains: readonly string[];
|
|
71
|
+
readonly analyzedAt: string;
|
|
72
|
+
readonly observedEntries: number;
|
|
73
|
+
readonly ignoredEntries: number;
|
|
74
|
+
readonly candidates: readonly HarCandidate[];
|
|
75
|
+
readonly warnings: readonly string[];
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export type ReviewedTemplateReservation = {
|
|
79
|
+
readonly schemaVersion: 1;
|
|
80
|
+
readonly state: "capture-required";
|
|
81
|
+
readonly targetOrigin: string;
|
|
82
|
+
readonly targetManifestSchemaVersion: 4 | 5;
|
|
83
|
+
readonly evidence: {
|
|
84
|
+
readonly path: "derivation.candidates.json";
|
|
85
|
+
readonly sha256: string;
|
|
86
|
+
};
|
|
87
|
+
readonly instructions: readonly string[];
|
|
88
|
+
readonly prohibited: readonly string[];
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
92
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Browser derivation is allowed for exact public HTTPS targets, including
|
|
96
|
+
* LinkedIn and X. Execution remains separately gated by reviewed manifests;
|
|
97
|
+
* recording traffic alone never authorizes or exposes a request capability.
|
|
98
|
+
*/
|
|
99
|
+
export function assertBrowserDerivationTargetAllowed(target: URL): void {
|
|
100
|
+
if (target.protocol !== "https:" || target.username !== "" || target.password !== "") {
|
|
101
|
+
throw new Error("browser derivation requires an exact public HTTPS target");
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const staticRouteSegments = new Set([
|
|
106
|
+
"api", "graphql", "rest", "rpc", "voyager", "messaging", "messages", "conversation", "conversations",
|
|
107
|
+
"inbox", "feed", "posts", "comments", "reactions", "search", "query", "mutation", "realtime", "presence",
|
|
108
|
+
"status", "statuses", "notifications", "profile", "profiles", "users", "member", "members", "dash", "internal",
|
|
109
|
+
"public", "private", "content", "read", "write", "send", "create", "update", "delete", "list", "detail",
|
|
110
|
+
"details", "home", "in",
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
function safeStaticSegment(value: string): boolean {
|
|
114
|
+
const lower = value.toLowerCase();
|
|
115
|
+
return staticRouteSegments.has(lower)
|
|
116
|
+
|| /^v\d{1,3}$/u.test(lower);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function safePathTemplate(pathname: string): string {
|
|
120
|
+
if (pathname.length > 4_096 || pathname.split("/").length > 128) return "/:oversized-path";
|
|
121
|
+
let parameter = 0;
|
|
122
|
+
const rendered = pathname.split("/").map((segment) => {
|
|
123
|
+
if (segment === "" || safeStaticSegment(segment)) return segment;
|
|
124
|
+
parameter += 1;
|
|
125
|
+
return `:segment${parameter}`;
|
|
126
|
+
}).join("/");
|
|
127
|
+
return rendered.startsWith("/") ? rendered : `/${rendered}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function mergeShapes(left: JsonShape | null, right: JsonShape | null): JsonShape | null {
|
|
131
|
+
if (left === null) return right;
|
|
132
|
+
if (right === null) return left;
|
|
133
|
+
if (typeof left === "string" || typeof right === "string") return left === right ? left : "string";
|
|
134
|
+
if ("array" in left && "array" in right) {
|
|
135
|
+
if (left.array === "empty") return right;
|
|
136
|
+
if (right.array === "empty") return left;
|
|
137
|
+
return { array: mergeShapes(left.array, right.array) ?? "empty" };
|
|
138
|
+
}
|
|
139
|
+
if ("object" in left && "object" in right) {
|
|
140
|
+
const keys = [...new Set([...Object.keys(left.object), ...Object.keys(right.object)])].sort().slice(0, 200);
|
|
141
|
+
const object: Record<string, JsonShape> = {};
|
|
142
|
+
for (const key of keys) {
|
|
143
|
+
object[key] = mergeShapes(left.object[key] ?? null, right.object[key] ?? null) ?? "null";
|
|
144
|
+
}
|
|
145
|
+
return { object, ...((left.truncated === true || right.truncated === true || keys.length >= 200) ? { truncated: true } : {}) };
|
|
146
|
+
}
|
|
147
|
+
return "string";
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function jsonShape(value: unknown, depth = 0): JsonShape {
|
|
151
|
+
if (value === null) return "null";
|
|
152
|
+
if (typeof value === "boolean") return "boolean";
|
|
153
|
+
if (typeof value === "number") return "number";
|
|
154
|
+
if (typeof value === "string") return "string";
|
|
155
|
+
if (depth >= 8) return "string";
|
|
156
|
+
if (Array.isArray(value)) {
|
|
157
|
+
let item: JsonShape | null = null;
|
|
158
|
+
for (const candidate of value.slice(0, 20)) item = mergeShapes(item, jsonShape(candidate, depth + 1));
|
|
159
|
+
return { array: item ?? "empty" };
|
|
160
|
+
}
|
|
161
|
+
if (isRecord(value)) {
|
|
162
|
+
const keys = Object.keys(value).sort();
|
|
163
|
+
const object: Record<string, JsonShape> = {};
|
|
164
|
+
for (const [index, key] of keys.slice(0, 200).entries()) {
|
|
165
|
+
// Object keys are data too: GraphQL aliases, form fields, and custom
|
|
166
|
+
// payloads routinely contain account IDs, emails, or message text.
|
|
167
|
+
object[`field${index + 1}`] = jsonShape(value[key], depth + 1);
|
|
168
|
+
}
|
|
169
|
+
return { object, ...(keys.length > 200 ? { truncated: true } : {}) };
|
|
170
|
+
}
|
|
171
|
+
return "string";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function parsedJson(text: unknown, maximumBytes = 2 * 1024 * 1024): unknown {
|
|
175
|
+
if (typeof text !== "string" || Buffer.byteLength(text, "utf8") > maximumBytes) return null;
|
|
176
|
+
try {
|
|
177
|
+
return JSON.parse(text) as unknown;
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const stableHeaderNames = new Set([
|
|
184
|
+
"accept", "accept-encoding", "accept-language", "cache-control", "content-length", "content-type",
|
|
185
|
+
"if-match", "if-modified-since", "if-none-match", "origin", "pragma", "range", "referer", "user-agent",
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
function headerCategory(name: string): string {
|
|
189
|
+
const lower = name.toLowerCase();
|
|
190
|
+
if (lower === "authorization" || lower === "proxy-authorization") return "authorization";
|
|
191
|
+
if (lower === "cookie" || lower === "set-cookie") return "cookie";
|
|
192
|
+
if (lower.includes("csrf") || lower.includes("xsrf")) return "csrf-token";
|
|
193
|
+
return stableHeaderNames.has(lower) ? lower : "custom-header";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function headerNames(value: unknown): readonly string[] {
|
|
197
|
+
if (!Array.isArray(value)) return [];
|
|
198
|
+
return [...new Set(value.flatMap((entry) => {
|
|
199
|
+
if (!isRecord(entry) || typeof entry.name !== "string") return [];
|
|
200
|
+
return /^[a-z0-9!#$%&'*+.^_`|~-]{1,128}$/iu.test(entry.name) ? [headerCategory(entry.name)] : [];
|
|
201
|
+
}))].sort().slice(0, 200);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function mimeCategory(value: unknown): string | null {
|
|
205
|
+
if (typeof value !== "string") return null;
|
|
206
|
+
const base = value.split(";", 1)[0]?.trim().toLowerCase() ?? "";
|
|
207
|
+
if ([
|
|
208
|
+
"application/json", "application/graphql-response+json", "application/xml", "application/octet-stream",
|
|
209
|
+
"text/html", "text/plain", "text/event-stream", "text/css", "text/xml",
|
|
210
|
+
].includes(base)) return base;
|
|
211
|
+
if (/^(?:image|audio|video|font)\/[a-z0-9.+-]{1,64}$/u.test(base)) return `${base.split("/", 1)[0]}/*`;
|
|
212
|
+
return base === "" ? null : "other";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function postData(value: unknown): {
|
|
216
|
+
readonly kind: HarCandidate["requestBodyKind"];
|
|
217
|
+
readonly shape: JsonShape | null;
|
|
218
|
+
} {
|
|
219
|
+
if (!isRecord(value)) return { kind: "none", shape: null };
|
|
220
|
+
const mimeType = typeof value.mimeType === "string" ? value.mimeType.toLowerCase() : "";
|
|
221
|
+
const text = typeof value.text === "string" ? value.text : null;
|
|
222
|
+
if (mimeType.includes("json")) {
|
|
223
|
+
const parsed = parsedJson(text);
|
|
224
|
+
return { kind: "json", shape: parsed === null ? null : jsonShape(parsed) };
|
|
225
|
+
}
|
|
226
|
+
if (mimeType.includes("x-www-form-urlencoded") || Array.isArray(value.params)) {
|
|
227
|
+
const count = Array.isArray(value.params)
|
|
228
|
+
? Math.min(200, new Set(value.params.flatMap((entry) => isRecord(entry) && typeof entry.name === "string" ? [entry.name] : [])).size)
|
|
229
|
+
: 0;
|
|
230
|
+
return {
|
|
231
|
+
kind: "form",
|
|
232
|
+
shape: { object: Object.fromEntries(Array.from({ length: count }, (_unused, index) => [`field${index + 1}`, "string" as const])) },
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
return { kind: text === null ? "none" : "text", shape: null };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function responseDetails(value: unknown): { readonly status: number | null; readonly mimeType: string | null; readonly shape: JsonShape | null } {
|
|
239
|
+
if (!isRecord(value)) return { status: null, mimeType: null, shape: null };
|
|
240
|
+
const status = typeof value.status === "number" && Number.isSafeInteger(value.status) ? value.status : null;
|
|
241
|
+
const content = isRecord(value.content) ? value.content : null;
|
|
242
|
+
const rawMimeType = content !== null && typeof content.mimeType === "string" ? content.mimeType : null;
|
|
243
|
+
const mimeType = mimeCategory(rawMimeType);
|
|
244
|
+
const parsed = content === null || rawMimeType?.toLowerCase().includes("json") !== true ? null : parsedJson(content.text);
|
|
245
|
+
return { status, mimeType, shape: parsed === null ? null : jsonShape(parsed) };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
type CandidateAccumulator = {
|
|
249
|
+
method: string;
|
|
250
|
+
origin: string;
|
|
251
|
+
pathTemplate: string;
|
|
252
|
+
firstParty: boolean;
|
|
253
|
+
sampleCount: number;
|
|
254
|
+
statuses: Set<number>;
|
|
255
|
+
mimeTypes: Set<string>;
|
|
256
|
+
queryKeys: Set<string>;
|
|
257
|
+
requestHeaderNames: Set<string>;
|
|
258
|
+
requestBodyKind: HarCandidate["requestBodyKind"];
|
|
259
|
+
requestShape: JsonShape | null;
|
|
260
|
+
responseShape: JsonShape | null;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
function parseHarRoot(value: unknown): readonly unknown[] {
|
|
264
|
+
if (!isRecord(value) || !isRecord(value.log) || !Array.isArray(value.log.entries)) throw new Error("HAR must contain log.entries[]");
|
|
265
|
+
if (value.log.entries.length > MAX_HAR_ENTRIES) throw new Error(`HAR contains more than ${MAX_HAR_ENTRIES} entries`);
|
|
266
|
+
return value.log.entries;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function normalizedBrowserDomains(values: readonly string[] | undefined, hostname: string): readonly string[] {
|
|
270
|
+
const domains = values ?? [hostname];
|
|
271
|
+
if (
|
|
272
|
+
domains.length < 1
|
|
273
|
+
|| domains.length > 100
|
|
274
|
+
|| domains.some((domain) => !/^(?:\*\.)?[a-z0-9](?:[a-z0-9.-]{0,251}[a-z0-9])?$/u.test(domain) || domain.includes(".."))
|
|
275
|
+
) throw new Error("browser domains must contain 1-100 exact or wildcard hostnames");
|
|
276
|
+
const normalized = [...new Set(domains.map((domain) => domain.toLowerCase()))];
|
|
277
|
+
if (!normalized.some((domain) => domain === hostname || (domain.startsWith("*.") && (hostname === domain.slice(2) || hostname.endsWith(`.${domain.slice(2)}`))))) {
|
|
278
|
+
throw new Error("browser domains must cover the target hostname");
|
|
279
|
+
}
|
|
280
|
+
return normalized;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Analyze a bounded HAR without retaining headers, cookies, body values, URLs, or private response content. */
|
|
284
|
+
export function analyzeHarValue(
|
|
285
|
+
value: unknown,
|
|
286
|
+
adapterId: string,
|
|
287
|
+
targetOrigin: string,
|
|
288
|
+
now = new Date(),
|
|
289
|
+
browserDomains?: readonly string[],
|
|
290
|
+
): HarAnalysis {
|
|
291
|
+
const target = new URL(targetOrigin);
|
|
292
|
+
if (target.protocol !== "https:" || target.origin !== targetOrigin) throw new Error("target origin must be an exact HTTPS origin");
|
|
293
|
+
assertBrowserDerivationTargetAllowed(target);
|
|
294
|
+
const entries = parseHarRoot(value);
|
|
295
|
+
const domains = normalizedBrowserDomains(browserDomains, target.hostname.toLowerCase());
|
|
296
|
+
const candidates = new Map<string, CandidateAccumulator>();
|
|
297
|
+
let ignoredEntries = 0;
|
|
298
|
+
for (const rawEntry of entries) {
|
|
299
|
+
if (!isRecord(rawEntry) || !isRecord(rawEntry.request)) {
|
|
300
|
+
ignoredEntries += 1;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const rawMethod = typeof rawEntry.request.method === "string" ? rawEntry.request.method.toUpperCase() : "";
|
|
304
|
+
const method = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "CONNECT", "TRACE"].includes(rawMethod)
|
|
305
|
+
? rawMethod
|
|
306
|
+
: "OTHER";
|
|
307
|
+
const rawUrl = rawEntry.request.url;
|
|
308
|
+
if (!/^[A-Z]{3,12}$/u.test(rawMethod) || typeof rawUrl !== "string" || rawUrl.length > 64 * 1024) {
|
|
309
|
+
ignoredEntries += 1;
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
let url: URL;
|
|
313
|
+
try {
|
|
314
|
+
url = new URL(rawUrl);
|
|
315
|
+
} catch {
|
|
316
|
+
ignoredEntries += 1;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if ((url.protocol !== "https:" && url.protocol !== "http:") || url.username !== "" || url.password !== "") {
|
|
320
|
+
ignoredEntries += 1;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const pathTemplate = safePathTemplate(url.pathname);
|
|
324
|
+
const key = `${method}\0${url.origin}\0${pathTemplate}`;
|
|
325
|
+
const requestBody = postData(rawEntry.request.postData);
|
|
326
|
+
const response = responseDetails(rawEntry.response);
|
|
327
|
+
if (url.origin !== target.origin) {
|
|
328
|
+
ignoredEntries += 1;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (!candidates.has(key) && candidates.size >= 5_000) {
|
|
332
|
+
ignoredEntries += 1;
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const current = candidates.get(key) ?? {
|
|
336
|
+
method,
|
|
337
|
+
origin: url.origin,
|
|
338
|
+
pathTemplate,
|
|
339
|
+
firstParty: url.origin === target.origin,
|
|
340
|
+
sampleCount: 0,
|
|
341
|
+
statuses: new Set<number>(),
|
|
342
|
+
mimeTypes: new Set<string>(),
|
|
343
|
+
queryKeys: new Set<string>(),
|
|
344
|
+
requestHeaderNames: new Set<string>(),
|
|
345
|
+
requestBodyKind: requestBody.kind,
|
|
346
|
+
requestShape: null,
|
|
347
|
+
responseShape: null,
|
|
348
|
+
};
|
|
349
|
+
current.sampleCount += 1;
|
|
350
|
+
if (response.status !== null && current.statuses.size < 50) current.statuses.add(response.status);
|
|
351
|
+
if (response.mimeType !== null && current.mimeTypes.size < 100) current.mimeTypes.add(response.mimeType);
|
|
352
|
+
const queryCount = Math.min(200, new Set(url.searchParams.keys()).size);
|
|
353
|
+
for (let index = 0; index < queryCount; index += 1) {
|
|
354
|
+
current.queryKeys.add(`query${index + 1}`);
|
|
355
|
+
}
|
|
356
|
+
for (const name of headerNames(rawEntry.request.headers)) {
|
|
357
|
+
if (current.requestHeaderNames.size >= 200) break;
|
|
358
|
+
current.requestHeaderNames.add(name);
|
|
359
|
+
}
|
|
360
|
+
if (current.requestBodyKind !== requestBody.kind) current.requestBodyKind = "text";
|
|
361
|
+
current.requestShape = mergeShapes(current.requestShape, requestBody.shape);
|
|
362
|
+
current.responseShape = mergeShapes(current.responseShape, response.shape);
|
|
363
|
+
candidates.set(key, current);
|
|
364
|
+
}
|
|
365
|
+
const output = [...candidates.values()]
|
|
366
|
+
.filter((candidate) => !/\.(?:css|js|png|jpe?g|gif|svg|woff2?|ico|map)$/iu.test(candidate.pathTemplate))
|
|
367
|
+
.sort((left, right) => Number(right.firstParty) - Number(left.firstParty) || right.sampleCount - left.sampleCount || left.pathTemplate.localeCompare(right.pathTemplate))
|
|
368
|
+
.slice(0, 2_000)
|
|
369
|
+
.map((candidate): HarCandidate => ({
|
|
370
|
+
method: candidate.method,
|
|
371
|
+
origin: candidate.origin,
|
|
372
|
+
pathTemplate: candidate.pathTemplate,
|
|
373
|
+
firstParty: candidate.firstParty,
|
|
374
|
+
reviewRequired: true,
|
|
375
|
+
sampleCount: candidate.sampleCount,
|
|
376
|
+
statuses: [...candidate.statuses].sort((left, right) => left - right),
|
|
377
|
+
mimeTypes: [...candidate.mimeTypes].sort(),
|
|
378
|
+
queryKeys: [...candidate.queryKeys].sort(),
|
|
379
|
+
requestHeaderNames: [...candidate.requestHeaderNames].sort(),
|
|
380
|
+
requestBodyKind: candidate.requestBodyKind,
|
|
381
|
+
requestShape: candidate.requestShape,
|
|
382
|
+
responseShape: candidate.responseShape,
|
|
383
|
+
}));
|
|
384
|
+
return {
|
|
385
|
+
schemaVersion: 1,
|
|
386
|
+
adapterId,
|
|
387
|
+
targetOrigin,
|
|
388
|
+
browserDomains: domains,
|
|
389
|
+
analyzedAt: now.toISOString(),
|
|
390
|
+
observedEntries: entries.length,
|
|
391
|
+
ignoredEntries,
|
|
392
|
+
candidates: output,
|
|
393
|
+
warnings: [
|
|
394
|
+
"Every candidate is disabled until a human or agent reviews its semantics; HTTP method does not establish whether an endpoint mutates state.",
|
|
395
|
+
"This report keeps only exact-target-origin structural route tokens and abstract field/header/query categories; it omits cross-origin paths, URLs, path values, field names, query names and values, custom header names and values, cookies, request values, response values, and raw bodies.",
|
|
396
|
+
],
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function analyzeHarFile(
|
|
401
|
+
path: string,
|
|
402
|
+
adapterId: string,
|
|
403
|
+
targetOrigin: string,
|
|
404
|
+
browserDomains?: readonly string[],
|
|
405
|
+
expectedParent?: Readonly<PrivateDirectoryIdentity>,
|
|
406
|
+
): HarAnalysis {
|
|
407
|
+
const target = new URL(targetOrigin);
|
|
408
|
+
if (target.protocol !== "https:" || target.origin !== targetOrigin) throw new Error("target origin must be an exact HTTPS origin");
|
|
409
|
+
assertBrowserDerivationTargetAllowed(target);
|
|
410
|
+
return analyzeHarValue(
|
|
411
|
+
JSON.parse(readRegularFile(path, MAX_HAR_BYTES, "HAR input", expectedParent)) as unknown,
|
|
412
|
+
adapterId,
|
|
413
|
+
targetOrigin,
|
|
414
|
+
new Date(),
|
|
415
|
+
browserDomains,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function emptyManifest(
|
|
420
|
+
adapterId: string,
|
|
421
|
+
targetOrigin: string,
|
|
422
|
+
browserDomains?: readonly string[],
|
|
423
|
+
surfaceId?: PlatformSurfaceId,
|
|
424
|
+
): GhostgetManifest {
|
|
425
|
+
if (!/^[a-z][a-z0-9-]{0,47}$/u.test(adapterId)) throw new Error("adapter ID must be lowercase kebab-case");
|
|
426
|
+
const origin = new URL(targetOrigin);
|
|
427
|
+
if (origin.protocol !== "https:" || origin.origin !== targetOrigin) throw new Error("target origin must be an exact HTTPS origin");
|
|
428
|
+
const inferredSurfaces = platformSurfaceIds.filter((candidate) =>
|
|
429
|
+
socialPlatformCatalog[candidate].originPolicy.exactOrigins.includes(targetOrigin as `https://${string}`));
|
|
430
|
+
if (surfaceId === undefined && inferredSurfaces.length > 1) {
|
|
431
|
+
throw new Error(
|
|
432
|
+
`target origin matches multiple reviewed surfaces (${inferredSurfaces.join(", ")}); select one with --platform`,
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
if (surfaceId !== undefined && inferredSurfaces.length > 0 && !inferredSurfaces.includes(surfaceId)) {
|
|
436
|
+
throw new Error(`target origin belongs to ${inferredSurfaces.join(", ")}, not the selected ${surfaceId} surface`);
|
|
437
|
+
}
|
|
438
|
+
const effectiveSurfaceId = surfaceId ?? (inferredSurfaces.length === 1 ? inferredSurfaces[0] : undefined);
|
|
439
|
+
let origins = [targetOrigin];
|
|
440
|
+
let domains = normalizedBrowserDomains(browserDomains, origin.hostname.toLowerCase());
|
|
441
|
+
if (effectiveSurfaceId !== undefined) {
|
|
442
|
+
const surface = socialPlatformCatalog[effectiveSurfaceId];
|
|
443
|
+
const exact = new Set<string>(surface.originPolicy.exactOrigins);
|
|
444
|
+
if (!exact.has(targetOrigin)) {
|
|
445
|
+
if (surface.originPolicy.additionalExactOrigins.state === "forbidden") {
|
|
446
|
+
throw new Error(`target origin is outside the reviewed ${effectiveSurfaceId} origin policy`);
|
|
447
|
+
}
|
|
448
|
+
const baseOrigin = surface.originPolicy.exactOrigins[0];
|
|
449
|
+
if (baseOrigin === undefined) throw new Error(`${effectiveSurfaceId} has no reviewed base origin`);
|
|
450
|
+
origins = [baseOrigin, targetOrigin];
|
|
451
|
+
domains = normalizedBrowserDomains([
|
|
452
|
+
...domains,
|
|
453
|
+
new URL(baseOrigin).hostname.toLowerCase(),
|
|
454
|
+
], origin.hostname.toLowerCase());
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return {
|
|
458
|
+
schemaVersion: GHOSTGET_MANIFEST_SCHEMA_VERSION,
|
|
459
|
+
id: adapterId,
|
|
460
|
+
version: "0.1.0",
|
|
461
|
+
displayName: adapterId.split("-").map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1)}`).join(" "),
|
|
462
|
+
...(effectiveSurfaceId === undefined ? {} : { surfaceId: effectiveSurfaceId }),
|
|
463
|
+
origins,
|
|
464
|
+
browserDomains: domains,
|
|
465
|
+
operations: {},
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function derivedManifest(
|
|
470
|
+
adapterId: string,
|
|
471
|
+
targetOrigin: string,
|
|
472
|
+
browserDomains: readonly string[],
|
|
473
|
+
registry: ProviderPluginRegistry,
|
|
474
|
+
surfaceId?: PlatformSurfaceId,
|
|
475
|
+
): GhostgetManifest {
|
|
476
|
+
const base = emptyManifest(adapterId, targetOrigin, browserDomains, surfaceId);
|
|
477
|
+
const protectedHostname = isReviewedTemplateProtectedHostname(
|
|
478
|
+
new URL(targetOrigin).hostname,
|
|
479
|
+
registry,
|
|
480
|
+
);
|
|
481
|
+
return {
|
|
482
|
+
...base,
|
|
483
|
+
schemaVersion: base.surfaceId === "linkedin" || base.surfaceId === "x" || protectedHostname
|
|
484
|
+
? GHOSTGET_WEB_SESSION_MANIFEST_SCHEMA_VERSION
|
|
485
|
+
: GHOSTGET_REVIEWED_TEMPLATE_MANIFEST_SCHEMA_VERSION,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
export function reviewedTemplateReservation(analysis: HarAnalysis, manifest: GhostgetManifest): ReviewedTemplateReservation {
|
|
490
|
+
const codeOwned = manifest.schemaVersion === GHOSTGET_WEB_SESSION_MANIFEST_SCHEMA_VERSION;
|
|
491
|
+
return {
|
|
492
|
+
schemaVersion: 1,
|
|
493
|
+
state: "capture-required",
|
|
494
|
+
targetOrigin: analysis.targetOrigin,
|
|
495
|
+
targetManifestSchemaVersion: codeOwned ? 4 : 5,
|
|
496
|
+
evidence: {
|
|
497
|
+
path: "derivation.candidates.json",
|
|
498
|
+
sha256: sha256(canonicalJson(analysis)),
|
|
499
|
+
},
|
|
500
|
+
instructions: codeOwned
|
|
501
|
+
? [
|
|
502
|
+
"This origin requires a code-owned schemaVersion 4 contract; do not convert captured traffic into a generic executable template.",
|
|
503
|
+
"Use the capture only as review evidence when updating the provider plugin that owns this protected hostname family.",
|
|
504
|
+
]
|
|
505
|
+
: [
|
|
506
|
+
"Choose one semantic operation and classify its R1, R2, or R3 risk from behavior, never from its HTTP method.",
|
|
507
|
+
"Review exact request method, path segments, typed input sinks, non-secret fixed headers, cookie/CSRF sources, response variants, projections, and target bindings.",
|
|
508
|
+
"Keep every schemaVersion 5 operation at reviewedTemplate.state capture-required; version 1 is an inert reservation and cannot plan or execute even an R1 read.",
|
|
509
|
+
"Implement a code-owned contract for executable operations today; a future reviewed-template contractVersion 2 must add a reviewed current-account identity preflight and response-scope binding.",
|
|
510
|
+
"Install only after an explicit review; this capture-required reservation is intentionally non-executable.",
|
|
511
|
+
],
|
|
512
|
+
prohibited: [
|
|
513
|
+
"Do not copy cookies, authorization values, CSRF values, request values, response values, or raw HAR bodies into the manifest.",
|
|
514
|
+
"Do not infer an executable template or promote a candidate automatically from this structural report.",
|
|
515
|
+
"Do not use wildcard origins, arbitrary URLs, arbitrary headers, scripts, eval, or DOM automation.",
|
|
516
|
+
],
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function syncPath(path: string, directory: boolean): void {
|
|
521
|
+
if (process.platform === "win32") return;
|
|
522
|
+
const noFollow = "O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0;
|
|
523
|
+
const directoryOnly = directory && "O_DIRECTORY" in constants ? constants.O_DIRECTORY : 0;
|
|
524
|
+
const descriptor = openSync(path, constants.O_RDONLY | noFollow | directoryOnly);
|
|
525
|
+
try {
|
|
526
|
+
fsyncSync(descriptor);
|
|
527
|
+
} finally {
|
|
528
|
+
closeSync(descriptor);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function copyScaffoldTree(
|
|
533
|
+
source: string,
|
|
534
|
+
target: string,
|
|
535
|
+
budget: { entries: number; bytes: number },
|
|
536
|
+
depth = 0,
|
|
537
|
+
): void {
|
|
538
|
+
if (depth > MAX_SCAFFOLD_DEPTH) throw new Error(`scaffold directory exceeds depth ${MAX_SCAFFOLD_DEPTH}`);
|
|
539
|
+
const sourceStats = lstatSync(source);
|
|
540
|
+
if (!sourceStats.isDirectory() || sourceStats.isSymbolicLink()) throw new Error(`scaffold source must be a real directory: ${source}`);
|
|
541
|
+
mkdirSync(target, { mode: 0o700 });
|
|
542
|
+
for (const entry of readdirSync(source, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
543
|
+
budget.entries += 1;
|
|
544
|
+
if (budget.entries > MAX_SCAFFOLD_ENTRIES) throw new Error(`scaffold directory exceeds ${MAX_SCAFFOLD_ENTRIES} entries`);
|
|
545
|
+
const sourcePath = join(source, entry.name);
|
|
546
|
+
const targetPath = join(target, entry.name);
|
|
547
|
+
const stats = lstatSync(sourcePath);
|
|
548
|
+
if (stats.isSymbolicLink()) throw new Error(`scaffold directory contains a symbolic link: ${sourcePath}`);
|
|
549
|
+
if (stats.isDirectory()) {
|
|
550
|
+
copyScaffoldTree(sourcePath, targetPath, budget, depth + 1);
|
|
551
|
+
continue;
|
|
552
|
+
}
|
|
553
|
+
if (!stats.isFile()) throw new Error(`scaffold directory contains a non-regular entry: ${sourcePath}`);
|
|
554
|
+
budget.bytes += stats.size;
|
|
555
|
+
if (budget.bytes > MAX_SCAFFOLD_BYTES) throw new Error(`scaffold directory exceeds ${MAX_SCAFFOLD_BYTES} bytes`);
|
|
556
|
+
copyFileSync(sourcePath, targetPath);
|
|
557
|
+
chmodSync(targetPath, stats.mode & 0o777);
|
|
558
|
+
syncPath(targetPath, false);
|
|
559
|
+
}
|
|
560
|
+
chmodSync(target, 0o700);
|
|
561
|
+
syncPath(target, true);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function processIsAlive(pid: number): boolean {
|
|
565
|
+
try {
|
|
566
|
+
process.kill(pid, 0);
|
|
567
|
+
return true;
|
|
568
|
+
} catch (error) {
|
|
569
|
+
return !(typeof error === "object" && error !== null && "code" in error && error.code === "ESRCH");
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function transactionArtifacts(parent: string, outputName: string): readonly {
|
|
574
|
+
readonly path: string;
|
|
575
|
+
readonly kind: "stage" | "backup";
|
|
576
|
+
readonly pid: number;
|
|
577
|
+
}[] {
|
|
578
|
+
const prefixes = [
|
|
579
|
+
`.${outputName}.wrench-scaffold-`,
|
|
580
|
+
`.${outputName}.oh-scaffold-`,
|
|
581
|
+
] as const;
|
|
582
|
+
const artifacts: { path: string; kind: "stage" | "backup"; pid: number }[] = [];
|
|
583
|
+
let overflow = false;
|
|
584
|
+
const directory = opendirSync(parent);
|
|
585
|
+
try {
|
|
586
|
+
for (;;) {
|
|
587
|
+
const entry = directory.readSync();
|
|
588
|
+
if (entry === null) break;
|
|
589
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
|
|
590
|
+
const prefix = prefixes.find((candidate) => entry.name.startsWith(candidate));
|
|
591
|
+
if (prefix === undefined) continue;
|
|
592
|
+
const suffix = entry.name.slice(prefix.length);
|
|
593
|
+
const match = /^(stage|backup)-(\d+)-[0-9a-f-]{36}$/u.exec(suffix);
|
|
594
|
+
if (match === null) continue;
|
|
595
|
+
const pid = Number(match[2]);
|
|
596
|
+
if (!Number.isSafeInteger(pid) || pid < 1) continue;
|
|
597
|
+
if (artifacts.length >= MAX_TRANSACTION_ARTIFACTS) {
|
|
598
|
+
overflow = true;
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
artifacts.push({ path: join(parent, entry.name), kind: match[1] as "stage" | "backup", pid });
|
|
602
|
+
}
|
|
603
|
+
} finally {
|
|
604
|
+
directory.closeSync();
|
|
605
|
+
}
|
|
606
|
+
if (overflow) throw new Error(`scaffold output parent contains more than ${MAX_TRANSACTION_ARTIFACTS} transaction artifacts`);
|
|
607
|
+
return artifacts;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function assertParentTransactionCapacity(parent: string, outputExists: boolean): void {
|
|
611
|
+
let entries = 0;
|
|
612
|
+
const directory = opendirSync(parent);
|
|
613
|
+
try {
|
|
614
|
+
for (;;) {
|
|
615
|
+
const entry = directory.readSync();
|
|
616
|
+
if (entry === null) break;
|
|
617
|
+
entries += 1;
|
|
618
|
+
const maximumExisting = outputExists ? MAX_SCAFFOLD_ENTRIES - 1 : MAX_SCAFFOLD_ENTRIES - 2;
|
|
619
|
+
if (entries > maximumExisting) {
|
|
620
|
+
throw new Error(`scaffold output parent lacks bounded capacity for an atomic transaction (limit ${MAX_SCAFFOLD_ENTRIES})`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
} finally {
|
|
624
|
+
directory.closeSync();
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function recoverScaffoldTransactions(parent: string, output: string): void {
|
|
629
|
+
const artifacts = transactionArtifacts(parent, basename(output));
|
|
630
|
+
const active = artifacts.find((artifact) => processIsAlive(artifact.pid));
|
|
631
|
+
if (active !== undefined) throw new Error(`another scaffold writer owns a transaction for: ${output}`);
|
|
632
|
+
const backups = artifacts.filter((artifact) => artifact.kind === "backup");
|
|
633
|
+
if (!existsSync(output) && backups.length > 1) {
|
|
634
|
+
throw new Error(`multiple interrupted scaffold backups require manual recovery beside: ${output}`);
|
|
635
|
+
}
|
|
636
|
+
if (!existsSync(output) && backups[0] !== undefined) {
|
|
637
|
+
renameSync(backups[0].path, output);
|
|
638
|
+
syncPath(parent, true);
|
|
639
|
+
}
|
|
640
|
+
for (const artifact of artifacts) {
|
|
641
|
+
if (artifact.path === backups[0]?.path && existsSync(output)) {
|
|
642
|
+
if (!existsSync(artifact.path)) continue;
|
|
643
|
+
}
|
|
644
|
+
rmSync(artifact.path, { recursive: true, force: true });
|
|
645
|
+
}
|
|
646
|
+
if (artifacts.length > 0) syncPath(parent, true);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export function writeDerivationScaffold(
|
|
650
|
+
outputDirectory: string,
|
|
651
|
+
analysis: HarAnalysis,
|
|
652
|
+
options: {
|
|
653
|
+
readonly force: boolean;
|
|
654
|
+
readonly registry: ProviderPluginRegistry;
|
|
655
|
+
readonly surfaceId?: PlatformSurfaceId;
|
|
656
|
+
readonly extraFiles?: Readonly<Record<string, unknown>>;
|
|
657
|
+
},
|
|
658
|
+
): {
|
|
659
|
+
readonly manifestPath: string;
|
|
660
|
+
readonly candidatesPath: string;
|
|
661
|
+
readonly reservationPath: string;
|
|
662
|
+
} {
|
|
663
|
+
const target = new URL(analysis.targetOrigin);
|
|
664
|
+
if (target.protocol !== "https:" || target.origin !== analysis.targetOrigin) {
|
|
665
|
+
throw new Error("target origin must be an exact HTTPS origin");
|
|
666
|
+
}
|
|
667
|
+
assertBrowserDerivationTargetAllowed(target);
|
|
668
|
+
const output = resolve(outputDirectory);
|
|
669
|
+
const parent = dirname(output);
|
|
670
|
+
if (parent === output) throw new Error("scaffold output cannot be a filesystem root");
|
|
671
|
+
mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
672
|
+
const parentStats = lstatSync(parent);
|
|
673
|
+
if (!parentStats.isDirectory() || parentStats.isSymbolicLink()) throw new Error(`output parent must be a real directory: ${parent}`);
|
|
674
|
+
recoverScaffoldTransactions(parent, output);
|
|
675
|
+
assertParentTransactionCapacity(parent, existsSync(output));
|
|
676
|
+
const token = `${process.pid}-${crypto.randomUUID()}`;
|
|
677
|
+
const manifestPath = join(output, "wrench-adapter.json");
|
|
678
|
+
const candidatesPath = join(output, "derivation.candidates.json");
|
|
679
|
+
const reservationPath = join(output, "reviewed-template.reservation.json");
|
|
680
|
+
const staging = join(parent, `.${basename(output)}.wrench-scaffold-stage-${token}`);
|
|
681
|
+
let backup: string | null = null;
|
|
682
|
+
try {
|
|
683
|
+
assertScaffoldOutput(output, options.force);
|
|
684
|
+
if (existsSync(output)) copyScaffoldTree(output, staging, { entries: 0, bytes: 0 });
|
|
685
|
+
else {
|
|
686
|
+
mkdirSync(staging, { mode: 0o700 });
|
|
687
|
+
chmodSync(staging, 0o700);
|
|
688
|
+
}
|
|
689
|
+
const manifest = derivedManifest(
|
|
690
|
+
analysis.adapterId,
|
|
691
|
+
analysis.targetOrigin,
|
|
692
|
+
analysis.browserDomains,
|
|
693
|
+
options.registry,
|
|
694
|
+
options.surfaceId,
|
|
695
|
+
);
|
|
696
|
+
writePrivateJson(join(staging, "wrench-adapter.json"), manifest);
|
|
697
|
+
writePrivateJson(join(staging, "derivation.candidates.json"), analysis);
|
|
698
|
+
writePrivateJson(
|
|
699
|
+
join(staging, "reviewed-template.reservation.json"),
|
|
700
|
+
reviewedTemplateReservation(analysis, manifest),
|
|
701
|
+
);
|
|
702
|
+
for (const [name, contents] of Object.entries(options.extraFiles ?? {})) {
|
|
703
|
+
if (!/^[a-z][a-z0-9.-]{0,100}\.json$/u.test(name) || name.includes("..")) {
|
|
704
|
+
throw new Error("scaffold extra file has an unsafe name");
|
|
705
|
+
}
|
|
706
|
+
writePrivateJson(join(staging, name), contents);
|
|
707
|
+
}
|
|
708
|
+
syncPath(staging, true);
|
|
709
|
+
if (existsSync(output)) {
|
|
710
|
+
backup = join(parent, `.${basename(output)}.wrench-scaffold-backup-${token}`);
|
|
711
|
+
renameSync(output, backup);
|
|
712
|
+
syncPath(parent, true);
|
|
713
|
+
}
|
|
714
|
+
try {
|
|
715
|
+
renameSync(staging, output);
|
|
716
|
+
syncPath(parent, true);
|
|
717
|
+
} catch (error) {
|
|
718
|
+
if (backup !== null && !existsSync(output) && existsSync(backup)) {
|
|
719
|
+
renameSync(backup, output);
|
|
720
|
+
backup = null;
|
|
721
|
+
syncPath(parent, true);
|
|
722
|
+
}
|
|
723
|
+
throw error;
|
|
724
|
+
}
|
|
725
|
+
if (backup !== null) {
|
|
726
|
+
rmSync(backup, { recursive: true, force: true });
|
|
727
|
+
backup = null;
|
|
728
|
+
syncPath(parent, true);
|
|
729
|
+
}
|
|
730
|
+
return { manifestPath, candidatesPath, reservationPath };
|
|
731
|
+
} finally {
|
|
732
|
+
if (existsSync(staging)) rmSync(staging, { recursive: true, force: true });
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
export function assertScaffoldOutput(outputDirectory: string, force: boolean): void {
|
|
737
|
+
const output = resolve(outputDirectory);
|
|
738
|
+
if (existsSync(output) && !force) throw new Error(`output already exists: ${output}`);
|
|
739
|
+
if (existsSync(output)) {
|
|
740
|
+
const stats = lstatSync(output);
|
|
741
|
+
if (!stats.isDirectory() || stats.isSymbolicLink()) throw new Error(`output must be a real directory: ${output}`);
|
|
742
|
+
}
|
|
743
|
+
}
|