@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.
Files changed (500) hide show
  1. package/CHANGELOG.md +458 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1359 -0
  4. package/bunfig.toml +2 -0
  5. package/dist/apple-photos-client.js +704 -0
  6. package/dist/beeper-client.js +4602 -0
  7. package/dist/client.js +1605 -0
  8. package/dist/index-26yq8q16.js +31 -0
  9. package/dist/index-9t13b4k7.js +508 -0
  10. package/dist/index-axwm9b3v.js +5 -0
  11. package/dist/index-gwk7rbyj.js +30 -0
  12. package/dist/index.js +701 -0
  13. package/dist/messaging.js +1130 -0
  14. package/dist/omni-client.js +1406 -0
  15. package/dist/whatsapp-client.js +355 -0
  16. package/docs/imessage-direct-provider.md +106 -0
  17. package/docs/rental-listings.md +94 -0
  18. package/package.json +600 -0
  19. package/skills/ghostget/SKILL.md +251 -0
  20. package/skills/ghostget/agents/openai.yaml +4 -0
  21. package/skills/ghostget/references/adapter-contract.md +181 -0
  22. package/skills/ghostget/references/apple-photos.md +115 -0
  23. package/skills/ghostget/references/article-drafts.md +235 -0
  24. package/skills/ghostget/references/code-owned-provider-scaffold.md +134 -0
  25. package/skills/ghostget/references/cross-posting-video.md +139 -0
  26. package/skills/ghostget/references/cross-posting.md +82 -0
  27. package/skills/ghostget/references/derivation.md +390 -0
  28. package/skills/ghostget/references/hraness-social-profile-stats.json +269 -0
  29. package/skills/ghostget/references/install.md +66 -0
  30. package/skills/ghostget/references/linkedin-adapter.md +442 -0
  31. package/skills/ghostget/references/messaging.md +168 -0
  32. package/skills/ghostget/references/meta-comet-contract.md +153 -0
  33. package/skills/ghostget/references/platform-patterns.md +122 -0
  34. package/skills/ghostget/references/provider-plugins.md +496 -0
  35. package/skills/ghostget/references/reddit-flair.md +78 -0
  36. package/skills/ghostget/references/safety-and-state.md +214 -0
  37. package/skills/ghostget/references/settlement-and-duplicate-cleanup.md +79 -0
  38. package/skills/ghostget/references/social-platform-routing.md +66 -0
  39. package/skills/ghostget/references/social-profile-stats.md +195 -0
  40. package/skills/ghostget/references/social-video-platform-routing.md +89 -0
  41. package/skills/ghostget/references/whatsapp-adapter.md +101 -0
  42. package/skills/ghostget/references/x-adapter.md +332 -0
  43. package/skills/ghostget/references/x-ai-disclosure.md +71 -0
  44. package/src/apple-photos-cli.ts +639 -0
  45. package/src/apple-photos-client-types.ts +168 -0
  46. package/src/apple-photos-client.ts +201 -0
  47. package/src/apple-photos-contact-evidence.ts +838 -0
  48. package/src/apple-photos-local-source.ts +1131 -0
  49. package/src/args.ts +2053 -0
  50. package/src/article-draft-document.ts +417 -0
  51. package/src/article-draft-embeds.ts +123 -0
  52. package/src/article-draft-images.ts +214 -0
  53. package/src/assets/adapter-template/wrench-adapter.json +40 -0
  54. package/src/assets/adapters/beeper/wrench-web-adapter.json +1500 -0
  55. package/src/assets/adapters/beeper/wrench-web-adapter.v1.0.0.json +129 -0
  56. package/src/assets/adapters/beeper/wrench-web-adapter.v1.1.0.json +207 -0
  57. package/src/assets/adapters/beeper/wrench-web-adapter.v2.0.0.json +1463 -0
  58. package/src/assets/adapters/beeper/wrench-web-adapter.v2.1.0.json +1463 -0
  59. package/src/assets/adapters/beeper/wrench-web-adapter.v2.2.0.json +1475 -0
  60. package/src/assets/adapters/beeper/wrench-web-adapter.v2.3.0.json +1488 -0
  61. package/src/assets/adapters/bluesky/wrench-web-adapter.json +684 -0
  62. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.0.0.json +581 -0
  63. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.1.0.json +581 -0
  64. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.2.0.json +581 -0
  65. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.3.0.json +615 -0
  66. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.4.0.json +684 -0
  67. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.5.0.json +684 -0
  68. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.6.0.json +684 -0
  69. package/src/assets/adapters/clasificados/wrench-web-adapter.json +54 -0
  70. package/src/assets/adapters/facebook/wrench-web-adapter.json +750 -0
  71. package/src/assets/adapters/facebook/wrench-web-adapter.v1.0.0.json +706 -0
  72. package/src/assets/adapters/facebook/wrench-web-adapter.v1.1.0.json +703 -0
  73. package/src/assets/adapters/facebook/wrench-web-adapter.v1.2.0.json +750 -0
  74. package/src/assets/adapters/facebook-group/wrench-web-adapter.json +682 -0
  75. package/src/assets/adapters/facebook-group/wrench-web-adapter.v1.0.0.json +583 -0
  76. package/src/assets/adapters/facebook-group/wrench-web-adapter.v1.1.0.json +682 -0
  77. package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.json +437 -0
  78. package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.0.0.json +383 -0
  79. package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.1.0.json +437 -0
  80. package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.1.1.json +437 -0
  81. package/src/assets/adapters/facebook-page/wrench-web-adapter.json +880 -0
  82. package/src/assets/adapters/facebook-page/wrench-web-adapter.v1.0.0.json +740 -0
  83. package/src/assets/adapters/facebook-page/wrench-web-adapter.v1.1.0.json +880 -0
  84. package/src/assets/adapters/github/wrench-web-adapter.json +69 -0
  85. package/src/assets/adapters/github/wrench-web-adapter.v1.0.0.json +42 -0
  86. package/src/assets/adapters/github/wrench-web-adapter.v1.1.0.json +69 -0
  87. package/src/assets/adapters/gmail/wrench-adapter.json +161 -0
  88. package/src/assets/adapters/gmail/wrench-adapter.v1.2.0.json +157 -0
  89. package/src/assets/adapters/hacker-news/wrench-web-adapter.json +315 -0
  90. package/src/assets/adapters/hacker-news/wrench-web-adapter.v1.0.0.json +315 -0
  91. package/src/assets/adapters/imessage/wrench-web-adapter.json +236 -0
  92. package/src/assets/adapters/instagram/wrench-web-adapter.json +719 -0
  93. package/src/assets/adapters/instagram/wrench-web-adapter.v1.0.0.json +627 -0
  94. package/src/assets/adapters/instagram/wrench-web-adapter.v1.1.0.json +609 -0
  95. package/src/assets/adapters/instagram/wrench-web-adapter.v1.2.0.json +640 -0
  96. package/src/assets/adapters/instagram/wrench-web-adapter.v1.3.0.json +671 -0
  97. package/src/assets/adapters/instagram/wrench-web-adapter.v1.4.0.json +710 -0
  98. package/src/assets/adapters/instagram/wrench-web-adapter.v1.5.0.json +710 -0
  99. package/src/assets/adapters/instagram/wrench-web-adapter.v1.6.0.json +719 -0
  100. package/src/assets/adapters/instagram/wrench-web-adapter.v1.7.0.json +719 -0
  101. package/src/assets/adapters/linkedin/wrench-adapter.json +442 -0
  102. package/src/assets/adapters/linkedin/wrench-adapter.v0.4.0.json +124 -0
  103. package/src/assets/adapters/linkedin/wrench-adapter.v1.0.0.json +411 -0
  104. package/src/assets/adapters/linkedin/wrench-web-adapter.json +914 -0
  105. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.0.0.json +604 -0
  106. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.1.0.json +604 -0
  107. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.10.0.json +809 -0
  108. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.11.0.json +809 -0
  109. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.12.0.json +820 -0
  110. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.13.0.json +819 -0
  111. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.14.0.json +819 -0
  112. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.15.0.json +819 -0
  113. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.16.0.json +874 -0
  114. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.17.0.json +874 -0
  115. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.18.0.json +874 -0
  116. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.19.0.json +874 -0
  117. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.2.0.json +731 -0
  118. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.3.0.json +768 -0
  119. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.4.0.json +808 -0
  120. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.5.0.json +808 -0
  121. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.6.0.json +808 -0
  122. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.7.0.json +825 -0
  123. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.8.0.json +792 -0
  124. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.9.0.json +809 -0
  125. package/src/assets/adapters/reddit/wrench-web-adapter.json +855 -0
  126. package/src/assets/adapters/reddit/wrench-web-adapter.v1.0.0.json +577 -0
  127. package/src/assets/adapters/reddit/wrench-web-adapter.v1.1.0.json +604 -0
  128. package/src/assets/adapters/reddit/wrench-web-adapter.v1.10.0.json +712 -0
  129. package/src/assets/adapters/reddit/wrench-web-adapter.v1.11.0.json +712 -0
  130. package/src/assets/adapters/reddit/wrench-web-adapter.v1.12.0.json +712 -0
  131. package/src/assets/adapters/reddit/wrench-web-adapter.v1.2.0.json +653 -0
  132. package/src/assets/adapters/reddit/wrench-web-adapter.v1.3.0.json +712 -0
  133. package/src/assets/adapters/reddit/wrench-web-adapter.v1.4.0.json +713 -0
  134. package/src/assets/adapters/reddit/wrench-web-adapter.v1.5.0.json +712 -0
  135. package/src/assets/adapters/reddit/wrench-web-adapter.v1.6.0.json +713 -0
  136. package/src/assets/adapters/reddit/wrench-web-adapter.v1.7.0.json +713 -0
  137. package/src/assets/adapters/reddit/wrench-web-adapter.v1.8.0.json +713 -0
  138. package/src/assets/adapters/reddit/wrench-web-adapter.v1.9.0.json +713 -0
  139. package/src/assets/adapters/substack/wrench-web-adapter.json +936 -0
  140. package/src/assets/adapters/substack/wrench-web-adapter.v1.0.0.json +817 -0
  141. package/src/assets/adapters/substack/wrench-web-adapter.v1.1.0.json +812 -0
  142. package/src/assets/adapters/substack/wrench-web-adapter.v1.2.0.json +812 -0
  143. package/src/assets/adapters/substack/wrench-web-adapter.v1.3.0.json +902 -0
  144. package/src/assets/adapters/substack/wrench-web-adapter.v1.4.0.json +936 -0
  145. package/src/assets/adapters/substack/wrench-web-adapter.v1.5.0.json +936 -0
  146. package/src/assets/adapters/substack/wrench-web-adapter.v1.6.0.json +936 -0
  147. package/src/assets/adapters/threads/wrench-web-adapter.json +692 -0
  148. package/src/assets/adapters/threads/wrench-web-adapter.v1.0.0.json +629 -0
  149. package/src/assets/adapters/threads/wrench-web-adapter.v1.1.0.json +623 -0
  150. package/src/assets/adapters/threads/wrench-web-adapter.v1.2.0.json +622 -0
  151. package/src/assets/adapters/threads/wrench-web-adapter.v1.3.0.json +622 -0
  152. package/src/assets/adapters/threads/wrench-web-adapter.v1.4.0.json +622 -0
  153. package/src/assets/adapters/threads/wrench-web-adapter.v1.5.0.json +699 -0
  154. package/src/assets/adapters/threads/wrench-web-adapter.v1.6.0.json +693 -0
  155. package/src/assets/adapters/threads/wrench-web-adapter.v1.7.0.json +692 -0
  156. package/src/assets/adapters/tiktok/wrench-web-adapter.json +762 -0
  157. package/src/assets/adapters/tiktok/wrench-web-adapter.v1.0.0.json +681 -0
  158. package/src/assets/adapters/tiktok/wrench-web-adapter.v1.1.0.json +708 -0
  159. package/src/assets/adapters/tiktok/wrench-web-adapter.v1.2.0.json +762 -0
  160. package/src/assets/adapters/tiktok/wrench-web-adapter.v1.3.0.json +762 -0
  161. package/src/assets/adapters/twitch/wrench-web-adapter.json +42 -0
  162. package/src/assets/adapters/twitch/wrench-web-adapter.v1.0.0.json +42 -0
  163. package/src/assets/adapters/whatsapp/wrench-web-adapter.json +400 -0
  164. package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.0.0.json +355 -0
  165. package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.1.0.json +386 -0
  166. package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.2.0.json +386 -0
  167. package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.3.0.json +400 -0
  168. package/src/assets/adapters/x/wrench-adapter.json +749 -0
  169. package/src/assets/adapters/x/wrench-adapter.v1.0.0.json +700 -0
  170. package/src/assets/adapters/x/wrench-adapter.v1.1.0.json +707 -0
  171. package/src/assets/adapters/x/wrench-web-adapter.json +712 -0
  172. package/src/assets/adapters/x/wrench-web-adapter.v1.1.0.json +579 -0
  173. package/src/assets/adapters/x/wrench-web-adapter.v1.10.0.json +685 -0
  174. package/src/assets/adapters/x/wrench-web-adapter.v1.11.0.json +712 -0
  175. package/src/assets/adapters/x/wrench-web-adapter.v1.12.0.json +712 -0
  176. package/src/assets/adapters/x/wrench-web-adapter.v1.13.0.json +712 -0
  177. package/src/assets/adapters/x/wrench-web-adapter.v1.2.0.json +578 -0
  178. package/src/assets/adapters/x/wrench-web-adapter.v1.3.0.json +610 -0
  179. package/src/assets/adapters/x/wrench-web-adapter.v1.4.0.json +617 -0
  180. package/src/assets/adapters/x/wrench-web-adapter.v1.5.0.json +634 -0
  181. package/src/assets/adapters/x/wrench-web-adapter.v1.6.0.json +632 -0
  182. package/src/assets/adapters/x/wrench-web-adapter.v1.7.0.json +649 -0
  183. package/src/assets/adapters/x/wrench-web-adapter.v1.8.0.json +649 -0
  184. package/src/assets/adapters/x/wrench-web-adapter.v1.9.0.json +683 -0
  185. package/src/assets/adapters/youtube/wrench-web-adapter.json +561 -0
  186. package/src/assets/adapters/youtube/wrench-web-adapter.v1.0.0.json +465 -0
  187. package/src/assets/adapters/youtube/wrench-web-adapter.v1.1.0.json +519 -0
  188. package/src/assets/adapters/youtube/wrench-web-adapter.v1.2.0.json +561 -0
  189. package/src/assets/adapters/youtube/wrench-web-adapter.v1.3.0.json +561 -0
  190. package/src/assets/code-owned-provider-template/plugin.test.ts.template +54 -0
  191. package/src/assets/code-owned-provider-template/plugin.ts.template +102 -0
  192. package/src/assets/code-owned-provider-template/runtime.internal.test.ts.template +44 -0
  193. package/src/assets/code-owned-provider-template/runtime.ts.template +59 -0
  194. package/src/assets/code-owned-provider-template/wrench-adapter.json.template +33 -0
  195. package/src/assets/fonts/nebula-sans/LICENSE.txt +96 -0
  196. package/src/assets/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
  197. package/src/assets/fonts/nebula-sans/PROVENANCE.md +14 -0
  198. package/src/assets.d.ts +4 -0
  199. package/src/auth.ts +1157 -0
  200. package/src/beeper-client-types.ts +169 -0
  201. package/src/beeper-client.ts +250 -0
  202. package/src/beeper-contact-interactions-cli.ts +180 -0
  203. package/src/beeper-contact-interactions.ts +1612 -0
  204. package/src/beeper-message-bundle-v1.ts +257 -0
  205. package/src/beeper-message-like-me-cli.ts +84 -0
  206. package/src/beeper-message-like-me-export.ts +2274 -0
  207. package/src/beeper-message-like-me-recovery.ts +931 -0
  208. package/src/beeper-message-like-me-source.ts +4034 -0
  209. package/src/browser-admission.ts +928 -0
  210. package/src/browser-snapshots.ts +260 -0
  211. package/src/browser.ts +3873 -0
  212. package/src/canonical-json.ts +60 -0
  213. package/src/catalog-cli.ts +638 -0
  214. package/src/cli.ts +208 -0
  215. package/src/client-types.ts +301 -0
  216. package/src/client.ts +2726 -0
  217. package/src/confirmed-write-failure.ts +44 -0
  218. package/src/confirmed-write-model.ts +72 -0
  219. package/src/confirmed-write-platform.ts +1055 -0
  220. package/src/confirmed-write-program.ts +224 -0
  221. package/src/confirmed-write-runtime.ts +16 -0
  222. package/src/cursor-token.ts +228 -0
  223. package/src/derivation-dnr-readiness.ts +367 -0
  224. package/src/derivation-file-chooser.ts +384 -0
  225. package/src/derivation-network-boundary.ts +835 -0
  226. package/src/derivation-network-guard.ts +782 -0
  227. package/src/derivation-network-proxy-helper.ts +440 -0
  228. package/src/derivation-network-proxy.ts +385 -0
  229. package/src/derive-command-helper.ts +693 -0
  230. package/src/derive-fixtures.ts +343 -0
  231. package/src/derive-review.ts +980 -0
  232. package/src/derive.ts +4754 -0
  233. package/src/fixtures/beeper-message-like-me-v1/accounts.ndjson +2 -0
  234. package/src/fixtures/beeper-message-like-me-v1/conversations.ndjson +1 -0
  235. package/src/fixtures/beeper-message-like-me-v1/manifest.json +1 -0
  236. package/src/fixtures/beeper-message-like-me-v1/messages.ndjson +2 -0
  237. package/src/fixtures/beeper-message-like-me-v1/participants.ndjson +3 -0
  238. package/src/fixtures/beeper-message-like-me-v1/reactions.ndjson +1 -0
  239. package/src/fixtures/beeper-message-like-me-v1/tombstones.ndjson +1 -0
  240. package/src/ghostget.ts +3672 -0
  241. package/src/gmail-capture.ts +1244 -0
  242. package/src/har-internal.ts +1242 -0
  243. package/src/har.ts +743 -0
  244. package/src/index.ts +218 -0
  245. package/src/invocation-read-platform.ts +55 -0
  246. package/src/invocation-read-program.ts +93 -0
  247. package/src/invocation-read-runtime.ts +11 -0
  248. package/src/linked-device-lifecycle-admission.ts +450 -0
  249. package/src/linked-device-lifecycle-journal.ts +1560 -0
  250. package/src/linked-device-lifecycle-runtime.ts +1602 -0
  251. package/src/local-cli-admission.ts +135 -0
  252. package/src/local-cli-contracts.ts +271 -0
  253. package/src/local-cli-execution.ts +98 -0
  254. package/src/local-cli-surface-contract.ts +1510 -0
  255. package/src/local-cli-tool-identity.ts +336 -0
  256. package/src/media/archive.ts +2325 -0
  257. package/src/media/args.ts +303 -0
  258. package/src/media/cli.ts +287 -0
  259. package/src/media/doctor.ts +475 -0
  260. package/src/media/ffmpeg.ts +1330 -0
  261. package/src/media/http-capture.ts +711 -0
  262. package/src/media/http-probe.ts +593 -0
  263. package/src/media/http.ts +387 -0
  264. package/src/media/index.ts +10 -0
  265. package/src/media/local-transcription.ts +613 -0
  266. package/src/media/lock.ts +321 -0
  267. package/src/media/manifest.ts +1712 -0
  268. package/src/media/metadata.ts +906 -0
  269. package/src/media/process.ts +723 -0
  270. package/src/media/revision.ts +166 -0
  271. package/src/media/runtime-closure.ts +1063 -0
  272. package/src/media/source-router.ts +187 -0
  273. package/src/media/transcriber-config.ts +1122 -0
  274. package/src/media/transcript-persistence-model.ts +15 -0
  275. package/src/media/transcript-persistence-platform.ts +89 -0
  276. package/src/media/transcript-persistence-program.ts +39 -0
  277. package/src/media/transcript-persistence-runtime.ts +24 -0
  278. package/src/media/transcript.ts +616 -0
  279. package/src/media/utf8-order.ts +30 -0
  280. package/src/media/whisper-cpp.ts +464 -0
  281. package/src/media/whisper-language.ts +58 -0
  282. package/src/media/yt-dlp.ts +439 -0
  283. package/src/message-like-me-agentic-messaging.ts +882 -0
  284. package/src/messaging-action-store.ts +793 -0
  285. package/src/messaging-runtime.ts +2136 -0
  286. package/src/messaging-store.ts +539 -0
  287. package/src/messaging-types.ts +1663 -0
  288. package/src/messaging.ts +334 -0
  289. package/src/model.ts +2671 -0
  290. package/src/oauth-google.ts +888 -0
  291. package/src/omni-client-types.ts +306 -0
  292. package/src/omni-client.ts +2225 -0
  293. package/src/omni-limits.ts +34 -0
  294. package/src/omni-model.ts +1614 -0
  295. package/src/omni-request.ts +379 -0
  296. package/src/omni-runtime.ts +1632 -0
  297. package/src/operation-deadline.ts +189 -0
  298. package/src/path-helper.ts +1656 -0
  299. package/src/pinned-https.ts +194 -0
  300. package/src/plan-assets.ts +832 -0
  301. package/src/platform-catalog.ts +1832 -0
  302. package/src/plugins/beeper-linked-device/plugin.ts +310 -0
  303. package/src/plugins/bluesky-web/plugin.ts +178 -0
  304. package/src/plugins/clasificados-web/plugin.ts +66 -0
  305. package/src/plugins/github-web/plugin.ts +80 -0
  306. package/src/plugins/gmail-official/plugin.ts +110 -0
  307. package/src/plugins/hacker-news-web/plugin.ts +52 -0
  308. package/src/plugins/imessage-direct/plugin.ts +195 -0
  309. package/src/plugins/imessage-direct/vendor/0001-fix-keep-AppleScript-send-payloads-out-of-child-argv.patch +1430 -0
  310. package/src/plugins/imessage-direct/vendor/0002-feat-rpc-add-exact-chat-lookup.patch +225 -0
  311. package/src/plugins/imessage-direct/vendor/provenance.json +80 -0
  312. package/src/plugins/linkedin-official/plugin.ts +84 -0
  313. package/src/plugins/linkedin-web/plugin.ts +412 -0
  314. package/src/plugins/meta-web/plugin.ts +317 -0
  315. package/src/plugins/reddit-web/flair.ts +492 -0
  316. package/src/plugins/reddit-web/plugin.ts +477 -0
  317. package/src/plugins/substack-web/plugin.ts +124 -0
  318. package/src/plugins/tiktok-web/plugin.ts +67 -0
  319. package/src/plugins/twitch-web/plugin.ts +54 -0
  320. package/src/plugins/whatsapp-linked-device/plugin.ts +105 -0
  321. package/src/plugins/x-official/plugin.ts +64 -0
  322. package/src/plugins/x-web/plugin.ts +488 -0
  323. package/src/plugins/youtube-web/plugin.ts +101 -0
  324. package/src/portable-run-recovery.ts +466 -0
  325. package/src/process-identity.ts +501 -0
  326. package/src/profile-clone-helper.ts +104 -0
  327. package/src/provider-catalog-views.ts +51 -0
  328. package/src/provider-context.ts +69 -0
  329. package/src/provider-contract-definitions-x.ts +45 -0
  330. package/src/provider-contract-definitions.ts +948 -0
  331. package/src/provider-contract-input-gmail.ts +176 -0
  332. package/src/provider-contract-input-linkedin.ts +77 -0
  333. package/src/provider-contract-input-x.ts +304 -0
  334. package/src/provider-contract-planning.ts +23 -0
  335. package/src/provider-contract-semantic-identity.ts +54 -0
  336. package/src/provider-contracts.ts +251 -0
  337. package/src/provider-http.ts +723 -0
  338. package/src/provider-plugin-auth.ts +36 -0
  339. package/src/provider-plugin-builtins.ts +229 -0
  340. package/src/provider-plugin-cleanup-barrier.ts +229 -0
  341. package/src/provider-plugin-cleanup-execution.ts +79 -0
  342. package/src/provider-plugin-cleanup-resource.ts +470 -0
  343. package/src/provider-plugin-contract-identity.ts +707 -0
  344. package/src/provider-plugin-host.ts +1972 -0
  345. package/src/provider-plugin-identifiers.ts +60 -0
  346. package/src/provider-plugin-invocation-lease.ts +889 -0
  347. package/src/provider-plugin-lifecycle-kernel.ts +566 -0
  348. package/src/provider-plugin-lifecycle.ts +1455 -0
  349. package/src/provider-plugin-module-analysis.ts +336 -0
  350. package/src/provider-plugin-package.ts +2530 -0
  351. package/src/provider-plugin-portable-authority.ts +106 -0
  352. package/src/provider-plugin-portable-catalog.ts +94 -0
  353. package/src/provider-plugin-portable-identity.ts +315 -0
  354. package/src/provider-plugin-portable-runtime.ts +2057 -0
  355. package/src/provider-plugin-protocol.ts +1625 -0
  356. package/src/provider-plugin-registry.ts +3342 -0
  357. package/src/provider-plugin-store.ts +2155 -0
  358. package/src/provider-plugin-test-fixture.ts +5 -0
  359. package/src/provider-plugin.ts +4907 -0
  360. package/src/provider-plugins.generated.ts +43 -0
  361. package/src/provider-plugins.ts +8 -0
  362. package/src/provider-subject.ts +41 -0
  363. package/src/provider.ts +315 -0
  364. package/src/providers/beeper-local-runtime.ts +5426 -0
  365. package/src/providers/beeper-local.ts +2739 -0
  366. package/src/providers/beeper-messaging.ts +462 -0
  367. package/src/providers/beeper-omni.ts +1159 -0
  368. package/src/providers/bluesky-web-runtime.ts +2895 -0
  369. package/src/providers/bluesky-web.ts +1614 -0
  370. package/src/providers/clasificados-web-runtime.ts +255 -0
  371. package/src/providers/clasificados-web.ts +503 -0
  372. package/src/providers/contact-projection.ts +301 -0
  373. package/src/providers/github-read-model.ts +94 -0
  374. package/src/providers/github-read-platform.ts +165 -0
  375. package/src/providers/github-read-program.ts +97 -0
  376. package/src/providers/github-web-runtime.ts +320 -0
  377. package/src/providers/github-web.ts +403 -0
  378. package/src/providers/gmail-api.ts +2834 -0
  379. package/src/providers/gmail-omni.ts +830 -0
  380. package/src/providers/gmail.ts +1130 -0
  381. package/src/providers/hacker-news-web-runtime.ts +220 -0
  382. package/src/providers/hacker-news-web.ts +809 -0
  383. package/src/providers/imessage-direct-install.ts +354 -0
  384. package/src/providers/imessage-direct-messaging.ts +532 -0
  385. package/src/providers/imessage-direct-omni.ts +400 -0
  386. package/src/providers/imessage-direct-runtime.ts +1498 -0
  387. package/src/providers/imessage-direct.ts +351 -0
  388. package/src/providers/instagram-video-foundations.ts +913 -0
  389. package/src/providers/instagram-web-profile-browser.ts +600 -0
  390. package/src/providers/iso-bmff.ts +338 -0
  391. package/src/providers/linkedin-company-platform.ts +42 -0
  392. package/src/providers/linkedin-company-program.ts +88 -0
  393. package/src/providers/linkedin-contact-failure.ts +84 -0
  394. package/src/providers/linkedin-contact-platform.ts +45 -0
  395. package/src/providers/linkedin-contact-program.ts +91 -0
  396. package/src/providers/linkedin-profile-activity-failure.ts +76 -0
  397. package/src/providers/linkedin-profile-activity-platform.ts +41 -0
  398. package/src/providers/linkedin-profile-activity-program.ts +67 -0
  399. package/src/providers/linkedin-read-failure.ts +52 -0
  400. package/src/providers/linkedin-self-platform.ts +53 -0
  401. package/src/providers/linkedin-self-program.ts +122 -0
  402. package/src/providers/linkedin-web-article-browser.ts +794 -0
  403. package/src/providers/linkedin-web-bootstrap.ts +146 -0
  404. package/src/providers/linkedin-web-contact.ts +830 -0
  405. package/src/providers/linkedin-web-feed-browser.ts +832 -0
  406. package/src/providers/linkedin-web-feed.ts +864 -0
  407. package/src/providers/linkedin-web-post-browser.ts +653 -0
  408. package/src/providers/linkedin-web-profile-browser.ts +683 -0
  409. package/src/providers/linkedin-web-runtime.ts +2388 -0
  410. package/src/providers/linkedin-web.ts +3305 -0
  411. package/src/providers/linkedin.ts +1885 -0
  412. package/src/providers/meta-bootstrap.ts +1043 -0
  413. package/src/providers/meta-facebook-group.ts +690 -0
  414. package/src/providers/meta-marketplace-relay.ts +747 -0
  415. package/src/providers/meta-omni.ts +373 -0
  416. package/src/providers/meta-relay-bundle.ts +1049 -0
  417. package/src/providers/meta-web-descriptors.ts +1740 -0
  418. package/src/providers/meta-web-runtime.ts +3388 -0
  419. package/src/providers/meta-web.ts +2931 -0
  420. package/src/providers/read-failure.ts +139 -0
  421. package/src/providers/reddit-omni.ts +462 -0
  422. package/src/providers/reddit-web-runtime.ts +1929 -0
  423. package/src/providers/reddit-web.ts +1611 -0
  424. package/src/providers/rental-listings.ts +460 -0
  425. package/src/providers/substack-omni.ts +406 -0
  426. package/src/providers/substack-video-mp4.ts +33 -0
  427. package/src/providers/substack-web-runtime.ts +2812 -0
  428. package/src/providers/substack-web.ts +1502 -0
  429. package/src/providers/tiktok-video-mp4.ts +33 -0
  430. package/src/providers/tiktok-web-runtime.ts +770 -0
  431. package/src/providers/tiktok-web.ts +2786 -0
  432. package/src/providers/twitch-web-runtime.ts +226 -0
  433. package/src/providers/twitch-web.ts +274 -0
  434. package/src/providers/whatsapp-account-identity.ts +55 -0
  435. package/src/providers/whatsapp-contact-projection-helper.ts +874 -0
  436. package/src/providers/whatsapp-contact-projection-protocol.ts +403 -0
  437. package/src/providers/whatsapp-interaction-projection-helper.ts +1452 -0
  438. package/src/providers/whatsapp-interaction-projection-protocol.ts +462 -0
  439. package/src/providers/whatsapp-message-export-projection-protocol.ts +532 -0
  440. package/src/providers/whatsapp-messaging.ts +132 -0
  441. package/src/providers/whatsapp-omni.ts +586 -0
  442. package/src/providers/whatsapp-web-runtime.ts +3108 -0
  443. package/src/providers/whatsapp-web.ts +1037 -0
  444. package/src/providers/x-image-provenance.ts +523 -0
  445. package/src/providers/x-made-with-ai.ts +106 -0
  446. package/src/providers/x-transaction-id.ts +413 -0
  447. package/src/providers/x-web-runtime.ts +3485 -0
  448. package/src/providers/x-web.ts +2332 -0
  449. package/src/providers/x.ts +1889 -0
  450. package/src/providers/youtube-web-runtime.ts +1400 -0
  451. package/src/providers/youtube-web.ts +1221 -0
  452. package/src/read-admission-runtime.ts +91 -0
  453. package/src/read-client.ts +521 -0
  454. package/src/read-effect-platform.ts +7 -0
  455. package/src/read-effect-runtime.ts +14 -0
  456. package/src/read-effect.ts +52 -0
  457. package/src/read-projection-admission.ts +866 -0
  458. package/src/read-projections.ts +3151 -0
  459. package/src/recovery.ts +1492 -0
  460. package/src/reviewed-template.ts +146 -0
  461. package/src/run-journal.ts +1272 -0
  462. package/src/runtime.ts +5116 -0
  463. package/src/scripts/generate-provider-plugin-catalog.ts +121 -0
  464. package/src/scripts/install-whatsapp-protocol.sh +232 -0
  465. package/src/scripts/recover-install-stages.ts +1491 -0
  466. package/src/scripts/resolve-state-home.ts +30 -0
  467. package/src/scripts/scaffold-web-provider.ts +973 -0
  468. package/src/scripts/sync-bundled-adapters.ts +695 -0
  469. package/src/session-secrets.ts +1183 -0
  470. package/src/state-helper.bunfig.toml +2 -0
  471. package/src/state-helper.ts +2914 -0
  472. package/src/storage.ts +3842 -0
  473. package/src/transport-policy.ts +2 -0
  474. package/src/usage.ts +182 -0
  475. package/src/version.ts +2 -0
  476. package/src/web-session-authentication-policy.ts +136 -0
  477. package/src/web-session-cleanup-admission.ts +2344 -0
  478. package/src/web-session-cleanup-join.ts +99 -0
  479. package/src/web-session-client.ts +996 -0
  480. package/src/web-session-contract-definitions.ts +820 -0
  481. package/src/web-session-contract-planning.ts +42 -0
  482. package/src/web-session-contracts.ts +228 -0
  483. package/src/web-session-cookies.ts +500 -0
  484. package/src/web-session-execution.ts +385 -0
  485. package/src/web-session-http.ts +373 -0
  486. package/src/web-session-read-errors.ts +19 -0
  487. package/src/web-session-read-runtime.ts +124 -0
  488. package/src/web-session-recovery.ts +818 -0
  489. package/src/web-session-sites.ts +58 -0
  490. package/src/web-session-template.ts +994 -0
  491. package/src/web-session.ts +93 -0
  492. package/src/whatsapp-client-binding.ts +21 -0
  493. package/src/whatsapp-client-types.ts +75 -0
  494. package/src/whatsapp-client.ts +395 -0
  495. package/src/whatsapp-export-coordinate.ts +17 -0
  496. package/src/whatsapp-message-bundle-v2.ts +195 -0
  497. package/src/whatsapp-message-like-me-cli.ts +195 -0
  498. package/src/whatsapp-message-like-me-export.ts +1072 -0
  499. package/src/whatsapp-message-like-me-source.ts +1012 -0
  500. 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
+ }