@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,1072 @@
|
|
|
1
|
+
import { constants, type Stats } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
chmod,
|
|
4
|
+
lstat,
|
|
5
|
+
mkdtemp,
|
|
6
|
+
open,
|
|
7
|
+
readdir,
|
|
8
|
+
realpath,
|
|
9
|
+
rmdir,
|
|
10
|
+
type FileHandle,
|
|
11
|
+
} from "node:fs/promises";
|
|
12
|
+
import { basename, dirname, isAbsolute, resolve, sep } from "node:path";
|
|
13
|
+
import { createHash } from "node:crypto";
|
|
14
|
+
import { dlopen, ptr } from "bun:ffi";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
exportBeeperMessageLikeMeBundle,
|
|
18
|
+
type BeeperMessageLikeMeArtifact,
|
|
19
|
+
type BeeperMessageLikeMeBundleProgress,
|
|
20
|
+
type BeeperMessageLikeMeExportResult,
|
|
21
|
+
} from "./beeper-message-like-me-export";
|
|
22
|
+
import {
|
|
23
|
+
createBeeperMessageLikeMeDirectoryLease,
|
|
24
|
+
releaseBeeperMessageLikeMeDirectoryLease,
|
|
25
|
+
type BeeperMessageLikeMeDirectoryLease,
|
|
26
|
+
} from "./beeper-message-like-me-recovery";
|
|
27
|
+
import { parseBeeperMessageLikeMeRecord } from "./beeper-message-bundle-v1";
|
|
28
|
+
import { canonicalJson, sha256 } from "./canonical-json";
|
|
29
|
+
import { removePrivateDirectoryTree } from "./storage";
|
|
30
|
+
import {
|
|
31
|
+
WHATSAPP_MESSAGE_BUNDLE_V2_ARTIFACTS,
|
|
32
|
+
WHATSAPP_MESSAGE_BUNDLE_V2_FORMAT,
|
|
33
|
+
WHATSAPP_MESSAGE_BUNDLE_V2_LIMITS,
|
|
34
|
+
WHATSAPP_MESSAGE_BUNDLE_V2_PROVIDER,
|
|
35
|
+
WHATSAPP_MESSAGE_BUNDLE_V2_SCHEMA_VERSION,
|
|
36
|
+
WHATSAPP_MESSAGE_BUNDLE_V2_SOURCE,
|
|
37
|
+
parseWhatsAppMessageBundleV2Completion,
|
|
38
|
+
parseWhatsAppMessageBundleV2Descriptor,
|
|
39
|
+
parseWhatsAppMessageBundleV2Manifest,
|
|
40
|
+
parseWhatsAppMessageBundleV2Record,
|
|
41
|
+
toLocalMessageBundleV1Record,
|
|
42
|
+
whatsAppMessageBundleV2BundleSha256,
|
|
43
|
+
type WhatsAppMessageBundleV2Artifact,
|
|
44
|
+
type WhatsAppMessageBundleV2Manifest,
|
|
45
|
+
} from "./whatsapp-message-bundle-v2";
|
|
46
|
+
import type { WhatsAppMessageLikeMeExportSource } from "./whatsapp-message-like-me-source";
|
|
47
|
+
|
|
48
|
+
const PRIVATE_DIRECTORY_MODE = 0o700;
|
|
49
|
+
const PRIVATE_FILE_MODE = 0o600;
|
|
50
|
+
const DARWIN_RENAME_EXCL = 0x0000_0004;
|
|
51
|
+
const LINUX_RENAME_NOREPLACE = 0x0000_0001;
|
|
52
|
+
const AT_FDCWD = -100;
|
|
53
|
+
|
|
54
|
+
type NativeExclusiveRename = (source: Uint8Array, destination: Uint8Array) => number;
|
|
55
|
+
let cachedRename: NativeExclusiveRename | undefined;
|
|
56
|
+
|
|
57
|
+
export type WhatsAppMessageLikeMeBundleProgress =
|
|
58
|
+
| BeeperMessageLikeMeBundleProgress
|
|
59
|
+
| Readonly<{ phase: "v2-conversion"; artifact: number; artifacts: number; records: number }>;
|
|
60
|
+
|
|
61
|
+
export type WhatsAppMessageLikeMeExportRequest = Readonly<{
|
|
62
|
+
outputRoot: string;
|
|
63
|
+
source: WhatsAppMessageLikeMeExportSource;
|
|
64
|
+
signal?: AbortSignal;
|
|
65
|
+
onProgress?: (progress: WhatsAppMessageLikeMeBundleProgress) => void;
|
|
66
|
+
clock?: () => Date;
|
|
67
|
+
recoveryEnvironment?: Readonly<Record<string, string | undefined>>;
|
|
68
|
+
}>;
|
|
69
|
+
|
|
70
|
+
export type WhatsAppMessageLikeMeExportResult = Readonly<{
|
|
71
|
+
outputRoot: string;
|
|
72
|
+
manifestPath: string;
|
|
73
|
+
manifestSha256: string;
|
|
74
|
+
manifest: WhatsAppMessageBundleV2Manifest;
|
|
75
|
+
}>;
|
|
76
|
+
|
|
77
|
+
type ArtifactWriter = {
|
|
78
|
+
readonly specification: typeof WHATSAPP_MESSAGE_BUNDLE_V2_ARTIFACTS[number];
|
|
79
|
+
readonly handle: FileHandle;
|
|
80
|
+
readonly hash: ReturnType<typeof createHash>;
|
|
81
|
+
records: number;
|
|
82
|
+
bytes: number;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
type PrivateDirectoryIdentity = Readonly<{
|
|
86
|
+
device: number;
|
|
87
|
+
inode: number;
|
|
88
|
+
}>;
|
|
89
|
+
|
|
90
|
+
type ValidatedOutputRoot = Readonly<{
|
|
91
|
+
outputRoot: string;
|
|
92
|
+
parent: string;
|
|
93
|
+
parentIdentity: PrivateDirectoryIdentity;
|
|
94
|
+
}>;
|
|
95
|
+
|
|
96
|
+
type PrivateDirectory = Readonly<{
|
|
97
|
+
path: string;
|
|
98
|
+
identity: PrivateDirectoryIdentity;
|
|
99
|
+
}>;
|
|
100
|
+
|
|
101
|
+
type StagedManifest = Readonly<{
|
|
102
|
+
bytes: number;
|
|
103
|
+
sha256: string;
|
|
104
|
+
}>;
|
|
105
|
+
|
|
106
|
+
type PrivateFileIdentity = Readonly<{
|
|
107
|
+
device: number;
|
|
108
|
+
inode: number;
|
|
109
|
+
uid: number;
|
|
110
|
+
mode: number;
|
|
111
|
+
nlink: number;
|
|
112
|
+
bytes: number;
|
|
113
|
+
mtimeMs: number;
|
|
114
|
+
ctimeMs: number;
|
|
115
|
+
}>;
|
|
116
|
+
|
|
117
|
+
type BoundV1Artifact = {
|
|
118
|
+
readonly path: string;
|
|
119
|
+
readonly manifest: BeeperMessageLikeMeArtifact;
|
|
120
|
+
readonly identity: PrivateFileIdentity;
|
|
121
|
+
readonly handle: FileHandle;
|
|
122
|
+
closed: boolean;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
function fail(message: string): never {
|
|
126
|
+
throw new Error(`WhatsApp Message Like Me export: ${message}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isErrno(value: unknown, code: string): boolean {
|
|
130
|
+
return typeof value === "object" && value !== null && "code" in value && value.code === code;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function throwIfAborted(signal: AbortSignal | undefined): void {
|
|
134
|
+
if (signal?.aborted === true) return fail("export was cancelled");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function absent(path: string): Promise<boolean> {
|
|
138
|
+
try {
|
|
139
|
+
await lstat(path);
|
|
140
|
+
return false;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (isErrno(error, "ENOENT")) return true;
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function validateOutputRoot(value: unknown): Promise<ValidatedOutputRoot> {
|
|
148
|
+
if (
|
|
149
|
+
typeof value !== "string"
|
|
150
|
+
|| !isAbsolute(value)
|
|
151
|
+
|| resolve(value) !== value
|
|
152
|
+
|| value === sep
|
|
153
|
+
|| value.length > 4_096
|
|
154
|
+
|| basename(value).includes("\0")
|
|
155
|
+
) return fail("--output must be a normalized absolute new directory");
|
|
156
|
+
const parent = dirname(value);
|
|
157
|
+
const metadata = await lstat(parent);
|
|
158
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink() || await realpath(parent) !== parent) {
|
|
159
|
+
return fail("output parent must be one canonical real directory");
|
|
160
|
+
}
|
|
161
|
+
const uid = process.getuid?.();
|
|
162
|
+
if (uid === undefined) return fail("private exports require a POSIX user identity");
|
|
163
|
+
if (metadata.uid !== uid) return fail("output parent must be owned by the current user");
|
|
164
|
+
if ((metadata.mode & 0o022) !== 0) {
|
|
165
|
+
return fail("output parent must not be writable by the group or other users");
|
|
166
|
+
}
|
|
167
|
+
if (!(await absent(value))) return fail("output directory already exists");
|
|
168
|
+
return Object.freeze({
|
|
169
|
+
outputRoot: value,
|
|
170
|
+
parent,
|
|
171
|
+
parentIdentity: Object.freeze({ device: metadata.dev, inode: metadata.ino }),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function assertParentUnchanged(output: ValidatedOutputRoot): Promise<void> {
|
|
176
|
+
try {
|
|
177
|
+
const metadata = await lstat(output.parent);
|
|
178
|
+
const uid = process.getuid?.();
|
|
179
|
+
if (
|
|
180
|
+
!metadata.isDirectory()
|
|
181
|
+
|| metadata.isSymbolicLink()
|
|
182
|
+
|| uid === undefined
|
|
183
|
+
|| metadata.uid !== uid
|
|
184
|
+
|| (metadata.mode & 0o022) !== 0
|
|
185
|
+
|| metadata.dev !== output.parentIdentity.device
|
|
186
|
+
|| metadata.ino !== output.parentIdentity.inode
|
|
187
|
+
|| await realpath(output.parent) !== output.parent
|
|
188
|
+
) return fail("output parent changed during export");
|
|
189
|
+
} catch {
|
|
190
|
+
return fail("output parent changed during export");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function assertPrivateDirectory(
|
|
195
|
+
path: string,
|
|
196
|
+
expected?: PrivateDirectoryIdentity,
|
|
197
|
+
): Promise<PrivateDirectoryIdentity> {
|
|
198
|
+
try {
|
|
199
|
+
const metadata = await lstat(path);
|
|
200
|
+
const uid = process.getuid?.();
|
|
201
|
+
if (
|
|
202
|
+
!metadata.isDirectory()
|
|
203
|
+
|| metadata.isSymbolicLink()
|
|
204
|
+
|| uid === undefined
|
|
205
|
+
|| metadata.uid !== uid
|
|
206
|
+
|| (metadata.mode & 0o777) !== PRIVATE_DIRECTORY_MODE
|
|
207
|
+
|| (expected !== undefined
|
|
208
|
+
&& (metadata.dev !== expected.device || metadata.ino !== expected.inode))
|
|
209
|
+
|| await realpath(path) !== path
|
|
210
|
+
) return fail("private export directory changed");
|
|
211
|
+
return Object.freeze({ device: metadata.dev, inode: metadata.ino });
|
|
212
|
+
} catch {
|
|
213
|
+
return fail("private export directory changed");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function removeOwnedPrivateDirectory(
|
|
218
|
+
path: string,
|
|
219
|
+
identity: PrivateDirectoryIdentity,
|
|
220
|
+
): Promise<void> {
|
|
221
|
+
try {
|
|
222
|
+
const removed = removePrivateDirectoryTree(path, Object.freeze({
|
|
223
|
+
device: String(identity.device),
|
|
224
|
+
inode: String(identity.inode),
|
|
225
|
+
}));
|
|
226
|
+
if (!removed) return fail("private export directory could not be removed safely");
|
|
227
|
+
} catch {
|
|
228
|
+
return fail("private export directory could not be removed safely");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function createPrivateDirectory(
|
|
233
|
+
output: ValidatedOutputRoot,
|
|
234
|
+
prefix: string,
|
|
235
|
+
): Promise<PrivateDirectory> {
|
|
236
|
+
await assertParentUnchanged(output);
|
|
237
|
+
const candidate = await mkdtemp(resolve(output.parent, prefix));
|
|
238
|
+
let identity: PrivateDirectoryIdentity | undefined;
|
|
239
|
+
try {
|
|
240
|
+
await chmod(candidate, PRIVATE_DIRECTORY_MODE);
|
|
241
|
+
identity = await assertPrivateDirectory(candidate);
|
|
242
|
+
if (
|
|
243
|
+
dirname(candidate) !== output.parent
|
|
244
|
+
|| identity.device !== output.parentIdentity.device
|
|
245
|
+
|| candidate === output.outputRoot
|
|
246
|
+
) return fail("private export directory must be a distinct sibling on the output filesystem");
|
|
247
|
+
await assertParentUnchanged(output);
|
|
248
|
+
return Object.freeze({ path: candidate, identity });
|
|
249
|
+
} catch (error) {
|
|
250
|
+
if (identity === undefined) {
|
|
251
|
+
try {
|
|
252
|
+
await rmdir(candidate);
|
|
253
|
+
} catch {
|
|
254
|
+
// Never recursively remove a directory whose identity was not captured.
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
await removeOwnedPrivateDirectory(candidate, identity);
|
|
258
|
+
}
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async function assertPrivateFile(path: string, expectedBytes: number): Promise<void> {
|
|
264
|
+
try {
|
|
265
|
+
const metadata = await lstat(path);
|
|
266
|
+
const uid = process.getuid?.();
|
|
267
|
+
if (
|
|
268
|
+
!metadata.isFile()
|
|
269
|
+
|| metadata.isSymbolicLink()
|
|
270
|
+
|| uid === undefined
|
|
271
|
+
|| metadata.uid !== uid
|
|
272
|
+
|| metadata.nlink !== 1
|
|
273
|
+
|| (metadata.mode & 0o777) !== PRIVATE_FILE_MODE
|
|
274
|
+
|| metadata.size !== expectedBytes
|
|
275
|
+
) return fail("private staged artifact changed");
|
|
276
|
+
} catch {
|
|
277
|
+
return fail("private staged artifact changed");
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function privateFileSha256(path: string, expectedBytes: number): Promise<string> {
|
|
282
|
+
let handle: FileHandle;
|
|
283
|
+
try {
|
|
284
|
+
handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
285
|
+
} catch {
|
|
286
|
+
return fail("private staged artifact changed before validation");
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
const uid = process.getuid?.();
|
|
290
|
+
const before = await handle.stat();
|
|
291
|
+
const entryBefore = await lstat(path);
|
|
292
|
+
if (
|
|
293
|
+
!before.isFile()
|
|
294
|
+
|| uid === undefined
|
|
295
|
+
|| before.uid !== uid
|
|
296
|
+
|| before.nlink !== 1
|
|
297
|
+
|| (before.mode & 0o777) !== PRIVATE_FILE_MODE
|
|
298
|
+
|| before.size !== expectedBytes
|
|
299
|
+
|| !entryBefore.isFile()
|
|
300
|
+
|| entryBefore.isSymbolicLink()
|
|
301
|
+
|| entryBefore.dev !== before.dev
|
|
302
|
+
|| entryBefore.ino !== before.ino
|
|
303
|
+
|| entryBefore.nlink !== 1
|
|
304
|
+
) return fail("private staged artifact changed before validation");
|
|
305
|
+
|
|
306
|
+
const hash = createHash("sha256");
|
|
307
|
+
const buffer = Buffer.allocUnsafe(64 * 1024);
|
|
308
|
+
let offset = 0;
|
|
309
|
+
while (offset < expectedBytes) {
|
|
310
|
+
const length = Math.min(buffer.byteLength, expectedBytes - offset);
|
|
311
|
+
const { bytesRead } = await handle.read(buffer, 0, length, offset);
|
|
312
|
+
if (bytesRead === 0) return fail("private staged artifact ended during validation");
|
|
313
|
+
hash.update(buffer.subarray(0, bytesRead));
|
|
314
|
+
offset += bytesRead;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const after = await handle.stat();
|
|
318
|
+
const entryAfter = await lstat(path);
|
|
319
|
+
if (
|
|
320
|
+
after.dev !== before.dev
|
|
321
|
+
|| after.ino !== before.ino
|
|
322
|
+
|| !after.isFile()
|
|
323
|
+
|| after.uid !== before.uid
|
|
324
|
+
|| after.nlink !== 1
|
|
325
|
+
|| after.size !== before.size
|
|
326
|
+
|| after.mtimeMs !== before.mtimeMs
|
|
327
|
+
|| after.ctimeMs !== before.ctimeMs
|
|
328
|
+
|| (after.mode & 0o777) !== PRIVATE_FILE_MODE
|
|
329
|
+
|| !entryAfter.isFile()
|
|
330
|
+
|| entryAfter.isSymbolicLink()
|
|
331
|
+
|| entryAfter.dev !== after.dev
|
|
332
|
+
|| entryAfter.ino !== after.ino
|
|
333
|
+
|| entryAfter.nlink !== 1
|
|
334
|
+
) return fail("private staged artifact changed during validation");
|
|
335
|
+
return hash.digest("hex");
|
|
336
|
+
} catch {
|
|
337
|
+
return fail("private staged artifact changed during validation");
|
|
338
|
+
} finally {
|
|
339
|
+
await handle.close();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function samePrivateFileMetadata(
|
|
344
|
+
metadata: Stats,
|
|
345
|
+
identity: PrivateFileIdentity,
|
|
346
|
+
): boolean {
|
|
347
|
+
return metadata.isFile()
|
|
348
|
+
&& !metadata.isSymbolicLink()
|
|
349
|
+
&& metadata.dev === identity.device
|
|
350
|
+
&& metadata.ino === identity.inode
|
|
351
|
+
&& metadata.uid === identity.uid
|
|
352
|
+
&& (metadata.mode & 0o777) === identity.mode
|
|
353
|
+
&& metadata.nlink === identity.nlink
|
|
354
|
+
&& metadata.size === identity.bytes
|
|
355
|
+
&& metadata.mtimeMs === identity.mtimeMs
|
|
356
|
+
&& metadata.ctimeMs === identity.ctimeMs;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function assertBoundV1Artifact(
|
|
360
|
+
root: PrivateDirectory,
|
|
361
|
+
input: BoundV1Artifact,
|
|
362
|
+
): Promise<void> {
|
|
363
|
+
await assertPrivateDirectory(root.path, root.identity);
|
|
364
|
+
let opened: Stats;
|
|
365
|
+
let entry: Stats;
|
|
366
|
+
try {
|
|
367
|
+
opened = await input.handle.stat();
|
|
368
|
+
entry = await lstat(input.path);
|
|
369
|
+
} catch {
|
|
370
|
+
return fail(`${input.manifest.path} changed after v1 validation`);
|
|
371
|
+
}
|
|
372
|
+
if (
|
|
373
|
+
input.closed
|
|
374
|
+
|| !samePrivateFileMetadata(opened, input.identity)
|
|
375
|
+
|| !samePrivateFileMetadata(entry, input.identity)
|
|
376
|
+
) return fail(`${input.manifest.path} changed after v1 validation`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function openBoundV1Artifact(
|
|
380
|
+
root: PrivateDirectory,
|
|
381
|
+
artifact: BeeperMessageLikeMeArtifact,
|
|
382
|
+
): Promise<BoundV1Artifact> {
|
|
383
|
+
await assertPrivateDirectory(root.path, root.identity);
|
|
384
|
+
const path = resolve(root.path, artifact.path);
|
|
385
|
+
if (!path.startsWith(`${root.path}${sep}`)) {
|
|
386
|
+
return fail("validated v1 artifact path escaped its private bundle");
|
|
387
|
+
}
|
|
388
|
+
let handle: FileHandle;
|
|
389
|
+
try {
|
|
390
|
+
handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
391
|
+
} catch {
|
|
392
|
+
return fail(`${artifact.path} changed before v1 conversion`);
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
const uid = process.getuid?.();
|
|
396
|
+
const before = await handle.stat();
|
|
397
|
+
const entry = await lstat(path);
|
|
398
|
+
const after = await handle.stat();
|
|
399
|
+
if (
|
|
400
|
+
uid === undefined
|
|
401
|
+
|| !before.isFile()
|
|
402
|
+
|| before.isSymbolicLink()
|
|
403
|
+
|| before.uid !== uid
|
|
404
|
+
|| before.nlink !== 1
|
|
405
|
+
|| (before.mode & 0o777) !== PRIVATE_FILE_MODE
|
|
406
|
+
|| before.size !== artifact.bytes
|
|
407
|
+
|| before.dev !== after.dev
|
|
408
|
+
|| before.ino !== after.ino
|
|
409
|
+
|| before.uid !== after.uid
|
|
410
|
+
|| before.nlink !== after.nlink
|
|
411
|
+
|| before.size !== after.size
|
|
412
|
+
|| before.mtimeMs !== after.mtimeMs
|
|
413
|
+
|| before.ctimeMs !== after.ctimeMs
|
|
414
|
+
|| (before.mode & 0o777) !== (after.mode & 0o777)
|
|
415
|
+
|| !entry.isFile()
|
|
416
|
+
|| entry.isSymbolicLink()
|
|
417
|
+
|| entry.dev !== after.dev
|
|
418
|
+
|| entry.ino !== after.ino
|
|
419
|
+
|| entry.uid !== after.uid
|
|
420
|
+
|| entry.nlink !== after.nlink
|
|
421
|
+
|| entry.size !== after.size
|
|
422
|
+
|| entry.mtimeMs !== after.mtimeMs
|
|
423
|
+
|| entry.ctimeMs !== after.ctimeMs
|
|
424
|
+
|| (entry.mode & 0o777) !== PRIVATE_FILE_MODE
|
|
425
|
+
) return fail(`${artifact.path} changed before v1 conversion`);
|
|
426
|
+
const input: BoundV1Artifact = {
|
|
427
|
+
path,
|
|
428
|
+
manifest: artifact,
|
|
429
|
+
identity: Object.freeze({
|
|
430
|
+
device: after.dev,
|
|
431
|
+
inode: after.ino,
|
|
432
|
+
uid: after.uid,
|
|
433
|
+
mode: after.mode & 0o777,
|
|
434
|
+
nlink: after.nlink,
|
|
435
|
+
bytes: after.size,
|
|
436
|
+
mtimeMs: after.mtimeMs,
|
|
437
|
+
ctimeMs: after.ctimeMs,
|
|
438
|
+
}),
|
|
439
|
+
handle,
|
|
440
|
+
closed: false,
|
|
441
|
+
};
|
|
442
|
+
await assertBoundV1Artifact(root, input);
|
|
443
|
+
return input;
|
|
444
|
+
} catch (error) {
|
|
445
|
+
try {
|
|
446
|
+
await handle.close();
|
|
447
|
+
} catch (closeError) {
|
|
448
|
+
throw new AggregateError(
|
|
449
|
+
[error, closeError],
|
|
450
|
+
"WhatsApp Message Like Me export: v1 artifact validation and close both failed",
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
throw error;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function closeBoundV1Artifacts(inputs: readonly BoundV1Artifact[]): Promise<void> {
|
|
458
|
+
const errors: unknown[] = [];
|
|
459
|
+
for (const input of inputs) {
|
|
460
|
+
if (input.closed) continue;
|
|
461
|
+
input.closed = true;
|
|
462
|
+
try {
|
|
463
|
+
await input.handle.close();
|
|
464
|
+
} catch (error) {
|
|
465
|
+
errors.push(error);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (errors.length > 0) {
|
|
469
|
+
throw new AggregateError(
|
|
470
|
+
errors,
|
|
471
|
+
"WhatsApp Message Like Me export: validated v1 artifact handles could not be closed",
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async function openBoundV1Artifacts(
|
|
477
|
+
root: PrivateDirectory,
|
|
478
|
+
result: BeeperMessageLikeMeExportResult,
|
|
479
|
+
): Promise<BoundV1Artifact[]> {
|
|
480
|
+
if (
|
|
481
|
+
result.outputRoot !== root.path
|
|
482
|
+
|| result.manifestPath !== resolve(root.path, "manifest.json")
|
|
483
|
+
|| result.manifest.artifacts.length !== WHATSAPP_MESSAGE_BUNDLE_V2_ARTIFACTS.length
|
|
484
|
+
) return fail("validated v1 bundle result does not identify the expected private bundle");
|
|
485
|
+
await assertPrivateDirectory(root.path, root.identity);
|
|
486
|
+
const expectedNames = [
|
|
487
|
+
...result.manifest.artifacts.map((artifact) => artifact.path),
|
|
488
|
+
"manifest.json",
|
|
489
|
+
].sort();
|
|
490
|
+
const observedNames = (await readdir(root.path)).sort();
|
|
491
|
+
if (
|
|
492
|
+
observedNames.length !== expectedNames.length
|
|
493
|
+
|| observedNames.some((name, index) => name !== expectedNames[index])
|
|
494
|
+
) return fail("validated v1 bundle no longer contains its exact seven-file inventory");
|
|
495
|
+
|
|
496
|
+
const manifestText = `${canonicalJson(result.manifest)}\n`;
|
|
497
|
+
const expectedManifestSha256 = sha256(manifestText);
|
|
498
|
+
if (
|
|
499
|
+
result.manifestSha256 !== expectedManifestSha256
|
|
500
|
+
|| await privateFileSha256(result.manifestPath, Buffer.byteLength(manifestText, "utf8"))
|
|
501
|
+
!== expectedManifestSha256
|
|
502
|
+
) return fail("validated v1 manifest changed before conversion");
|
|
503
|
+
|
|
504
|
+
const inputs: BoundV1Artifact[] = [];
|
|
505
|
+
try {
|
|
506
|
+
for (const [index, specification] of WHATSAPP_MESSAGE_BUNDLE_V2_ARTIFACTS.entries()) {
|
|
507
|
+
const artifact = result.manifest.artifacts[index];
|
|
508
|
+
if (
|
|
509
|
+
artifact === undefined
|
|
510
|
+
|| artifact.path !== specification.path
|
|
511
|
+
|| artifact.mediaType !== "application/x-ndjson"
|
|
512
|
+
|| artifact.recordKind !== specification.kind
|
|
513
|
+
|| artifact.records !== result.manifest.counts[specification.kind]
|
|
514
|
+
) return fail("validated v1 manifest changed its fixed artifact binding");
|
|
515
|
+
inputs.push(await openBoundV1Artifact(root, artifact));
|
|
516
|
+
}
|
|
517
|
+
await assertPrivateDirectory(root.path, root.identity);
|
|
518
|
+
return inputs;
|
|
519
|
+
} catch (error) {
|
|
520
|
+
try {
|
|
521
|
+
await closeBoundV1Artifacts(inputs);
|
|
522
|
+
} catch (closeError) {
|
|
523
|
+
throw new AggregateError(
|
|
524
|
+
[error, closeError],
|
|
525
|
+
"WhatsApp Message Like Me export: v1 bundle binding and close both failed",
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
throw error;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
async function validateCompleteBundle(
|
|
533
|
+
root: string,
|
|
534
|
+
identity: PrivateDirectoryIdentity,
|
|
535
|
+
artifacts: readonly WhatsAppMessageBundleV2Artifact[],
|
|
536
|
+
manifest: StagedManifest,
|
|
537
|
+
): Promise<void> {
|
|
538
|
+
await assertPrivateDirectory(root, identity);
|
|
539
|
+
const expectedNames = [
|
|
540
|
+
...artifacts.map((artifact) => artifact.path),
|
|
541
|
+
"manifest.json",
|
|
542
|
+
].sort();
|
|
543
|
+
const observedNames = (await readdir(root)).sort();
|
|
544
|
+
if (
|
|
545
|
+
observedNames.length !== expectedNames.length
|
|
546
|
+
|| observedNames.some((name, index) => name !== expectedNames[index])
|
|
547
|
+
) return fail("private staging directory does not contain the exact seven-file bundle");
|
|
548
|
+
for (const artifact of artifacts) {
|
|
549
|
+
const path = resolve(root, artifact.path);
|
|
550
|
+
await assertPrivateFile(path, artifact.bytes);
|
|
551
|
+
if (await privateFileSha256(path, artifact.bytes) !== artifact.sha256) {
|
|
552
|
+
return fail(`${artifact.path} changed before publication`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const manifestPath = resolve(root, "manifest.json");
|
|
556
|
+
await assertPrivateFile(manifestPath, manifest.bytes);
|
|
557
|
+
if (await privateFileSha256(manifestPath, manifest.bytes) !== manifest.sha256) {
|
|
558
|
+
return fail("manifest.json changed before publication");
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function writeAll(handle: FileHandle, bytes: Uint8Array): Promise<void> {
|
|
563
|
+
let offset = 0;
|
|
564
|
+
while (offset < bytes.byteLength) {
|
|
565
|
+
const result = await handle.write(bytes, offset, bytes.byteLength - offset, null);
|
|
566
|
+
if (result.bytesWritten < 1) return fail("private output stopped accepting bytes");
|
|
567
|
+
offset += result.bytesWritten;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
async function createWriter(
|
|
572
|
+
staging: PrivateDirectory,
|
|
573
|
+
specification: typeof WHATSAPP_MESSAGE_BUNDLE_V2_ARTIFACTS[number],
|
|
574
|
+
): Promise<ArtifactWriter> {
|
|
575
|
+
await assertPrivateDirectory(staging.path, staging.identity);
|
|
576
|
+
const path = resolve(staging.path, specification.path);
|
|
577
|
+
if (!path.startsWith(`${staging.path}${sep}`)) return fail("artifact path escaped private staging");
|
|
578
|
+
const handle = await open(
|
|
579
|
+
path,
|
|
580
|
+
constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW,
|
|
581
|
+
PRIVATE_FILE_MODE,
|
|
582
|
+
);
|
|
583
|
+
try {
|
|
584
|
+
await handle.chmod(PRIVATE_FILE_MODE);
|
|
585
|
+
const metadata = await handle.stat();
|
|
586
|
+
const uid = process.getuid?.();
|
|
587
|
+
if (
|
|
588
|
+
!metadata.isFile()
|
|
589
|
+
|| uid === undefined
|
|
590
|
+
|| metadata.uid !== uid
|
|
591
|
+
|| metadata.nlink !== 1
|
|
592
|
+
|| (metadata.mode & 0o777) !== PRIVATE_FILE_MODE
|
|
593
|
+
) return fail(`could not create private staged ${specification.path}`);
|
|
594
|
+
return { specification, handle, hash: createHash("sha256"), records: 0, bytes: 0 };
|
|
595
|
+
} catch (error) {
|
|
596
|
+
await handle.close();
|
|
597
|
+
throw error;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
async function convertArtifact(
|
|
602
|
+
inputRoot: PrivateDirectory,
|
|
603
|
+
input: BoundV1Artifact,
|
|
604
|
+
staging: PrivateDirectory,
|
|
605
|
+
specification: typeof WHATSAPP_MESSAGE_BUNDLE_V2_ARTIFACTS[number],
|
|
606
|
+
signal: AbortSignal | undefined,
|
|
607
|
+
onRecord: (records: number) => void,
|
|
608
|
+
): Promise<WhatsAppMessageBundleV2Artifact> {
|
|
609
|
+
if (
|
|
610
|
+
input.manifest.path !== specification.path
|
|
611
|
+
|| input.manifest.mediaType !== "application/x-ndjson"
|
|
612
|
+
|| input.manifest.recordKind !== specification.kind
|
|
613
|
+
) return fail("validated v1 artifact does not match its v2 conversion slot");
|
|
614
|
+
await assertBoundV1Artifact(inputRoot, input);
|
|
615
|
+
const writer = await createWriter(staging, specification);
|
|
616
|
+
let failure: unknown;
|
|
617
|
+
try {
|
|
618
|
+
const inputHash = createHash("sha256");
|
|
619
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
620
|
+
const readBuffer = Buffer.allocUnsafe(64 * 1024);
|
|
621
|
+
let pending = Buffer.alloc(0);
|
|
622
|
+
let inputBytes = 0;
|
|
623
|
+
while (inputBytes < input.manifest.bytes) {
|
|
624
|
+
throwIfAborted(signal);
|
|
625
|
+
const length = Math.min(readBuffer.byteLength, input.manifest.bytes - inputBytes);
|
|
626
|
+
const { bytesRead } = await input.handle.read(
|
|
627
|
+
readBuffer,
|
|
628
|
+
0,
|
|
629
|
+
length,
|
|
630
|
+
inputBytes,
|
|
631
|
+
);
|
|
632
|
+
if (bytesRead === 0) {
|
|
633
|
+
return fail(`${specification.path} ended before its manifested byte count`);
|
|
634
|
+
}
|
|
635
|
+
const chunk = readBuffer.subarray(0, bytesRead);
|
|
636
|
+
inputHash.update(chunk);
|
|
637
|
+
inputBytes += bytesRead;
|
|
638
|
+
const buffered = pending.byteLength === 0
|
|
639
|
+
? chunk
|
|
640
|
+
: Buffer.concat([pending, chunk], pending.byteLength + chunk.byteLength);
|
|
641
|
+
let lineStart = 0;
|
|
642
|
+
for (;;) {
|
|
643
|
+
const lineEnd = buffered.indexOf(0x0a, lineStart);
|
|
644
|
+
if (lineEnd < 0) break;
|
|
645
|
+
const lineBytes = buffered.subarray(lineStart, lineEnd);
|
|
646
|
+
if (lineBytes.byteLength === 0) {
|
|
647
|
+
return fail(`${specification.path} contains an empty line`);
|
|
648
|
+
}
|
|
649
|
+
let raw: unknown;
|
|
650
|
+
try {
|
|
651
|
+
raw = JSON.parse(decoder.decode(lineBytes)) as unknown;
|
|
652
|
+
} catch {
|
|
653
|
+
return fail(`${specification.path} contains malformed UTF-8 JSON`);
|
|
654
|
+
}
|
|
655
|
+
const v1 = parseBeeperMessageLikeMeRecord(raw, writer.records);
|
|
656
|
+
if (v1.kind !== specification.kind) {
|
|
657
|
+
return fail(`${specification.path} contains the wrong v1 record kind`);
|
|
658
|
+
}
|
|
659
|
+
const v2 = parseWhatsAppMessageBundleV2Record({
|
|
660
|
+
...v1,
|
|
661
|
+
schemaVersion: WHATSAPP_MESSAGE_BUNDLE_V2_SCHEMA_VERSION,
|
|
662
|
+
}, writer.records);
|
|
663
|
+
if (v2.kind !== specification.kind) {
|
|
664
|
+
return fail(`${specification.path} contains the wrong v2 record kind`);
|
|
665
|
+
}
|
|
666
|
+
const bytes = Buffer.from(`${canonicalJson(v2)}\n`, "utf8");
|
|
667
|
+
if (bytes.byteLength > WHATSAPP_MESSAGE_BUNDLE_V2_LIMITS.recordBytes) {
|
|
668
|
+
return fail("one v2 record exceeded its byte bound");
|
|
669
|
+
}
|
|
670
|
+
await writeAll(writer.handle, bytes);
|
|
671
|
+
writer.hash.update(bytes);
|
|
672
|
+
writer.records += 1;
|
|
673
|
+
writer.bytes += bytes.byteLength;
|
|
674
|
+
onRecord(writer.records);
|
|
675
|
+
lineStart = lineEnd + 1;
|
|
676
|
+
}
|
|
677
|
+
pending = Buffer.from(buffered.subarray(lineStart));
|
|
678
|
+
if (pending.byteLength > WHATSAPP_MESSAGE_BUNDLE_V2_LIMITS.recordBytes) {
|
|
679
|
+
return fail(`${specification.path} contains an overlong record`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const extra = Buffer.allocUnsafe(1);
|
|
683
|
+
if ((await input.handle.read(extra, 0, 1, inputBytes)).bytesRead !== 0) {
|
|
684
|
+
return fail(`${specification.path} exceeded its manifested byte count`);
|
|
685
|
+
}
|
|
686
|
+
if (pending.byteLength !== 0) {
|
|
687
|
+
return fail(`${specification.path} does not end at an NDJSON record boundary`);
|
|
688
|
+
}
|
|
689
|
+
if (writer.records !== input.manifest.records) {
|
|
690
|
+
return fail(`${specification.path} record count changed after v1 validation`);
|
|
691
|
+
}
|
|
692
|
+
if (inputHash.digest("hex") !== input.manifest.sha256) {
|
|
693
|
+
return fail(`${specification.path} digest changed after v1 validation`);
|
|
694
|
+
}
|
|
695
|
+
await assertBoundV1Artifact(inputRoot, input);
|
|
696
|
+
await writer.handle.sync();
|
|
697
|
+
const metadata = await writer.handle.stat();
|
|
698
|
+
if (
|
|
699
|
+
!metadata.isFile()
|
|
700
|
+
|| metadata.nlink !== 1
|
|
701
|
+
|| metadata.uid !== process.getuid?.()
|
|
702
|
+
|| (metadata.mode & 0o777) !== PRIVATE_FILE_MODE
|
|
703
|
+
|| metadata.size !== writer.bytes
|
|
704
|
+
) return fail(`${specification.path} changed during conversion`);
|
|
705
|
+
} catch (error) {
|
|
706
|
+
failure = error;
|
|
707
|
+
} finally {
|
|
708
|
+
try {
|
|
709
|
+
await writer.handle.close();
|
|
710
|
+
} catch (error) {
|
|
711
|
+
failure ??= error;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
if (failure !== undefined) throw failure;
|
|
715
|
+
await assertPrivateDirectory(staging.path, staging.identity);
|
|
716
|
+
await assertPrivateFile(resolve(staging.path, specification.path), writer.bytes);
|
|
717
|
+
return Object.freeze({
|
|
718
|
+
path: specification.path,
|
|
719
|
+
mediaType: "application/x-ndjson",
|
|
720
|
+
recordKind: specification.kind,
|
|
721
|
+
records: writer.records,
|
|
722
|
+
bytes: writer.bytes,
|
|
723
|
+
sha256: writer.hash.digest("hex"),
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function nativeExclusiveRename(): NativeExclusiveRename {
|
|
728
|
+
if (cachedRename !== undefined) return cachedRename;
|
|
729
|
+
if (process.platform === "darwin") {
|
|
730
|
+
const library = dlopen("/usr/lib/libSystem.B.dylib", {
|
|
731
|
+
renamex_np: { args: ["cstring", "cstring", "u32"], returns: "int" },
|
|
732
|
+
} as const);
|
|
733
|
+
cachedRename = (source, destination) => library.symbols.renamex_np(
|
|
734
|
+
ptr(source), ptr(destination), DARWIN_RENAME_EXCL,
|
|
735
|
+
);
|
|
736
|
+
return cachedRename;
|
|
737
|
+
}
|
|
738
|
+
if (process.platform === "linux") {
|
|
739
|
+
const library = dlopen("libc.so.6", {
|
|
740
|
+
renameat2: { args: ["int", "cstring", "int", "cstring", "u32"], returns: "int" },
|
|
741
|
+
} as const);
|
|
742
|
+
cachedRename = (source, destination) => library.symbols.renameat2(
|
|
743
|
+
AT_FDCWD, ptr(source), AT_FDCWD, ptr(destination), LINUX_RENAME_NOREPLACE,
|
|
744
|
+
);
|
|
745
|
+
return cachedRename;
|
|
746
|
+
}
|
|
747
|
+
return fail("atomic no-clobber publication is unsupported on this platform");
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
async function publishExclusive(source: string, destination: string): Promise<void> {
|
|
751
|
+
const encode = (value: string): Buffer => {
|
|
752
|
+
if (value.includes("\0")) return fail("publication path is invalid");
|
|
753
|
+
return Buffer.from(`${value}\0`, "utf8");
|
|
754
|
+
};
|
|
755
|
+
if (nativeExclusiveRename()(encode(source), encode(destination)) === 0) return;
|
|
756
|
+
if (!(await absent(destination))) return fail("output directory appeared before publication");
|
|
757
|
+
return fail("atomic no-clobber publication failed");
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
async function syncDirectory(
|
|
761
|
+
path: string,
|
|
762
|
+
expected: PrivateDirectoryIdentity,
|
|
763
|
+
): Promise<void> {
|
|
764
|
+
let handle: FileHandle;
|
|
765
|
+
try {
|
|
766
|
+
handle = await open(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
767
|
+
} catch {
|
|
768
|
+
return fail("directory changed before synchronization");
|
|
769
|
+
}
|
|
770
|
+
try {
|
|
771
|
+
const metadata = await handle.stat();
|
|
772
|
+
if (
|
|
773
|
+
!metadata.isDirectory()
|
|
774
|
+
|| metadata.dev !== expected.device
|
|
775
|
+
|| metadata.ino !== expected.inode
|
|
776
|
+
) return fail("directory changed before synchronization");
|
|
777
|
+
await handle.sync();
|
|
778
|
+
} catch {
|
|
779
|
+
return fail("directory changed before synchronization");
|
|
780
|
+
} finally {
|
|
781
|
+
await handle.close();
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
async function createDirectoryLease(
|
|
786
|
+
request: Readonly<{
|
|
787
|
+
directory: PrivateDirectory;
|
|
788
|
+
parentIdentity: PrivateDirectoryIdentity;
|
|
789
|
+
outputRoot?: string;
|
|
790
|
+
recoveryEnvironment?: Readonly<Record<string, string | undefined>>;
|
|
791
|
+
}>,
|
|
792
|
+
): Promise<BeeperMessageLikeMeDirectoryLease | undefined> {
|
|
793
|
+
if (request.recoveryEnvironment === undefined) return undefined;
|
|
794
|
+
await assertPrivateDirectory(request.directory.path, request.directory.identity);
|
|
795
|
+
const createdAtMs = Date.now();
|
|
796
|
+
const lease = await createBeeperMessageLikeMeDirectoryLease({
|
|
797
|
+
role: request.outputRoot === undefined ? "raw-working" : "bundle-stage",
|
|
798
|
+
path: request.directory.path,
|
|
799
|
+
...(request.outputRoot === undefined ? {} : { outputRoot: request.outputRoot }),
|
|
800
|
+
recoverAfterMs: createdAtMs,
|
|
801
|
+
nowMs: createdAtMs,
|
|
802
|
+
environment: request.recoveryEnvironment,
|
|
803
|
+
});
|
|
804
|
+
const bound = lease.claim.directoryIdentity.device === String(request.directory.identity.device)
|
|
805
|
+
&& lease.claim.directoryIdentity.inode === String(request.directory.identity.inode)
|
|
806
|
+
&& lease.claim.parentIdentity.device === String(request.parentIdentity.device)
|
|
807
|
+
&& lease.claim.parentIdentity.inode === String(request.parentIdentity.inode);
|
|
808
|
+
if (!bound) {
|
|
809
|
+
try {
|
|
810
|
+
releaseBeeperMessageLikeMeDirectoryLease(lease);
|
|
811
|
+
} catch (error) {
|
|
812
|
+
throw new AggregateError(
|
|
813
|
+
[new Error("WhatsApp Message Like Me export: directory lease identity mismatch"), error],
|
|
814
|
+
"WhatsApp Message Like Me export: directory lease binding and release both failed",
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
return fail("directory lease identity mismatch");
|
|
818
|
+
}
|
|
819
|
+
await assertPrivateDirectory(request.directory.path, request.directory.identity);
|
|
820
|
+
return lease;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function translatedSource(source: WhatsAppMessageLikeMeExportSource) {
|
|
824
|
+
parseWhatsAppMessageBundleV2Descriptor(source.descriptor);
|
|
825
|
+
let index = 0;
|
|
826
|
+
return Object.freeze({
|
|
827
|
+
descriptor: Object.freeze({
|
|
828
|
+
source: Object.freeze({ id: "beeper-local", version: "1.1.0" }),
|
|
829
|
+
provider: Object.freeze({ id: "beeper", version: "0.15.0" }),
|
|
830
|
+
}),
|
|
831
|
+
records: (async function* () {
|
|
832
|
+
for await (const raw of source.records) {
|
|
833
|
+
const parsed = parseWhatsAppMessageBundleV2Record(raw, index);
|
|
834
|
+
index += 1;
|
|
835
|
+
yield toLocalMessageBundleV1Record(parsed);
|
|
836
|
+
}
|
|
837
|
+
})(),
|
|
838
|
+
completion: async () => parseWhatsAppMessageBundleV2Completion(await source.completion()),
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
export async function exportWhatsAppMessageLikeMeBundle(
|
|
843
|
+
request: WhatsAppMessageLikeMeExportRequest,
|
|
844
|
+
): Promise<WhatsAppMessageLikeMeExportResult> {
|
|
845
|
+
const output = await validateOutputRoot(request.outputRoot);
|
|
846
|
+
throwIfAborted(request.signal);
|
|
847
|
+
const working = await createPrivateDirectory(output, ".wrench-whatsapp-mlm-work-");
|
|
848
|
+
const v1Root = resolve(working.path, "validated-v1");
|
|
849
|
+
let staging: PrivateDirectory | undefined;
|
|
850
|
+
let workingLease: BeeperMessageLikeMeDirectoryLease | undefined;
|
|
851
|
+
let stagingLease: BeeperMessageLikeMeDirectoryLease | undefined;
|
|
852
|
+
let v1Inputs: BoundV1Artifact[] = [];
|
|
853
|
+
let renamed = false;
|
|
854
|
+
let published = false;
|
|
855
|
+
let operationError: unknown;
|
|
856
|
+
try {
|
|
857
|
+
workingLease = await createDirectoryLease({
|
|
858
|
+
directory: working,
|
|
859
|
+
parentIdentity: output.parentIdentity,
|
|
860
|
+
...(request.recoveryEnvironment === undefined
|
|
861
|
+
? {}
|
|
862
|
+
: { recoveryEnvironment: request.recoveryEnvironment }),
|
|
863
|
+
});
|
|
864
|
+
await assertParentUnchanged(output);
|
|
865
|
+
const v1 = await exportBeeperMessageLikeMeBundle({
|
|
866
|
+
outputRoot: v1Root,
|
|
867
|
+
source: translatedSource(request.source),
|
|
868
|
+
...(request.signal === undefined ? {} : { signal: request.signal }),
|
|
869
|
+
...(request.clock === undefined ? {} : { clock: request.clock }),
|
|
870
|
+
...(request.onProgress === undefined ? {} : { onProgress: request.onProgress }),
|
|
871
|
+
...(request.recoveryEnvironment === undefined
|
|
872
|
+
? {}
|
|
873
|
+
: { recoveryEnvironment: request.recoveryEnvironment }),
|
|
874
|
+
});
|
|
875
|
+
throwIfAborted(request.signal);
|
|
876
|
+
await assertPrivateDirectory(working.path, working.identity);
|
|
877
|
+
const v1Directory = Object.freeze({
|
|
878
|
+
path: v1Root,
|
|
879
|
+
identity: await assertPrivateDirectory(v1Root),
|
|
880
|
+
});
|
|
881
|
+
if (
|
|
882
|
+
dirname(v1Directory.path) !== working.path
|
|
883
|
+
|| v1Directory.identity.device !== working.identity.device
|
|
884
|
+
) return fail("validated v1 bundle escaped its owned private working directory");
|
|
885
|
+
v1Inputs = await openBoundV1Artifacts(v1Directory, v1);
|
|
886
|
+
await assertParentUnchanged(output);
|
|
887
|
+
staging = await createPrivateDirectory(output, ".wrench-whatsapp-mlm-stage-");
|
|
888
|
+
stagingLease = await createDirectoryLease({
|
|
889
|
+
directory: staging,
|
|
890
|
+
parentIdentity: output.parentIdentity,
|
|
891
|
+
outputRoot: output.outputRoot,
|
|
892
|
+
...(request.recoveryEnvironment === undefined
|
|
893
|
+
? {}
|
|
894
|
+
: { recoveryEnvironment: request.recoveryEnvironment }),
|
|
895
|
+
});
|
|
896
|
+
const artifacts: WhatsAppMessageBundleV2Artifact[] = [];
|
|
897
|
+
let totalRecords = 0;
|
|
898
|
+
for (const [index, specification] of WHATSAPP_MESSAGE_BUNDLE_V2_ARTIFACTS.entries()) {
|
|
899
|
+
const input = v1Inputs[index];
|
|
900
|
+
if (input === undefined) return fail("validated v1 artifact binding is incomplete");
|
|
901
|
+
artifacts.push(await convertArtifact(
|
|
902
|
+
v1Directory,
|
|
903
|
+
input,
|
|
904
|
+
staging,
|
|
905
|
+
specification,
|
|
906
|
+
request.signal,
|
|
907
|
+
() => {
|
|
908
|
+
totalRecords += 1;
|
|
909
|
+
if (totalRecords === 1 || totalRecords % 5_000 === 0) {
|
|
910
|
+
request.onProgress?.(Object.freeze({
|
|
911
|
+
phase: "v2-conversion",
|
|
912
|
+
artifact: index + 1,
|
|
913
|
+
artifacts: WHATSAPP_MESSAGE_BUNDLE_V2_ARTIFACTS.length,
|
|
914
|
+
records: totalRecords,
|
|
915
|
+
}));
|
|
916
|
+
}
|
|
917
|
+
},
|
|
918
|
+
));
|
|
919
|
+
}
|
|
920
|
+
await closeBoundV1Artifacts(v1Inputs);
|
|
921
|
+
const counts = Object.freeze(Object.fromEntries(
|
|
922
|
+
artifacts.map((artifact) => [artifact.recordKind, artifact.records]),
|
|
923
|
+
)) as WhatsAppMessageBundleV2Manifest["counts"];
|
|
924
|
+
const manifestProjection = Object.freeze({
|
|
925
|
+
schemaVersion: WHATSAPP_MESSAGE_BUNDLE_V2_SCHEMA_VERSION,
|
|
926
|
+
format: WHATSAPP_MESSAGE_BUNDLE_V2_FORMAT,
|
|
927
|
+
source: WHATSAPP_MESSAGE_BUNDLE_V2_SOURCE,
|
|
928
|
+
provider: WHATSAPP_MESSAGE_BUNDLE_V2_PROVIDER,
|
|
929
|
+
timestamps: v1.manifest.timestamps,
|
|
930
|
+
completeness: v1.manifest.completeness,
|
|
931
|
+
warnings: v1.manifest.warnings,
|
|
932
|
+
privacy: Object.freeze({
|
|
933
|
+
classification: "private-local" as const,
|
|
934
|
+
attachments: "metadata-only" as const,
|
|
935
|
+
providerUrls: "excluded" as const,
|
|
936
|
+
credentials: "excluded" as const,
|
|
937
|
+
}),
|
|
938
|
+
counts,
|
|
939
|
+
artifacts: Object.freeze(artifacts),
|
|
940
|
+
});
|
|
941
|
+
const manifest = parseWhatsAppMessageBundleV2Manifest(Object.freeze({
|
|
942
|
+
...manifestProjection,
|
|
943
|
+
integrity: Object.freeze({
|
|
944
|
+
algorithm: "sha256",
|
|
945
|
+
bundleSha256: whatsAppMessageBundleV2BundleSha256(manifestProjection),
|
|
946
|
+
}),
|
|
947
|
+
}));
|
|
948
|
+
const manifestBytes = Buffer.from(`${canonicalJson(manifest)}\n`, "utf8");
|
|
949
|
+
if (manifestBytes.byteLength > WHATSAPP_MESSAGE_BUNDLE_V2_LIMITS.manifestBytes) {
|
|
950
|
+
return fail("manifest exceeded its byte bound");
|
|
951
|
+
}
|
|
952
|
+
await assertPrivateDirectory(staging.path, staging.identity);
|
|
953
|
+
const manifestPath = resolve(staging.path, "manifest.json");
|
|
954
|
+
const manifestHandle = await open(
|
|
955
|
+
manifestPath,
|
|
956
|
+
constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW,
|
|
957
|
+
PRIVATE_FILE_MODE,
|
|
958
|
+
);
|
|
959
|
+
try {
|
|
960
|
+
await manifestHandle.chmod(PRIVATE_FILE_MODE);
|
|
961
|
+
await writeAll(manifestHandle, manifestBytes);
|
|
962
|
+
await manifestHandle.sync();
|
|
963
|
+
const metadata = await manifestHandle.stat();
|
|
964
|
+
const uid = process.getuid?.();
|
|
965
|
+
if (
|
|
966
|
+
!metadata.isFile()
|
|
967
|
+
|| uid === undefined
|
|
968
|
+
|| metadata.uid !== uid
|
|
969
|
+
|| metadata.nlink !== 1
|
|
970
|
+
|| (metadata.mode & 0o777) !== PRIVATE_FILE_MODE
|
|
971
|
+
|| metadata.size !== manifestBytes.byteLength
|
|
972
|
+
) return fail("manifest changed during creation");
|
|
973
|
+
} finally {
|
|
974
|
+
await manifestHandle.close();
|
|
975
|
+
}
|
|
976
|
+
const stagedManifest = Object.freeze({
|
|
977
|
+
bytes: manifestBytes.byteLength,
|
|
978
|
+
sha256: sha256(manifestBytes.toString("utf8")),
|
|
979
|
+
});
|
|
980
|
+
await assertPrivateFile(manifestPath, stagedManifest.bytes);
|
|
981
|
+
await validateCompleteBundle(staging.path, staging.identity, artifacts, stagedManifest);
|
|
982
|
+
await syncDirectory(staging.path, staging.identity);
|
|
983
|
+
throwIfAborted(request.signal);
|
|
984
|
+
await assertParentUnchanged(output);
|
|
985
|
+
await publishExclusive(staging.path, output.outputRoot);
|
|
986
|
+
renamed = true;
|
|
987
|
+
request.onProgress?.(Object.freeze({
|
|
988
|
+
phase: "bundle-publishing",
|
|
989
|
+
elapsedSeconds: 0,
|
|
990
|
+
records: totalRecords,
|
|
991
|
+
bytes: artifacts.reduce((total, artifact) => total + artifact.bytes, 0) + manifestBytes.byteLength,
|
|
992
|
+
}));
|
|
993
|
+
await validateCompleteBundle(output.outputRoot, staging.identity, artifacts, stagedManifest);
|
|
994
|
+
throwIfAborted(request.signal);
|
|
995
|
+
await assertParentUnchanged(output);
|
|
996
|
+
await syncDirectory(output.parent, output.parentIdentity);
|
|
997
|
+
published = true;
|
|
998
|
+
return Object.freeze({
|
|
999
|
+
outputRoot: output.outputRoot,
|
|
1000
|
+
manifestPath: resolve(output.outputRoot, "manifest.json"),
|
|
1001
|
+
manifestSha256: stagedManifest.sha256,
|
|
1002
|
+
manifest,
|
|
1003
|
+
});
|
|
1004
|
+
} catch (error) {
|
|
1005
|
+
operationError = error;
|
|
1006
|
+
throw error;
|
|
1007
|
+
} finally {
|
|
1008
|
+
const cleanupErrors: unknown[] = [];
|
|
1009
|
+
try {
|
|
1010
|
+
await closeBoundV1Artifacts(v1Inputs);
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
cleanupErrors.push(error);
|
|
1013
|
+
}
|
|
1014
|
+
if (!published && staging !== undefined) {
|
|
1015
|
+
let removalDurable = true;
|
|
1016
|
+
try {
|
|
1017
|
+
await removeOwnedPrivateDirectory(
|
|
1018
|
+
renamed ? output.outputRoot : staging.path,
|
|
1019
|
+
staging.identity,
|
|
1020
|
+
);
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
removalDurable = false;
|
|
1023
|
+
cleanupErrors.push(error);
|
|
1024
|
+
}
|
|
1025
|
+
try {
|
|
1026
|
+
await syncDirectory(output.parent, output.parentIdentity);
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
removalDurable = false;
|
|
1029
|
+
cleanupErrors.push(error);
|
|
1030
|
+
}
|
|
1031
|
+
if (removalDurable && stagingLease !== undefined) {
|
|
1032
|
+
try {
|
|
1033
|
+
releaseBeeperMessageLikeMeDirectoryLease(stagingLease);
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
cleanupErrors.push(error);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
} else if (published && operationError === undefined && stagingLease !== undefined) {
|
|
1039
|
+
try {
|
|
1040
|
+
releaseBeeperMessageLikeMeDirectoryLease(stagingLease);
|
|
1041
|
+
} catch (error) {
|
|
1042
|
+
cleanupErrors.push(error);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
let workingRemovalDurable = true;
|
|
1046
|
+
try {
|
|
1047
|
+
await removeOwnedPrivateDirectory(working.path, working.identity);
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
workingRemovalDurable = false;
|
|
1050
|
+
cleanupErrors.push(error);
|
|
1051
|
+
}
|
|
1052
|
+
try {
|
|
1053
|
+
await syncDirectory(output.parent, output.parentIdentity);
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
workingRemovalDurable = false;
|
|
1056
|
+
cleanupErrors.push(error);
|
|
1057
|
+
}
|
|
1058
|
+
if (workingRemovalDurable && workingLease !== undefined) {
|
|
1059
|
+
try {
|
|
1060
|
+
releaseBeeperMessageLikeMeDirectoryLease(workingLease);
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
cleanupErrors.push(error);
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
if (cleanupErrors.length > 0) {
|
|
1066
|
+
throw new AggregateError(
|
|
1067
|
+
operationError === undefined ? cleanupErrors : [operationError, ...cleanupErrors],
|
|
1068
|
+
"WhatsApp Message Like Me export cleanup could not be verified",
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
}
|