@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,1452 @@
1
+ import {
2
+ constants as fsConstants,
3
+ closeSync,
4
+ fstatSync,
5
+ lstatSync,
6
+ openSync,
7
+ realpathSync,
8
+ writeSync,
9
+ type BigIntStats,
10
+ } from "node:fs";
11
+ import { createHash } from "node:crypto";
12
+
13
+ import { Database, constants as sqliteConstants } from "bun:sqlite";
14
+ import { canonicalJson } from "../canonical-json";
15
+
16
+ import {
17
+ canonicalWhatsAppParticipantJid,
18
+ } from "./whatsapp-account-identity";
19
+ import {
20
+ WHATSAPP_CONTACT_PROJECTION_PROTOCOL_VERSION,
21
+ isExactWhatsAppContactProjectionMode,
22
+ } from "./whatsapp-contact-projection-protocol";
23
+ import {
24
+ WHATSAPP_MATCHED_OWNER_IDENTITY,
25
+ projectWhatsAppContactsFromBoundCwd,
26
+ type WhatsAppMatchedOwnerIdentity,
27
+ } from "./whatsapp-contact-projection-helper";
28
+ import {
29
+ WHATSAPP_INTERACTION_PROJECTION_MAX_STDIN_BYTES,
30
+ WHATSAPP_INTERACTION_PROJECTION_MAX_STDOUT_BYTES,
31
+ WHATSAPP_INTERACTION_PROJECTION_PROTOCOL_VERSION,
32
+ WHATSAPP_INTERACTION_PROJECTION_SCHEMA_FINGERPRINT,
33
+ createWhatsAppInteractionProjectionFailure,
34
+ parseWhatsAppInteractionProjectionRequest,
35
+ type WhatsAppInteractionProjectionErrorCode,
36
+ type WhatsAppInteractionProjectionItem,
37
+ type WhatsAppInteractionProjectionRequest,
38
+ type WhatsAppInteractionProjectionSuccess,
39
+ } from "./whatsapp-interaction-projection-protocol";
40
+ import {
41
+ WHATSAPP_MESSAGE_EXPORT_PROJECTION_MAX_STDIN_BYTES,
42
+ WHATSAPP_MESSAGE_EXPORT_PROJECTION_MAX_STDOUT_BYTES,
43
+ WHATSAPP_MESSAGE_EXPORT_PROJECTION_PROTOCOL_VERSION,
44
+ WHATSAPP_MESSAGE_EXPORT_PROJECTION_SCHEMA_FINGERPRINT,
45
+ createWhatsAppMessageExportProjectionFailure,
46
+ parseWhatsAppMessageExportProjectionRequest,
47
+ type WhatsAppMessageExportProjectionItem,
48
+ type WhatsAppMessageExportProjectionRequest,
49
+ type WhatsAppMessageExportProjectionSuccess,
50
+ } from "./whatsapp-message-export-projection-protocol";
51
+
52
+ const MESSAGE_DATABASE_NAME = "wacli.db";
53
+ const SESSION_DATABASE_NAME = "session.db";
54
+ const IMMUTABLE_MESSAGE_DATABASE_URI = `file:${MESSAGE_DATABASE_NAME}?mode=ro&immutable=1`;
55
+ const IMMUTABLE_MESSAGE_DATABASE_FLAGS = sqliteConstants.SQLITE_OPEN_READONLY
56
+ | sqliteConstants.SQLITE_OPEN_URI
57
+ | sqliteConstants.SQLITE_OPEN_NOFOLLOW
58
+ | sqliteConstants.SQLITE_OPEN_PRIVATECACHE
59
+ | sqliteConstants.SQLITE_OPEN_EXRESCODE;
60
+ const MAX_MESSAGE_DATABASE_BYTES = 2n * 1024n * 1024n * 1024n;
61
+ const SQLITE_CACHE_KIB = 4_096;
62
+ const MAX_UNIX_SECONDS = 253_402_300_799n;
63
+ const SYSTEM_SENTINEL_JID = "0@s.whatsapp.net";
64
+ const MESSAGE_EXPORT_SESSION_MAX_MESSAGES = 500_000;
65
+ type MessageExportSessionSelfChatsExcluded = "none-detected" | "present-excluded";
66
+
67
+ type BoundMessageStoreRequest = Pick<
68
+ WhatsAppInteractionProjectionRequest,
69
+ "accountSubject" | "storeIdentity" | "sessionIdentity" | "messageStoreIdentity"
70
+ >;
71
+
72
+ class HelperFailure extends Error {
73
+ readonly code: WhatsAppInteractionProjectionErrorCode;
74
+
75
+ constructor(code: WhatsAppInteractionProjectionErrorCode) {
76
+ super("WhatsApp interaction projection helper failed");
77
+ this.name = "WhatsAppInteractionProjectionHelperFailure";
78
+ this.code = code;
79
+ }
80
+ }
81
+
82
+ function fail(code: WhatsAppInteractionProjectionErrorCode): never {
83
+ throw new HelperFailure(code);
84
+ }
85
+
86
+ function errorCode(error: unknown): WhatsAppInteractionProjectionErrorCode {
87
+ return error instanceof HelperFailure ? error.code : "database-invalid";
88
+ }
89
+
90
+ function isNoEntry(error: unknown): boolean {
91
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
92
+ }
93
+
94
+ function currentUid(): bigint | null {
95
+ return typeof process.getuid === "function" ? BigInt(process.getuid()) : null;
96
+ }
97
+
98
+ function sameSnapshot(left: BigIntStats, right: BigIntStats): boolean {
99
+ return left.dev === right.dev
100
+ && left.ino === right.ino
101
+ && left.mode === right.mode
102
+ && left.uid === right.uid
103
+ && left.gid === right.gid
104
+ && left.nlink === right.nlink
105
+ && left.size === right.size
106
+ && left.mtimeNs === right.mtimeNs
107
+ && left.ctimeNs === right.ctimeNs;
108
+ }
109
+
110
+ function matchesIdentity(
111
+ stats: BigIntStats,
112
+ identity: BoundMessageStoreRequest["messageStoreIdentity"],
113
+ ): boolean {
114
+ return stats.dev.toString() === identity.dev && stats.ino.toString() === identity.ino;
115
+ }
116
+
117
+ function assertBoundCwd(request: BoundMessageStoreRequest, initial?: BigIntStats): BigIntStats {
118
+ let cwd: string;
119
+ let pathStats: BigIntStats;
120
+ let dotStats: BigIntStats;
121
+ try {
122
+ cwd = process.cwd();
123
+ pathStats = lstatSync(cwd, { bigint: true });
124
+ dotStats = lstatSync(".", { bigint: true });
125
+ if (realpathSync(".") !== cwd) fail("store-binding-invalid");
126
+ } catch (error) {
127
+ if (error instanceof HelperFailure) throw error;
128
+ return fail("store-binding-invalid");
129
+ }
130
+ const uid = currentUid();
131
+ if (
132
+ !pathStats.isDirectory()
133
+ || !dotStats.isDirectory()
134
+ || pathStats.isSymbolicLink()
135
+ || (uid !== null && (pathStats.uid !== uid || dotStats.uid !== uid))
136
+ || !isExactWhatsAppContactProjectionMode(pathStats.mode, 0o700)
137
+ || !sameSnapshot(pathStats, dotStats)
138
+ || !matchesIdentity(pathStats, request.storeIdentity)
139
+ || (initial !== undefined && !sameSnapshot(initial, pathStats))
140
+ ) fail("store-binding-invalid");
141
+ return pathStats;
142
+ }
143
+
144
+ function assertNoMessageSidecars(): void {
145
+ for (const suffix of ["-journal", "-wal", "-shm"] as const) {
146
+ try {
147
+ lstatSync(`${MESSAGE_DATABASE_NAME}${suffix}`);
148
+ } catch (error) {
149
+ if (isNoEntry(error)) continue;
150
+ return fail("message-store-sidecar-state-unverified");
151
+ }
152
+ fail("message-store-sidecar-present");
153
+ }
154
+ }
155
+
156
+ function assertMessageFile(
157
+ stats: BigIntStats,
158
+ request: BoundMessageStoreRequest,
159
+ ): void {
160
+ const uid = currentUid();
161
+ if (
162
+ !stats.isFile()
163
+ || stats.isSymbolicLink()
164
+ || stats.nlink !== 1n
165
+ || (uid !== null && stats.uid !== uid)
166
+ || !isExactWhatsAppContactProjectionMode(stats.mode, 0o600)
167
+ || !matchesIdentity(stats, request.messageStoreIdentity)
168
+ ) fail("message-store-file-invalid");
169
+ if (stats.size < 1n || stats.size > MAX_MESSAGE_DATABASE_BYTES) {
170
+ fail("message-store-file-too-large");
171
+ }
172
+ }
173
+
174
+ function assertSessionOwnerFile(
175
+ stats: BigIntStats,
176
+ request: BoundMessageStoreRequest,
177
+ ): void {
178
+ const uid = currentUid();
179
+ if (
180
+ !stats.isFile()
181
+ || stats.isSymbolicLink()
182
+ || stats.nlink !== 1n
183
+ || (uid !== null && stats.uid !== uid)
184
+ || !isExactWhatsAppContactProjectionMode(stats.mode, 0o600)
185
+ || stats.dev.toString() !== request.sessionIdentity.dev
186
+ || stats.ino.toString() !== request.sessionIdentity.ino
187
+ ) fail("session-binding-invalid");
188
+ }
189
+
190
+ function exactSessionOwnerPathSnapshot(
191
+ initial: BigIntStats,
192
+ request: BoundMessageStoreRequest,
193
+ ): void {
194
+ let pathStats: BigIntStats;
195
+ try {
196
+ pathStats = lstatSync(SESSION_DATABASE_NAME, { bigint: true });
197
+ } catch {
198
+ return fail("session-binding-invalid");
199
+ }
200
+ assertSessionOwnerFile(pathStats, request);
201
+ if (!sameSnapshot(initial, pathStats)) fail("session-binding-invalid");
202
+ }
203
+
204
+ function exactMessagePathSnapshot(
205
+ initial: BigIntStats,
206
+ request: BoundMessageStoreRequest,
207
+ ): void {
208
+ let pathStats: BigIntStats;
209
+ try {
210
+ pathStats = lstatSync(MESSAGE_DATABASE_NAME, { bigint: true });
211
+ } catch {
212
+ return fail("message-store-file-invalid");
213
+ }
214
+ assertMessageFile(pathStats, request);
215
+ if (!sameSnapshot(initial, pathStats)) fail("message-store-file-invalid");
216
+ }
217
+
218
+ type SqliteRow = Readonly<Record<string, unknown>>;
219
+
220
+ function rows(
221
+ value: Iterable<unknown>,
222
+ maximum: number,
223
+ failureCode: WhatsAppInteractionProjectionErrorCode = "schema-mismatch",
224
+ ): readonly SqliteRow[] {
225
+ const result: SqliteRow[] = [];
226
+ for (const item of value) {
227
+ if (result.length >= maximum) fail(failureCode);
228
+ if (
229
+ typeof item !== "object"
230
+ || item === null
231
+ || Array.isArray(item)
232
+ || Object.getPrototypeOf(item) !== Object.prototype
233
+ ) fail(failureCode);
234
+ result.push(item as SqliteRow);
235
+ }
236
+ return result;
237
+ }
238
+
239
+ function exactRowKeys(row: SqliteRow, expected: readonly string[]): void {
240
+ const actual = Object.keys(row).sort();
241
+ const wanted = [...expected].sort();
242
+ if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) {
243
+ fail("schema-mismatch");
244
+ }
245
+ }
246
+
247
+ function pragmaInteger(value: unknown, expected: bigint): boolean {
248
+ return value === expected || value === Number(expected);
249
+ }
250
+
251
+ function singlePragmaInteger(database: Database, sql: string): bigint {
252
+ const result = rows(database.query(sql).iterate(), 1, "database-invalid");
253
+ if (result.length !== 1 || Object.values(result[0]!).length !== 1) fail("database-invalid");
254
+ const value = Object.values(result[0]!)[0];
255
+ if (typeof value === "bigint") return value;
256
+ if (typeof value === "number" && Number.isSafeInteger(value)) return BigInt(value);
257
+ return fail("database-invalid");
258
+ }
259
+
260
+ function configureDatabase(database: Database): void {
261
+ try {
262
+ database.exec(`
263
+ PRAGMA query_only = ON;
264
+ PRAGMA trusted_schema = OFF;
265
+ PRAGMA foreign_keys = ON;
266
+ PRAGMA temp_store = MEMORY;
267
+ PRAGMA cell_size_check = ON;
268
+ PRAGMA recursive_triggers = OFF;
269
+ PRAGMA writable_schema = OFF;
270
+ PRAGMA ignore_check_constraints = OFF;
271
+ PRAGMA automatic_index = OFF;
272
+ PRAGMA mmap_size = 0;
273
+ PRAGMA cache_size = -${SQLITE_CACHE_KIB};
274
+ `);
275
+ for (const [sql, expected] of [
276
+ ["PRAGMA query_only", 1n],
277
+ ["PRAGMA trusted_schema", 0n],
278
+ ["PRAGMA foreign_keys", 1n],
279
+ ["PRAGMA temp_store", 2n],
280
+ ["PRAGMA cell_size_check", 1n],
281
+ ["PRAGMA recursive_triggers", 0n],
282
+ ["PRAGMA writable_schema", 0n],
283
+ ["PRAGMA ignore_check_constraints", 0n],
284
+ ["PRAGMA automatic_index", 0n],
285
+ ["PRAGMA cache_size", BigInt(-SQLITE_CACHE_KIB)],
286
+ ] as const) {
287
+ if (singlePragmaInteger(database, sql) !== expected) fail("database-invalid");
288
+ }
289
+ const mmap = rows(database.query("PRAGMA mmap_size").iterate(), 1, "database-invalid");
290
+ if (mmap.length === 1 && !pragmaInteger(Object.values(mmap[0]!)[0], 0n)) {
291
+ fail("database-invalid");
292
+ }
293
+ } catch (error) {
294
+ if (error instanceof HelperFailure) throw error;
295
+ fail("database-invalid");
296
+ }
297
+ }
298
+
299
+ function assertIntegrity(database: Database): void {
300
+ try {
301
+ const integrity = rows(
302
+ database.query("PRAGMA integrity_check(1)").iterate(),
303
+ 1,
304
+ "database-integrity-failed",
305
+ );
306
+ if (
307
+ integrity.length !== 1
308
+ || Object.values(integrity[0]!).length !== 1
309
+ || Object.values(integrity[0]!)[0] !== "ok"
310
+ || rows(
311
+ database.query("PRAGMA foreign_key_check").iterate(),
312
+ 1,
313
+ "database-integrity-failed",
314
+ ).length !== 0
315
+ ) fail("database-integrity-failed");
316
+ } catch (error) {
317
+ if (error instanceof HelperFailure) throw error;
318
+ fail("database-integrity-failed");
319
+ }
320
+ }
321
+
322
+ const MESSAGE_COLUMNS = Object.freeze([
323
+ ["rowid", "INTEGER", 0n, null, 1n], ["chat_jid", "TEXT", 1n, null, 0n],
324
+ ["chat_name", "TEXT", 0n, null, 0n], ["msg_id", "TEXT", 1n, null, 0n],
325
+ ["sender_jid", "TEXT", 0n, null, 0n], ["sender_name", "TEXT", 0n, null, 0n],
326
+ ["ts", "INTEGER", 1n, null, 0n], ["from_me", "INTEGER", 1n, null, 0n],
327
+ ["text", "TEXT", 0n, null, 0n], ["display_text", "TEXT", 0n, null, 0n],
328
+ ["quoted_msg_id", "TEXT", 0n, null, 0n], ["quoted_sender_jid", "TEXT", 0n, null, 0n],
329
+ ["is_forwarded", "INTEGER", 1n, "0", 0n], ["forwarding_score", "INTEGER", 1n, "0", 0n],
330
+ ["reaction_to_id", "TEXT", 0n, null, 0n], ["reaction_emoji", "TEXT", 0n, null, 0n],
331
+ ["media_type", "TEXT", 0n, null, 0n], ["media_caption", "TEXT", 0n, null, 0n],
332
+ ["filename", "TEXT", 0n, null, 0n], ["mime_type", "TEXT", 0n, null, 0n],
333
+ ["direct_path", "TEXT", 0n, null, 0n], ["media_key", "BLOB", 0n, null, 0n],
334
+ ["file_sha256", "BLOB", 0n, null, 0n], ["file_enc_sha256", "BLOB", 0n, null, 0n],
335
+ ["file_length", "INTEGER", 0n, null, 0n], ["local_path", "TEXT", 0n, null, 0n],
336
+ ["downloaded_at", "INTEGER", 0n, null, 0n], ["media_unavailable_at", "INTEGER", 0n, null, 0n],
337
+ ["revoked", "INTEGER", 1n, "0", 0n], ["deleted_for_me", "INTEGER", 1n, "0", 0n],
338
+ ["deleted_at", "INTEGER", 0n, null, 0n], ["deletion_reason", "TEXT", 0n, null, 0n],
339
+ ["payload_purged_at", "INTEGER", 0n, null, 0n],
340
+ ["edited", "INTEGER", 1n, "0", 0n], ["edited_ts", "INTEGER", 1n, "0", 0n],
341
+ ["buttons", "TEXT", 0n, null, 0n],
342
+ ] as const);
343
+
344
+ const CHAT_COLUMNS = Object.freeze([
345
+ ["jid", "TEXT", 0n, null, 1n], ["kind", "TEXT", 1n, null, 0n],
346
+ ["name", "TEXT", 0n, null, 0n], ["last_message_ts", "INTEGER", 0n, null, 0n],
347
+ ["archived", "INTEGER", 1n, "0", 0n], ["pinned", "INTEGER", 1n, "0", 0n],
348
+ ["muted_until", "INTEGER", 1n, "0", 0n], ["unread", "INTEGER", 1n, "0", 0n],
349
+ ["unread_count", "INTEGER", 1n, "0", 0n],
350
+ ] as const);
351
+
352
+ function assertColumns(database: Database, table: "messages" | "chats", expected: typeof MESSAGE_COLUMNS | typeof CHAT_COLUMNS): void {
353
+ const actual = rows(database.query(`PRAGMA table_xinfo('${table}')`).iterate(), expected.length);
354
+ if (actual.length !== expected.length) fail("schema-mismatch");
355
+ for (const [index, tuple] of expected.entries()) {
356
+ const row = actual[index]!;
357
+ exactRowKeys(row, ["cid", "name", "type", "notnull", "dflt_value", "pk", "hidden"]);
358
+ const [name, type, notnull, defaultValue, pk] = tuple;
359
+ if (
360
+ !pragmaInteger(row.cid, BigInt(index))
361
+ || row.name !== name
362
+ || row.type !== type
363
+ || !pragmaInteger(row.notnull, notnull)
364
+ || row.dflt_value !== defaultValue
365
+ || !pragmaInteger(row.pk, pk)
366
+ || !pragmaInteger(row.hidden, 0n)
367
+ ) fail("schema-mismatch");
368
+ }
369
+ }
370
+
371
+ function assertIndexes(database: Database): void {
372
+ const indexes = rows(database.query("PRAGMA index_list('messages')").iterate(), 3);
373
+ if (indexes.length !== 3) fail("schema-mismatch");
374
+ const expected = new Map<string, readonly [bigint, string]>([
375
+ ["idx_messages_ts", [0n, "c"]],
376
+ ["idx_messages_chat_ts", [0n, "c"]],
377
+ ["sqlite_autoindex_messages_1", [1n, "u"]],
378
+ ] as const);
379
+ for (const row of indexes) {
380
+ exactRowKeys(row, ["seq", "name", "unique", "origin", "partial"]);
381
+ const specification = typeof row.name === "string" ? expected.get(row.name) : undefined;
382
+ if (
383
+ specification === undefined
384
+ || !pragmaInteger(row.unique, specification[0])
385
+ || row.origin !== specification[1]
386
+ || !pragmaInteger(row.partial, 0n)
387
+ ) fail("schema-mismatch");
388
+ }
389
+ for (const [name, columns] of [
390
+ ["idx_messages_ts", ["ts"]],
391
+ ["idx_messages_chat_ts", ["chat_jid", "ts"]],
392
+ ] as const) {
393
+ const indexColumns = rows(
394
+ database.query(`PRAGMA index_xinfo('${name}')`).iterate(),
395
+ columns.length + 1,
396
+ );
397
+ if (indexColumns.length !== columns.length + 1) fail("schema-mismatch");
398
+ for (const [index, column] of columns.entries()) {
399
+ const row = indexColumns[index]!;
400
+ exactRowKeys(row, ["seqno", "cid", "name", "desc", "coll", "key"]);
401
+ if (
402
+ !pragmaInteger(row.seqno, BigInt(index))
403
+ || row.name !== column
404
+ || !pragmaInteger(row.desc, 0n)
405
+ || row.coll !== "BINARY"
406
+ || !pragmaInteger(row.key, 1n)
407
+ ) fail("schema-mismatch");
408
+ }
409
+ const rowid = indexColumns.at(-1)!;
410
+ exactRowKeys(rowid, ["seqno", "cid", "name", "desc", "coll", "key"]);
411
+ if (
412
+ rowid.name !== null
413
+ || !pragmaInteger(rowid.cid, -1n)
414
+ || !pragmaInteger(rowid.key, 0n)
415
+ ) fail("schema-mismatch");
416
+ }
417
+ const uniqueColumns = rows(
418
+ database.query("PRAGMA index_xinfo('sqlite_autoindex_messages_1')").iterate(),
419
+ 3,
420
+ );
421
+ if (
422
+ uniqueColumns.length !== 3
423
+ || uniqueColumns[0]?.name !== "chat_jid"
424
+ || uniqueColumns[1]?.name !== "msg_id"
425
+ || !pragmaInteger(uniqueColumns[0]?.key, 1n)
426
+ || !pragmaInteger(uniqueColumns[1]?.key, 1n)
427
+ || !pragmaInteger(uniqueColumns[2]?.key, 0n)
428
+ ) fail("schema-mismatch");
429
+ const chatIndexes = rows(database.query("PRAGMA index_list('chats')").iterate(), 1);
430
+ if (
431
+ chatIndexes.length !== 1
432
+ || chatIndexes[0]?.name !== "sqlite_autoindex_chats_1"
433
+ || chatIndexes[0]?.origin !== "pk"
434
+ || !pragmaInteger(chatIndexes[0]?.unique, 1n)
435
+ || !pragmaInteger(chatIndexes[0]?.partial, 0n)
436
+ ) fail("schema-mismatch");
437
+ }
438
+
439
+ function assertForeignKey(database: Database): void {
440
+ const foreignKeys = rows(database.query("PRAGMA foreign_key_list('messages')").iterate(), 1);
441
+ const row = foreignKeys[0];
442
+ if (foreignKeys.length !== 1 || row === undefined) fail("schema-mismatch");
443
+ exactRowKeys(row, ["id", "seq", "table", "from", "to", "on_update", "on_delete", "match"]);
444
+ if (
445
+ !pragmaInteger(row.id, 0n)
446
+ || !pragmaInteger(row.seq, 0n)
447
+ || row.table !== "chats"
448
+ || row.from !== "chat_jid"
449
+ || row.to !== "jid"
450
+ || row.on_update !== "NO ACTION"
451
+ || row.on_delete !== "CASCADE"
452
+ || row.match !== "NONE"
453
+ ) fail("schema-mismatch");
454
+ }
455
+
456
+ function assertPinnedSchema(database: Database): void {
457
+ try {
458
+ const tables = rows(database.query(`
459
+ SELECT schema, name, type, ncol, wr, strict
460
+ FROM pragma_table_list
461
+ WHERE schema = 'main' AND name IN ('messages', 'chats')
462
+ LIMIT 3
463
+ `).iterate(), 3);
464
+ for (const [name, count] of [["messages", 36n], ["chats", 9n]] as const) {
465
+ const matches = tables.filter((row) => row.name === name);
466
+ if (matches.length === 1) {
467
+ exactRowKeys(matches[0]!, ["schema", "name", "type", "ncol", "wr", "strict"]);
468
+ }
469
+ if (
470
+ matches.length !== 1
471
+ || matches[0]?.type !== "table"
472
+ || !pragmaInteger(matches[0]?.ncol, count)
473
+ || !pragmaInteger(matches[0]?.wr, 0n)
474
+ || !pragmaInteger(matches[0]?.strict, 0n)
475
+ ) fail("schema-mismatch");
476
+ }
477
+ assertColumns(database, "messages", MESSAGE_COLUMNS);
478
+ assertColumns(database, "chats", CHAT_COLUMNS);
479
+ assertIndexes(database);
480
+ assertForeignKey(database);
481
+ } catch (error) {
482
+ if (error instanceof HelperFailure) throw error;
483
+ fail("schema-mismatch");
484
+ }
485
+ }
486
+
487
+ function sqliteBigInt(value: unknown): bigint {
488
+ if (typeof value === "bigint") return value;
489
+ if (typeof value === "number" && Number.isSafeInteger(value)) return BigInt(value);
490
+ if (typeof value === "string" && /^(?:0|[1-9][0-9]{0,18})$/u.test(value)) {
491
+ return BigInt(value);
492
+ }
493
+ return fail("projection-invalid");
494
+ }
495
+
496
+ function projectedJid(value: unknown, nullable = false): string | null {
497
+ if (nullable && (value === null || value === "")) return null;
498
+ if (
499
+ typeof value !== "string"
500
+ || value.length < 9
501
+ || value.length > 96
502
+ || !(
503
+ value === SYSTEM_SENTINEL_JID
504
+ || /^[0-9]{5,20}(?::[0-9]{1,5})?@s\.whatsapp\.net$/u.test(value)
505
+ || /^[0-9]{5,32}(?::[0-9]{1,5})?@lid$/u.test(value)
506
+ || /^[0-9]{5,32}(?:-[0-9]{5,20})?@g\.us$/u.test(value)
507
+ || /^[0-9]{5,32}@newsletter$/u.test(value)
508
+ || /^(?:status|[0-9]{5,32})@broadcast$/u.test(value)
509
+ )
510
+ ) fail("projection-invalid");
511
+ return value;
512
+ }
513
+
514
+ function legacyInteractionParticipantJid(value: string): string {
515
+ const match = /^([0-9]{5,20})(?::[0-9]{1,5})?@s\.whatsapp\.net$/u.exec(value)
516
+ ?? /^([0-9]{5,32})(?::[0-9]{1,5})?@lid$/u.exec(value);
517
+ if (match?.[1] === undefined) return fail("projection-invalid");
518
+ return value.endsWith("@lid")
519
+ ? `${match[1]}@lid`
520
+ : `${match[1]}@s.whatsapp.net`;
521
+ }
522
+
523
+ function assertMessageDirection(
524
+ item: Pick<WhatsAppInteractionProjectionItem, "chatJid" | "chatKind" | "senderJid" | "fromMe">,
525
+ owner: WhatsAppMatchedOwnerIdentity,
526
+ grammar: "canonical" | "legacy-interaction",
527
+ ): void {
528
+ if (item.chatKind !== "dm" && item.chatKind !== "group") return;
529
+ const directJid = grammar === "canonical"
530
+ ? /^(?:[1-9][0-9]{4,14}@s\.whatsapp\.net|[1-9][0-9]{4,19}@lid)$/u
531
+ : /^(?:[0-9]{5,20}@s\.whatsapp\.net|[0-9]{5,32}@lid)$/u;
532
+ const groupJid = grammar === "canonical"
533
+ ? /^[1-9][0-9]{4,19}(?:-[1-9][0-9]{0,19})?@g\.us$/u
534
+ : /^[0-9]{5,32}(?:-[0-9]{5,20})?@g\.us$/u;
535
+ if ((item.chatKind === "dm" && !directJid.test(item.chatJid))
536
+ || (item.chatKind === "group" && !groupJid.test(item.chatJid))) {
537
+ fail("projection-invalid");
538
+ }
539
+ const selfJids = new Set(owner.selfJids);
540
+ const senderJid = item.senderJid === null
541
+ ? null
542
+ : grammar === "canonical"
543
+ ? canonicalWhatsAppParticipantJid(item.senderJid)
544
+ : legacyInteractionParticipantJid(item.senderJid);
545
+ const valid = item.chatKind === "dm"
546
+ ? senderJid === null || (item.fromMe ? selfJids.has(senderJid) : senderJid === item.chatJid)
547
+ : senderJid === null || (item.fromMe ? selfJids.has(senderJid) : !selfJids.has(senderJid));
548
+ if (!valid) fail("projection-invalid");
549
+ }
550
+
551
+ function projectItem(
552
+ row: SqliteRow,
553
+ owner: WhatsAppMatchedOwnerIdentity,
554
+ ): WhatsAppInteractionProjectionItem {
555
+ exactRowKeys(row, ["rowid", "chat_jid", "msg_id", "sender_jid", "ts", "from_me", "chat_kind"]);
556
+ const rowid = sqliteBigInt(row.rowid);
557
+ const seconds = sqliteBigInt(row.ts);
558
+ const fromMe = sqliteBigInt(row.from_me);
559
+ if (
560
+ rowid < 1n
561
+ || seconds < 0n
562
+ || seconds > MAX_UNIX_SECONDS
563
+ || (fromMe !== 0n && fromMe !== 1n)
564
+ || typeof row.msg_id !== "string"
565
+ || !/^[A-Za-z0-9._~:-]{1,256}$/u.test(row.msg_id)
566
+ || (row.chat_kind !== "dm"
567
+ && row.chat_kind !== "group"
568
+ && row.chat_kind !== "broadcast"
569
+ && row.chat_kind !== "newsletter"
570
+ && row.chat_kind !== "unknown")
571
+ ) fail("projection-invalid");
572
+ const chatJid = projectedJid(row.chat_jid)! as string;
573
+ const projected = Object.freeze({
574
+ rowid: rowid.toString(),
575
+ chatJid,
576
+ messageId: row.msg_id,
577
+ senderJid: projectedJid(row.sender_jid, true),
578
+ timestamp: new Date(Number(seconds) * 1_000).toISOString(),
579
+ fromMe: fromMe === 1n,
580
+ chatKind: chatJid === SYSTEM_SENTINEL_JID ? "unknown" : row.chat_kind,
581
+ });
582
+ assertMessageDirection(projected, owner, "legacy-interaction");
583
+ return projected;
584
+ }
585
+
586
+ function interactionAnchor(item: WhatsAppInteractionProjectionItem): string {
587
+ return createHash("sha256")
588
+ .update(item.rowid)
589
+ .update("\0")
590
+ .update(item.chatJid)
591
+ .update("\0")
592
+ .update(item.messageId)
593
+ .digest("hex");
594
+ }
595
+
596
+ function assertCursorAnchor(
597
+ database: Database,
598
+ request: WhatsAppInteractionProjectionRequest,
599
+ owner: WhatsAppMatchedOwnerIdentity,
600
+ ): void {
601
+ if (request.cursor === "0") return;
602
+ let projected: readonly SqliteRow[];
603
+ try {
604
+ projected = rows(database.query(`
605
+ SELECT
606
+ CAST(m.rowid AS TEXT) AS rowid, m.chat_jid, m.msg_id, m.sender_jid,
607
+ CAST(m.ts AS TEXT) AS ts, CAST(m.from_me AS TEXT) AS from_me,
608
+ c.kind AS chat_kind
609
+ FROM messages m
610
+ JOIN chats c ON c.jid = m.chat_jid
611
+ WHERE m.rowid = ?1
612
+ LIMIT 2
613
+ `).iterate(BigInt(request.cursor)), 2, "projection-invalid");
614
+ } catch (error) {
615
+ if (error instanceof HelperFailure) throw error;
616
+ return fail("projection-invalid");
617
+ }
618
+ if (
619
+ projected.length !== 1
620
+ || interactionAnchor(projectItem(projected[0]!, owner)) !== request.cursorAnchor
621
+ ) fail("projection-invalid");
622
+ }
623
+
624
+ function projectInteractions(
625
+ database: Database,
626
+ request: WhatsAppInteractionProjectionRequest,
627
+ owner: WhatsAppMatchedOwnerIdentity,
628
+ ): WhatsAppInteractionProjectionSuccess {
629
+ let projectedRows: readonly SqliteRow[];
630
+ try {
631
+ projectedRows = rows(database.query(`
632
+ SELECT
633
+ CAST(m.rowid AS TEXT) AS rowid,
634
+ m.chat_jid,
635
+ m.msg_id,
636
+ m.sender_jid,
637
+ CAST(m.ts AS TEXT) AS ts,
638
+ CAST(m.from_me AS TEXT) AS from_me,
639
+ c.kind AS chat_kind
640
+ FROM messages m
641
+ JOIN chats c ON c.jid = m.chat_jid
642
+ WHERE m.rowid > ?1
643
+ ORDER BY m.rowid ASC
644
+ LIMIT ?2
645
+ `).iterate(BigInt(request.cursor), request.limit + 1), request.limit + 1, "projection-invalid");
646
+ } catch (error) {
647
+ if (error instanceof HelperFailure) throw error;
648
+ return fail("projection-invalid");
649
+ }
650
+ const projected = projectedRows.map((row) => projectItem(row, owner));
651
+ for (let index = 0; index < projected.length; index += 1) {
652
+ const previous = index === 0 ? request.cursor : projected[index - 1]?.rowid;
653
+ if (previous === undefined || BigInt(projected[index]!.rowid) <= BigInt(previous)) {
654
+ fail("projection-invalid");
655
+ }
656
+ }
657
+ const hasMore = projected.length > request.limit;
658
+ const interactions = Object.freeze(projected.slice(0, request.limit));
659
+ const last = interactions.at(-1);
660
+ const checkpoint = last === undefined
661
+ ? Object.freeze({ cursor: request.cursor, anchor: request.cursorAnchor })
662
+ : Object.freeze({ cursor: last.rowid, anchor: interactionAnchor(last) });
663
+ return Object.freeze({
664
+ schemaVersion: WHATSAPP_INTERACTION_PROJECTION_PROTOCOL_VERSION,
665
+ status: "succeeded",
666
+ projectionGeneration: Object.freeze({
667
+ messageStoreIdentity: request.messageStoreIdentity,
668
+ schemaFingerprint: WHATSAPP_INTERACTION_PROJECTION_SCHEMA_FINGERPRINT,
669
+ }),
670
+ accountJidAliases: owner.accountJidAliases,
671
+ interactions,
672
+ nextCursor: hasMore ? interactions.at(-1)?.rowid ?? null : null,
673
+ localInsertPageComplete: !hasMore,
674
+ checkpoint,
675
+ });
676
+ }
677
+
678
+ function messageExportGeneration(
679
+ stats: BigIntStats,
680
+ ): WhatsAppMessageExportProjectionSuccess["projectionGeneration"] {
681
+ return Object.freeze({
682
+ messageStoreIdentity: Object.freeze({
683
+ dev: stats.dev.toString(),
684
+ ino: stats.ino.toString(),
685
+ }),
686
+ size: stats.size.toString(),
687
+ mtimeNs: stats.mtimeNs.toString(),
688
+ ctimeNs: stats.ctimeNs.toString(),
689
+ schemaFingerprint: WHATSAPP_MESSAGE_EXPORT_PROJECTION_SCHEMA_FINGERPRINT,
690
+ });
691
+ }
692
+
693
+ function sameMessageExportGeneration(
694
+ left: WhatsAppMessageExportProjectionSuccess["projectionGeneration"],
695
+ right: WhatsAppMessageExportProjectionSuccess["projectionGeneration"],
696
+ ): boolean {
697
+ return left.messageStoreIdentity.dev === right.messageStoreIdentity.dev
698
+ && left.messageStoreIdentity.ino === right.messageStoreIdentity.ino
699
+ && left.size === right.size
700
+ && left.mtimeNs === right.mtimeNs
701
+ && left.ctimeNs === right.ctimeNs
702
+ && left.schemaFingerprint === right.schemaFingerprint;
703
+ }
704
+
705
+ function projectedText(
706
+ value: unknown,
707
+ maximumBytes: number,
708
+ ): string | null {
709
+ if (value === null || value === "") return null;
710
+ if (typeof value !== "string" || Buffer.byteLength(value, "utf8") > maximumBytes) {
711
+ return fail("projection-invalid");
712
+ }
713
+ return value;
714
+ }
715
+
716
+ function projectedBoolean(value: unknown): boolean {
717
+ const parsed = sqliteBigInt(value);
718
+ if (parsed !== 0n && parsed !== 1n) return fail("projection-invalid");
719
+ return parsed === 1n;
720
+ }
721
+
722
+ function projectedUnixTimestamp(value: unknown, nullable: boolean): string | null {
723
+ if (nullable && (value === null || value === "" || value === 0 || value === 0n || value === "0")) {
724
+ return null;
725
+ }
726
+ const seconds = sqliteBigInt(value);
727
+ if (seconds < 0n || seconds > MAX_UNIX_SECONDS) return fail("projection-invalid");
728
+ return new Date(Number(seconds) * 1_000).toISOString();
729
+ }
730
+
731
+ function projectedFileLength(value: unknown): number | null {
732
+ if (value === null || value === "") return null;
733
+ const parsed = sqliteBigInt(value);
734
+ if (parsed < 0n || parsed > BigInt(Number.MAX_SAFE_INTEGER)) {
735
+ return fail("projection-invalid");
736
+ }
737
+ return Number(parsed);
738
+ }
739
+
740
+ function projectMessageExportItem(
741
+ row: SqliteRow,
742
+ owner: WhatsAppMatchedOwnerIdentity,
743
+ ): WhatsAppMessageExportProjectionItem {
744
+ exactRowKeys(row, [
745
+ "rowid", "chat_jid", "chat_kind", "chat_name", "msg_id", "sender_jid", "sender_name",
746
+ "ts", "from_me", "text", "display_text", "quoted_msg_id", "quoted_sender_jid",
747
+ "reaction_to_id", "reaction_emoji", "media_type", "media_caption", "filename",
748
+ "mime_type", "file_length", "revoked", "deleted_for_me", "deleted_at",
749
+ "payload_purged_at", "edited", "edited_ts",
750
+ ]);
751
+ const rowid = sqliteBigInt(row.rowid);
752
+ if (
753
+ rowid < 1n
754
+ || (row.chat_kind !== "dm" && row.chat_kind !== "group")
755
+ || typeof row.msg_id !== "string"
756
+ || !/^[A-Za-z0-9._~:-]{1,256}$/u.test(row.msg_id)
757
+ ) return fail("projection-invalid");
758
+ const timestamp = projectedUnixTimestamp(row.ts, false)! as string;
759
+ const editedFlag = projectedBoolean(row.edited);
760
+ const rawEditedAt = projectedUnixTimestamp(row.edited_ts, true);
761
+ const edited = editedFlag && rawEditedAt !== null && rawEditedAt >= timestamp;
762
+ const revoked = projectedBoolean(row.revoked);
763
+ const deletedForMe = projectedBoolean(row.deleted_for_me);
764
+ const deletedAt = projectedUnixTimestamp(row.deleted_at, true);
765
+ const payloadPurgedAt = projectedUnixTimestamp(row.payload_purged_at, true);
766
+ const reactionToMessageId = projectedText(row.reaction_to_id, 256);
767
+ const reactionEmoji = projectedText(row.reaction_emoji, 8 * 1024);
768
+ if (
769
+ (reactionToMessageId !== null && !/^[A-Za-z0-9._~:-]{1,256}$/u.test(reactionToMessageId))
770
+ || (reactionEmoji !== null && reactionToMessageId === null)
771
+ || (deletedAt !== null && !revoked && !deletedForMe)
772
+ || (deletedAt !== null && deletedAt < timestamp)
773
+ || (payloadPurgedAt !== null && payloadPurgedAt < timestamp)
774
+ ) return fail("projection-invalid");
775
+ const fileNameCandidate = projectedText(row.filename, 8 * 1024);
776
+ const fileName = fileNameCandidate !== null && (
777
+ fileNameCandidate === "."
778
+ || fileNameCandidate === ".."
779
+ || fileNameCandidate.includes("/")
780
+ || fileNameCandidate.includes("\\")
781
+ ) ? null : fileNameCandidate;
782
+ const senderJid = row.sender_jid === SYSTEM_SENTINEL_JID
783
+ ? null
784
+ : projectedJid(row.sender_jid, true);
785
+ const quotedSenderJid = row.quoted_sender_jid === SYSTEM_SENTINEL_JID
786
+ ? null
787
+ : projectedJid(row.quoted_sender_jid, true);
788
+ const projected = Object.freeze({
789
+ rowid: rowid.toString(),
790
+ chatJid: projectedJid(row.chat_jid)! as string,
791
+ chatKind: row.chat_kind,
792
+ chatName: projectedText(row.chat_name, 8 * 1024),
793
+ messageId: row.msg_id,
794
+ senderJid,
795
+ senderName: projectedText(row.sender_name, 8 * 1024),
796
+ timestamp,
797
+ fromMe: projectedBoolean(row.from_me),
798
+ text: projectedText(row.text, 1024 * 1024),
799
+ displayText: projectedText(row.display_text, 1024 * 1024),
800
+ quotedMessageId: projectedText(row.quoted_msg_id, 256),
801
+ quotedSenderJid,
802
+ reactionToMessageId,
803
+ reactionEmoji,
804
+ mediaType: projectedText(row.media_type, 256),
805
+ mediaCaption: projectedText(row.media_caption, 1024 * 1024),
806
+ fileName,
807
+ mimeType: projectedText(row.mime_type, 256),
808
+ fileLength: projectedFileLength(row.file_length),
809
+ revoked,
810
+ deletedForMe,
811
+ deletedAt,
812
+ payloadPurgedAt,
813
+ edited,
814
+ editedAt: edited ? rawEditedAt : null,
815
+ });
816
+ assertMessageDirection(projected, owner, "canonical");
817
+ return projected;
818
+ }
819
+
820
+ function messageExportAnchor(item: WhatsAppMessageExportProjectionItem): string {
821
+ return createHash("sha256").update(JSON.stringify(item)).digest("hex");
822
+ }
823
+
824
+ function numericSqlExpression(
825
+ value: string,
826
+ minimumLength: number,
827
+ maximumLength: number,
828
+ ): string {
829
+ return `(
830
+ length(${value}) BETWEEN ${String(minimumLength)} AND ${String(maximumLength)}
831
+ AND substr(${value}, 1, 1) GLOB '[1-9]'
832
+ AND ${value} NOT GLOB '*[^0-9]*'
833
+ )`;
834
+ }
835
+
836
+ const MESSAGE_EXPORT_PN_BODY = "substr(m.chat_jid, 1, length(m.chat_jid) - 15)";
837
+ const MESSAGE_EXPORT_LID_BODY = "substr(m.chat_jid, 1, length(m.chat_jid) - 4)";
838
+ const MESSAGE_EXPORT_GROUP_BODY = "substr(m.chat_jid, 1, length(m.chat_jid) - 5)";
839
+ const MESSAGE_EXPORT_GROUP_SEPARATOR = `instr(${MESSAGE_EXPORT_GROUP_BODY}, '-')`;
840
+ const MESSAGE_EXPORT_GROUP_LEFT =
841
+ `substr(${MESSAGE_EXPORT_GROUP_BODY}, 1, ${MESSAGE_EXPORT_GROUP_SEPARATOR} - 1)`;
842
+ const MESSAGE_EXPORT_GROUP_RIGHT =
843
+ `substr(${MESSAGE_EXPORT_GROUP_BODY}, ${MESSAGE_EXPORT_GROUP_SEPARATOR} + 1)`;
844
+
845
+ const MESSAGE_EXPORT_FILTER = `
846
+ (
847
+ (
848
+ c.kind = 'dm'
849
+ AND (
850
+ (
851
+ substr(m.chat_jid, -15) = '@s.whatsapp.net'
852
+ AND ${numericSqlExpression(MESSAGE_EXPORT_PN_BODY, 5, 15)}
853
+ )
854
+ OR (
855
+ substr(m.chat_jid, -4) = '@lid'
856
+ AND ${numericSqlExpression(MESSAGE_EXPORT_LID_BODY, 5, 20)}
857
+ )
858
+ )
859
+ )
860
+ OR (
861
+ c.kind = 'group'
862
+ AND substr(m.chat_jid, -5) = '@g.us'
863
+ AND (
864
+ (
865
+ ${MESSAGE_EXPORT_GROUP_SEPARATOR} = 0
866
+ AND ${numericSqlExpression(MESSAGE_EXPORT_GROUP_BODY, 5, 20)}
867
+ )
868
+ OR (
869
+ ${MESSAGE_EXPORT_GROUP_SEPARATOR} BETWEEN 6 AND 21
870
+ AND ${numericSqlExpression(MESSAGE_EXPORT_GROUP_LEFT, 5, 20)}
871
+ AND ${numericSqlExpression(MESSAGE_EXPORT_GROUP_RIGHT, 1, 20)}
872
+ )
873
+ )
874
+ )
875
+ )
876
+ `;
877
+
878
+ const MESSAGE_EXPORT_SELF_CHAT_FILTER = `
879
+ NOT (
880
+ c.kind = 'dm'
881
+ AND (m.chat_jid = ?3 OR (?4 IS NOT NULL AND m.chat_jid = ?4))
882
+ )
883
+ `;
884
+
885
+ function hasExcludedNonConversationMessages(database: Database): boolean {
886
+ let projected: readonly SqliteRow[];
887
+ try {
888
+ projected = rows(database.query(`
889
+ SELECT EXISTS(
890
+ SELECT 1
891
+ FROM messages m
892
+ JOIN chats c ON c.jid = m.chat_jid
893
+ WHERE NOT (${MESSAGE_EXPORT_FILTER})
894
+ LIMIT 1
895
+ ) AS excluded
896
+ `).iterate(), 1, "projection-invalid");
897
+ } catch (error) {
898
+ if (error instanceof HelperFailure) throw error;
899
+ return fail("projection-invalid");
900
+ }
901
+ if (projected.length !== 1) return fail("projection-invalid");
902
+ exactRowKeys(projected[0]!, ["excluded"]);
903
+ return projectedBoolean(projected[0]!.excluded);
904
+ }
905
+
906
+ function selfChatsExcluded(
907
+ database: Database,
908
+ owner: WhatsAppMatchedOwnerIdentity,
909
+ ): MessageExportSessionSelfChatsExcluded {
910
+ try {
911
+ const result = rows(database.query(`
912
+ SELECT EXISTS(
913
+ SELECT 1
914
+ FROM messages m
915
+ JOIN chats c ON c.jid = m.chat_jid
916
+ WHERE c.kind = 'dm'
917
+ AND (m.chat_jid = ?1 OR (?2 IS NOT NULL AND m.chat_jid = ?2))
918
+ LIMIT 1
919
+ ) AS excluded
920
+ `).iterate(
921
+ owner.accountJidAliases.pnJid,
922
+ owner.accountJidAliases.lidJid,
923
+ ), 1, "projection-invalid");
924
+ if (result.length !== 1) return fail("projection-invalid");
925
+ exactRowKeys(result[0]!, ["excluded"]);
926
+ return projectedBoolean(result[0]!.excluded) ? "present-excluded" : "none-detected";
927
+ } catch (error) {
928
+ if (error instanceof HelperFailure) throw error;
929
+ return fail("projection-invalid");
930
+ }
931
+ }
932
+
933
+ const MESSAGE_EXPORT_COLUMNS = `
934
+ CAST(m.rowid AS TEXT) AS rowid,
935
+ m.chat_jid,
936
+ c.kind AS chat_kind,
937
+ COALESCE(m.chat_name, c.name) AS chat_name,
938
+ m.msg_id,
939
+ m.sender_jid,
940
+ m.sender_name,
941
+ CAST(m.ts AS TEXT) AS ts,
942
+ CAST(m.from_me AS TEXT) AS from_me,
943
+ m.text,
944
+ m.display_text,
945
+ m.quoted_msg_id,
946
+ m.quoted_sender_jid,
947
+ m.reaction_to_id,
948
+ m.reaction_emoji,
949
+ m.media_type,
950
+ m.media_caption,
951
+ m.filename,
952
+ m.mime_type,
953
+ CAST(m.file_length AS TEXT) AS file_length,
954
+ CAST(m.revoked AS TEXT) AS revoked,
955
+ CAST(m.deleted_for_me AS TEXT) AS deleted_for_me,
956
+ CAST(m.deleted_at AS TEXT) AS deleted_at,
957
+ CAST(m.payload_purged_at AS TEXT) AS payload_purged_at,
958
+ CAST(m.edited AS TEXT) AS edited,
959
+ CAST(m.edited_ts AS TEXT) AS edited_ts
960
+ `;
961
+
962
+ function assertMessageExportCursorAnchor(
963
+ database: Database,
964
+ request: WhatsAppMessageExportProjectionRequest,
965
+ owner: WhatsAppMatchedOwnerIdentity,
966
+ ): void {
967
+ if (request.cursor === "0") return;
968
+ let projected: readonly SqliteRow[];
969
+ try {
970
+ projected = rows(database.query(`
971
+ SELECT ${MESSAGE_EXPORT_COLUMNS}
972
+ FROM messages m
973
+ JOIN chats c ON c.jid = m.chat_jid
974
+ WHERE m.rowid = ?1
975
+ AND ${MESSAGE_EXPORT_FILTER}
976
+ AND ${MESSAGE_EXPORT_SELF_CHAT_FILTER}
977
+ LIMIT 2
978
+ `).iterate(
979
+ BigInt(request.cursor),
980
+ request.limit + 1,
981
+ owner.accountJidAliases.pnJid,
982
+ owner.accountJidAliases.lidJid,
983
+ ), 2, "projection-invalid");
984
+ } catch (error) {
985
+ if (error instanceof HelperFailure) throw error;
986
+ return fail("projection-invalid");
987
+ }
988
+ if (
989
+ projected.length !== 1
990
+ || messageExportAnchor(projectMessageExportItem(projected[0]!, owner)) !== request.cursorAnchor
991
+ ) return fail("projection-invalid");
992
+ }
993
+
994
+ function projectMessageExport(
995
+ database: Database,
996
+ request: WhatsAppMessageExportProjectionRequest,
997
+ fileStats: BigIntStats,
998
+ owner: WhatsAppMatchedOwnerIdentity,
999
+ precomputedNonConversationChatsExcluded?: boolean,
1000
+ ): WhatsAppMessageExportProjectionSuccess {
1001
+ const projectionGeneration = messageExportGeneration(fileStats);
1002
+ if (
1003
+ request.expectedGeneration !== null
1004
+ && !sameMessageExportGeneration(projectionGeneration, request.expectedGeneration)
1005
+ ) return fail("generation-mismatch");
1006
+ const nonConversationChatsExcluded = precomputedNonConversationChatsExcluded
1007
+ ?? hasExcludedNonConversationMessages(database);
1008
+ assertMessageExportCursorAnchor(database, request, owner);
1009
+ let projectedRows: readonly SqliteRow[];
1010
+ try {
1011
+ projectedRows = rows(database.query(`
1012
+ SELECT ${MESSAGE_EXPORT_COLUMNS}
1013
+ FROM messages m
1014
+ JOIN chats c ON c.jid = m.chat_jid
1015
+ WHERE m.rowid > ?1
1016
+ AND ${MESSAGE_EXPORT_FILTER}
1017
+ AND ${MESSAGE_EXPORT_SELF_CHAT_FILTER}
1018
+ ORDER BY m.rowid ASC
1019
+ LIMIT ?2
1020
+ `).iterate(
1021
+ BigInt(request.cursor),
1022
+ request.limit + 1,
1023
+ owner.accountJidAliases.pnJid,
1024
+ owner.accountJidAliases.lidJid,
1025
+ ), request.limit + 1, "projection-invalid");
1026
+ } catch (error) {
1027
+ if (error instanceof HelperFailure) throw error;
1028
+ return fail("projection-invalid");
1029
+ }
1030
+ const projected = projectedRows.map((row) =>
1031
+ projectMessageExportItem(row, owner));
1032
+ for (let index = 0; index < projected.length; index += 1) {
1033
+ const previous = index === 0 ? request.cursor : projected[index - 1]!.rowid;
1034
+ if (BigInt(projected[index]!.rowid) <= BigInt(previous)) return fail("projection-invalid");
1035
+ }
1036
+ const hasMore = projected.length > request.limit;
1037
+ const messages = Object.freeze(projected.slice(0, request.limit));
1038
+ const last = messages.at(-1);
1039
+ const checkpoint = last === undefined
1040
+ ? Object.freeze({ cursor: request.cursor, anchor: request.cursorAnchor })
1041
+ : Object.freeze({ cursor: last.rowid, anchor: messageExportAnchor(last) });
1042
+ return Object.freeze({
1043
+ schemaVersion: WHATSAPP_MESSAGE_EXPORT_PROJECTION_PROTOCOL_VERSION,
1044
+ status: "succeeded",
1045
+ projectionGeneration,
1046
+ accountJidAliases: owner.accountJidAliases,
1047
+ nonConversationChatsExcluded,
1048
+ messages,
1049
+ nextCursor: hasMore ? last?.rowid ?? null : null,
1050
+ localInsertPageComplete: !hasMore,
1051
+ checkpoint,
1052
+ });
1053
+ }
1054
+
1055
+ function projectBoundWhatsAppMessageStore<Result>(
1056
+ request: BoundMessageStoreRequest,
1057
+ projection: (
1058
+ database: Database,
1059
+ fileStats: BigIntStats,
1060
+ owner: WhatsAppMatchedOwnerIdentity,
1061
+ revalidate: () => void,
1062
+ ) => Result,
1063
+ ): Result {
1064
+ const initialCwd = assertBoundCwd(request);
1065
+ const noFollow = fsConstants.O_NOFOLLOW;
1066
+ if (typeof noFollow !== "number" || noFollow === 0) return fail("message-store-file-invalid");
1067
+ const nonBlock = typeof fsConstants.O_NONBLOCK === "number" ? fsConstants.O_NONBLOCK : 0;
1068
+ let sessionDescriptor: number;
1069
+ try {
1070
+ sessionDescriptor = openSync(SESSION_DATABASE_NAME, fsConstants.O_RDONLY | noFollow | nonBlock);
1071
+ } catch {
1072
+ return fail("session-binding-invalid");
1073
+ }
1074
+ let initialSession: BigIntStats;
1075
+ let owner: WhatsAppMatchedOwnerIdentity;
1076
+ try {
1077
+ initialSession = fstatSync(sessionDescriptor, { bigint: true });
1078
+ assertSessionOwnerFile(initialSession, request);
1079
+ exactSessionOwnerPathSnapshot(initialSession, request);
1080
+ const contacts = projectWhatsAppContactsFromBoundCwd({
1081
+ schemaVersion: WHATSAPP_CONTACT_PROJECTION_PROTOCOL_VERSION,
1082
+ operation: "contacts.list",
1083
+ accountSubject: request.accountSubject,
1084
+ cursor: null,
1085
+ limit: 1,
1086
+ storeIdentity: request.storeIdentity,
1087
+ sessionIdentity: request.sessionIdentity,
1088
+ });
1089
+ owner = contacts[WHATSAPP_MATCHED_OWNER_IDENTITY];
1090
+ const afterOwner = fstatSync(sessionDescriptor, { bigint: true });
1091
+ if (!sameSnapshot(initialSession, afterOwner)) fail("session-binding-invalid");
1092
+ exactSessionOwnerPathSnapshot(initialSession, request);
1093
+ } catch {
1094
+ try { closeSync(sessionDescriptor); } catch { /* owner mismatch remains categorical */ }
1095
+ return fail("owner-mismatch");
1096
+ }
1097
+ try {
1098
+ assertBoundCwd(request, initialCwd);
1099
+ assertNoMessageSidecars();
1100
+ } catch (error) {
1101
+ try {
1102
+ closeSync(sessionDescriptor);
1103
+ } catch {
1104
+ return fail("session-binding-invalid");
1105
+ }
1106
+ throw error;
1107
+ }
1108
+ let descriptor: number;
1109
+ try {
1110
+ descriptor = openSync(MESSAGE_DATABASE_NAME, fsConstants.O_RDONLY | noFollow | nonBlock);
1111
+ } catch {
1112
+ try { closeSync(sessionDescriptor); } catch { /* message-store failure remains categorical */ }
1113
+ return fail("message-store-file-invalid");
1114
+ }
1115
+ let initialFile: BigIntStats | undefined;
1116
+ let database: Database | undefined;
1117
+ let result: Result | undefined;
1118
+ let failure: unknown;
1119
+ try {
1120
+ initialFile = fstatSync(descriptor, { bigint: true });
1121
+ assertMessageFile(initialFile, request);
1122
+ exactMessagePathSnapshot(initialFile, request);
1123
+ // wacli persists WAL journal mode in the database header. Once the linked
1124
+ // device is quiescent it may leave no -wal/-shm sidecars, and an ordinary
1125
+ // read-only SQLite open then tries (and fails) to recreate shared-memory
1126
+ // state. The path is fixed, the parent directory and inode are bound above,
1127
+ // and both are revalidated after close, so immutable mode is the correct
1128
+ // side-effect-free way to read that already-proven quiescent snapshot.
1129
+ // SQLITE_OPEN_URI is explicit because Bun's Linux SQLite build does not
1130
+ // inherit macOS SQLite's process-wide URI-filename configuration.
1131
+ database = new Database(
1132
+ IMMUTABLE_MESSAGE_DATABASE_URI,
1133
+ IMMUTABLE_MESSAGE_DATABASE_FLAGS,
1134
+ );
1135
+ configureDatabase(database);
1136
+ assertIntegrity(database);
1137
+ assertPinnedSchema(database);
1138
+ const revalidate = (): void => {
1139
+ if (initialFile === undefined) fail("message-store-file-invalid");
1140
+ const after = fstatSync(descriptor, { bigint: true });
1141
+ if (!sameSnapshot(initialFile, after)) fail("message-store-file-invalid");
1142
+ exactMessagePathSnapshot(initialFile, request);
1143
+ const sessionAfter = fstatSync(sessionDescriptor, { bigint: true });
1144
+ if (!sameSnapshot(initialSession, sessionAfter)) fail("session-binding-invalid");
1145
+ exactSessionOwnerPathSnapshot(initialSession, request);
1146
+ assertBoundCwd(request, initialCwd);
1147
+ assertNoMessageSidecars();
1148
+ };
1149
+ result = projection(database, initialFile, owner, revalidate);
1150
+ } catch (error) {
1151
+ failure = error;
1152
+ } finally {
1153
+ try {
1154
+ database?.close();
1155
+ } catch {
1156
+ failure = new HelperFailure("database-invalid");
1157
+ }
1158
+ }
1159
+ try {
1160
+ if (initialFile !== undefined) {
1161
+ const after = fstatSync(descriptor, { bigint: true });
1162
+ if (!sameSnapshot(initialFile, after)) fail("message-store-file-invalid");
1163
+ exactMessagePathSnapshot(initialFile, request);
1164
+ }
1165
+ const sessionAfter = fstatSync(sessionDescriptor, { bigint: true });
1166
+ if (!sameSnapshot(initialSession, sessionAfter)) fail("session-binding-invalid");
1167
+ exactSessionOwnerPathSnapshot(initialSession, request);
1168
+ assertBoundCwd(request, initialCwd);
1169
+ assertNoMessageSidecars();
1170
+ } catch (error) {
1171
+ failure = error;
1172
+ } finally {
1173
+ try {
1174
+ closeSync(descriptor);
1175
+ } catch {
1176
+ failure = new HelperFailure("message-store-file-invalid");
1177
+ }
1178
+ try {
1179
+ closeSync(sessionDescriptor);
1180
+ } catch {
1181
+ failure = new HelperFailure("session-binding-invalid");
1182
+ }
1183
+ }
1184
+ if (failure !== undefined) {
1185
+ throw failure instanceof Error ? failure : new HelperFailure("database-invalid");
1186
+ }
1187
+ if (result === undefined) return fail("projection-invalid");
1188
+ return result;
1189
+ }
1190
+
1191
+ export function projectWhatsAppInteractionsFromBoundCwd(
1192
+ requestValue: unknown,
1193
+ ): WhatsAppInteractionProjectionSuccess {
1194
+ const request = parseWhatsAppInteractionProjectionRequest(requestValue);
1195
+ return projectBoundWhatsAppMessageStore(request, (database, _fileStats, owner) => {
1196
+ assertCursorAnchor(database, request, owner);
1197
+ return projectInteractions(database, request, owner);
1198
+ });
1199
+ }
1200
+
1201
+ export function projectWhatsAppMessageExportFromBoundCwd(
1202
+ requestValue: unknown,
1203
+ ): WhatsAppMessageExportProjectionSuccess {
1204
+ const request = parseWhatsAppMessageExportProjectionRequest(requestValue);
1205
+ return projectBoundWhatsAppMessageStore(
1206
+ request,
1207
+ (database, fileStats, owner) => projectMessageExport(database, request, fileStats, owner),
1208
+ );
1209
+ }
1210
+
1211
+ export function projectWhatsAppMessageExportSessionFromBoundCwd(
1212
+ requestValue: unknown,
1213
+ writeFrame: (frame: unknown) => void,
1214
+ options: Readonly<{
1215
+ /** Test-only seam proving the whole-store exclusion scan is session-scoped. */
1216
+ hasExcludedNonConversationMessagesForTest?: (database: Database) => boolean;
1217
+ }> = {},
1218
+ ): Readonly<{
1219
+ pages: number;
1220
+ messages: number;
1221
+ checkpoint: WhatsAppMessageExportProjectionSuccess["checkpoint"];
1222
+ projectionGeneration: WhatsAppMessageExportProjectionSuccess["projectionGeneration"];
1223
+ selfJids: readonly string[];
1224
+ selfChatsExcluded: MessageExportSessionSelfChatsExcluded;
1225
+ integrityChecks: 1;
1226
+ framesSha256: string;
1227
+ }> {
1228
+ const initial = parseWhatsAppMessageExportProjectionRequest(requestValue);
1229
+ if (
1230
+ initial.cursor !== "0"
1231
+ || initial.cursorAnchor !== null
1232
+ || initial.expectedGeneration !== null
1233
+ ) return fail("request-invalid");
1234
+ if (
1235
+ options.hasExcludedNonConversationMessagesForTest !== undefined
1236
+ && process.env.NODE_ENV !== "test"
1237
+ ) return fail("projection-invalid");
1238
+ return projectBoundWhatsAppMessageStore(
1239
+ initial,
1240
+ (database, fileStats, owner, revalidate) => {
1241
+ let request = initial;
1242
+ let pages = 0;
1243
+ let messages = 0;
1244
+ const framesHash = createHash("sha256");
1245
+ let finalResponse: WhatsAppMessageExportProjectionSuccess | undefined;
1246
+ const selfJids = Object.freeze([...owner.selfJids].sort());
1247
+ const expectedSelfJids = Object.freeze([
1248
+ owner.accountJidAliases.pnJid,
1249
+ ...(owner.accountJidAliases.lidJid === null
1250
+ ? []
1251
+ : [owner.accountJidAliases.lidJid]),
1252
+ ].sort());
1253
+ const excludedSelfChats = selfChatsExcluded(database, owner);
1254
+ const nonConversationChatsExcluded = (
1255
+ options.hasExcludedNonConversationMessagesForTest
1256
+ ?? hasExcludedNonConversationMessages
1257
+ )(database);
1258
+ if (
1259
+ selfJids.length !== expectedSelfJids.length
1260
+ || selfJids.some((jid, index) => jid !== expectedSelfJids[index])
1261
+ ) return fail("owner-mismatch");
1262
+ for (;;) {
1263
+ revalidate();
1264
+ pages += 1;
1265
+ const response = projectMessageExport(
1266
+ database,
1267
+ request,
1268
+ fileStats,
1269
+ owner,
1270
+ nonConversationChatsExcluded,
1271
+ );
1272
+ messages += response.messages.length;
1273
+ if (messages > MESSAGE_EXPORT_SESSION_MAX_MESSAGES) {
1274
+ return fail("projection-invalid");
1275
+ }
1276
+ finalResponse = response;
1277
+ const frame = Object.freeze({
1278
+ kind: "page" as const,
1279
+ index: pages,
1280
+ projectionGeneration: response.projectionGeneration,
1281
+ selfJids,
1282
+ selfChatsExcluded: excludedSelfChats,
1283
+ nonConversationChatsExcluded: response.nonConversationChatsExcluded,
1284
+ messages: response.messages,
1285
+ checkpoint: response.checkpoint,
1286
+ terminal: response.localInsertPageComplete,
1287
+ });
1288
+ const canonicalFrame = canonicalJson(frame);
1289
+ framesHash.update(canonicalFrame).update("\n");
1290
+ writeFrame(frame);
1291
+ revalidate();
1292
+ if (response.localInsertPageComplete) break;
1293
+ if (response.nextCursor === null || response.nextCursor !== response.checkpoint.cursor) {
1294
+ return fail("projection-invalid");
1295
+ }
1296
+ request = parseWhatsAppMessageExportProjectionRequest({
1297
+ ...request,
1298
+ cursor: response.checkpoint.cursor,
1299
+ cursorAnchor: response.checkpoint.anchor,
1300
+ expectedGeneration: response.projectionGeneration,
1301
+ });
1302
+ }
1303
+ if (finalResponse === undefined) return fail("projection-invalid");
1304
+ return Object.freeze({
1305
+ pages,
1306
+ messages,
1307
+ checkpoint: finalResponse.checkpoint,
1308
+ projectionGeneration: finalResponse.projectionGeneration,
1309
+ selfJids,
1310
+ selfChatsExcluded: excludedSelfChats,
1311
+ integrityChecks: 1 as const,
1312
+ framesSha256: framesHash.digest("hex"),
1313
+ });
1314
+ },
1315
+ );
1316
+ }
1317
+
1318
+ function writeSessionFrame(frame: unknown): void {
1319
+ const bytes = Buffer.from(`${canonicalJson(frame)}\n`, "utf8");
1320
+ if (bytes.byteLength > WHATSAPP_MESSAGE_EXPORT_PROJECTION_MAX_STDOUT_BYTES) {
1321
+ return fail("output-too-large");
1322
+ }
1323
+ let offset = 0;
1324
+ while (offset < bytes.length) {
1325
+ const written = writeSync(1, bytes, offset, bytes.length - offset);
1326
+ if (written < 1) return fail("projection-invalid");
1327
+ offset += written;
1328
+ }
1329
+ }
1330
+
1331
+ async function readBoundedStdin(): Promise<unknown> {
1332
+ const reader = Bun.stdin.stream().getReader();
1333
+ const chunks: Uint8Array[] = [];
1334
+ let length = 0;
1335
+ try {
1336
+ for (;;) {
1337
+ const item = await reader.read();
1338
+ if (item.done) break;
1339
+ length += item.value.byteLength;
1340
+ if (length > Math.max(
1341
+ WHATSAPP_INTERACTION_PROJECTION_MAX_STDIN_BYTES,
1342
+ WHATSAPP_MESSAGE_EXPORT_PROJECTION_MAX_STDIN_BYTES,
1343
+ )) {
1344
+ return fail("request-invalid");
1345
+ }
1346
+ chunks.push(item.value);
1347
+ }
1348
+ } catch {
1349
+ return fail("request-invalid");
1350
+ } finally {
1351
+ reader.releaseLock();
1352
+ }
1353
+ if (length < 2) return fail("request-invalid");
1354
+ try {
1355
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(
1356
+ Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)), length),
1357
+ )) as unknown;
1358
+ } catch {
1359
+ return fail("request-invalid");
1360
+ }
1361
+ }
1362
+
1363
+ export async function runWhatsAppInteractionProjectionHelper(): Promise<
1364
+ unknown
1365
+ > {
1366
+ let requestValue: unknown;
1367
+ try {
1368
+ if (process.argv.length !== 2) fail("request-invalid");
1369
+ requestValue = await readBoundedStdin();
1370
+ } catch {
1371
+ return createWhatsAppInteractionProjectionFailure("request-invalid");
1372
+ }
1373
+ const session = typeof requestValue === "object"
1374
+ && requestValue !== null
1375
+ && !Array.isArray(requestValue)
1376
+ && "operation" in requestValue
1377
+ && requestValue.operation === "message-like-me.export-session";
1378
+ if (session) {
1379
+ const record = requestValue as Readonly<Record<string, unknown>>;
1380
+ if (
1381
+ Object.keys(record).sort().join("\0") !== "operation\0request"
1382
+ ) {
1383
+ writeSessionFrame(Object.freeze({
1384
+ kind: "failed" as const,
1385
+ errorCode: "request-invalid" as const,
1386
+ }));
1387
+ return undefined;
1388
+ }
1389
+ try {
1390
+ const summary = projectWhatsAppMessageExportSessionFromBoundCwd(
1391
+ record.request,
1392
+ writeSessionFrame,
1393
+ );
1394
+ // projectBoundWhatsAppMessageStore closes and revalidates the database
1395
+ // before returning. The seal therefore proves that every page preceded
1396
+ // a clean close of the one identity-bound snapshot.
1397
+ writeSessionFrame(Object.freeze({ kind: "seal" as const, ...summary }));
1398
+ return undefined;
1399
+ } catch (error) {
1400
+ writeSessionFrame(Object.freeze({
1401
+ kind: "failed" as const,
1402
+ errorCode: errorCode(error),
1403
+ }));
1404
+ return undefined;
1405
+ }
1406
+ }
1407
+ const messageExport = typeof requestValue === "object"
1408
+ && requestValue !== null
1409
+ && !Array.isArray(requestValue)
1410
+ && "operation" in requestValue
1411
+ && requestValue.operation === "message-like-me.export";
1412
+ if (messageExport) {
1413
+ let request: WhatsAppMessageExportProjectionRequest;
1414
+ try {
1415
+ request = parseWhatsAppMessageExportProjectionRequest(requestValue);
1416
+ } catch {
1417
+ return createWhatsAppMessageExportProjectionFailure("request-invalid");
1418
+ }
1419
+ try {
1420
+ return projectWhatsAppMessageExportFromBoundCwd(request);
1421
+ } catch (error) {
1422
+ return createWhatsAppMessageExportProjectionFailure(errorCode(error));
1423
+ }
1424
+ }
1425
+ let request: WhatsAppInteractionProjectionRequest;
1426
+ try {
1427
+ request = parseWhatsAppInteractionProjectionRequest(requestValue);
1428
+ } catch {
1429
+ return createWhatsAppInteractionProjectionFailure("request-invalid");
1430
+ }
1431
+ try {
1432
+ return projectWhatsAppInteractionsFromBoundCwd(request);
1433
+ } catch (error) {
1434
+ return createWhatsAppInteractionProjectionFailure(errorCode(error));
1435
+ }
1436
+ }
1437
+
1438
+ async function main(): Promise<void> {
1439
+ let response = await runWhatsAppInteractionProjectionHelper();
1440
+ if (response === undefined) return;
1441
+ let output = `${JSON.stringify(response)}\n`;
1442
+ if (Buffer.byteLength(output, "utf8") > Math.max(
1443
+ WHATSAPP_INTERACTION_PROJECTION_MAX_STDOUT_BYTES,
1444
+ WHATSAPP_MESSAGE_EXPORT_PROJECTION_MAX_STDOUT_BYTES,
1445
+ )) {
1446
+ response = createWhatsAppMessageExportProjectionFailure("output-too-large");
1447
+ output = `${JSON.stringify(response)}\n`;
1448
+ }
1449
+ await Bun.write(Bun.stdout, output);
1450
+ }
1451
+
1452
+ if (import.meta.main) await main();