@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,2274 @@
1
+ import { constants } from "node:fs";
2
+ import {
3
+ chmod,
4
+ lstat,
5
+ mkdtemp,
6
+ open,
7
+ readdir,
8
+ realpath,
9
+ rename,
10
+ rmdir,
11
+ type FileHandle,
12
+ } from "node:fs/promises";
13
+ import { basename, dirname, isAbsolute, resolve, sep } from "node:path";
14
+ import { createHash } from "node:crypto";
15
+ import { types as nodeTypes } from "node:util";
16
+ import { dlopen, ptr } from "bun:ffi";
17
+
18
+ import {
19
+ LOCAL_MESSAGE_BUNDLE_V1_ARTIFACTS,
20
+ LOCAL_MESSAGE_BUNDLE_V1_FORMAT,
21
+ LOCAL_MESSAGE_BUNDLE_V1_LIMITS,
22
+ LOCAL_MESSAGE_BUNDLE_V1_PROVIDER_ID,
23
+ LOCAL_MESSAGE_BUNDLE_V1_SCHEMA_VERSION,
24
+ LOCAL_MESSAGE_BUNDLE_V1_SOURCE_ID,
25
+ LOCAL_MESSAGE_BUNDLE_V1_SOURCE_TRANSFORM_VERSION,
26
+ parseLocalMessageBundleV1Manifest,
27
+ parseLocalMessageBundleV1Record,
28
+ type LocalMessageBundleV1AccountRecord,
29
+ type LocalMessageBundleV1AttachmentRecord,
30
+ type LocalMessageBundleV1ConversationRecord,
31
+ type LocalMessageBundleV1MessageRecord,
32
+ type LocalMessageBundleV1ParticipantRecord,
33
+ type LocalMessageBundleV1Provenance,
34
+ type LocalMessageBundleV1ReactionRecord,
35
+ type LocalMessageBundleV1Record,
36
+ type LocalMessageBundleV1RecordKind,
37
+ type LocalMessageBundleV1TombstoneRecord,
38
+ } from "@hraness/message-like-me/message-bundle-v1";
39
+
40
+ import { canonicalJson, sha256 } from "./canonical-json";
41
+ import {
42
+ createBeeperMessageLikeMeDirectoryLease,
43
+ releaseBeeperMessageLikeMeDirectoryLease,
44
+ type BeeperMessageLikeMeDirectoryLease,
45
+ } from "./beeper-message-like-me-recovery";
46
+ import { removePrivateDirectoryTree } from "./storage";
47
+
48
+ export const BEEPER_MESSAGE_LIKE_ME_SCHEMA_VERSION =
49
+ LOCAL_MESSAGE_BUNDLE_V1_SCHEMA_VERSION;
50
+ export const BEEPER_MESSAGE_LIKE_ME_MAX_RECORDS =
51
+ LOCAL_MESSAGE_BUNDLE_V1_LIMITS.records;
52
+ export const BEEPER_MESSAGE_LIKE_ME_MAX_TOTAL_BYTES =
53
+ LOCAL_MESSAGE_BUNDLE_V1_LIMITS.totalBytes;
54
+
55
+ const PRIVATE_DIRECTORY_MODE = 0o700;
56
+ const PRIVATE_FILE_MODE = 0o600;
57
+ const MAX_IDENTIFIER_BYTES = LOCAL_MESSAGE_BUNDLE_V1_LIMITS.identifierBytes;
58
+ const MAX_SHORT_TEXT_BYTES = LOCAL_MESSAGE_BUNDLE_V1_LIMITS.shortTextBytes;
59
+ const MAX_BODY_BYTES = LOCAL_MESSAGE_BUNDLE_V1_LIMITS.bodyBytes;
60
+ const MAX_WARNING_CODES = LOCAL_MESSAGE_BUNDLE_V1_LIMITS.warnings;
61
+ const MAX_PARTICIPANTS = LOCAL_MESSAGE_BUNDLE_V1_LIMITS.participantsPerConversation;
62
+ const MAX_ATTACHMENTS = LOCAL_MESSAGE_BUNDLE_V1_LIMITS.attachmentsPerMessage;
63
+ const MAX_CONNECTED_ACCOUNTS = LOCAL_MESSAGE_BUNDLE_V1_LIMITS.accounts;
64
+ const BUNDLE_HEARTBEAT_INTERVAL_MS = 30_000;
65
+ const DARWIN_RENAME_EXCL = 0x0000_0004;
66
+ const LINUX_RENAME_NOREPLACE = 0x0000_0001;
67
+ const AT_FDCWD = -100;
68
+
69
+ type NativeExclusiveRename = (
70
+ source: Uint8Array,
71
+ destination: Uint8Array,
72
+ ) => number;
73
+
74
+ let cachedNativeExclusiveRename: NativeExclusiveRename | undefined;
75
+
76
+ const HARD_LIMITS = Object.freeze({
77
+ maxRecords: BEEPER_MESSAGE_LIKE_ME_MAX_RECORDS,
78
+ maxRecordBytes: LOCAL_MESSAGE_BUNDLE_V1_LIMITS.recordBytes,
79
+ maxTotalBytes: BEEPER_MESSAGE_LIKE_ME_MAX_TOTAL_BYTES,
80
+ });
81
+
82
+ export type BeeperMessageLikeMeExportLimits = {
83
+ readonly maxRecords: number;
84
+ readonly maxRecordBytes: number;
85
+ readonly maxTotalBytes: number;
86
+ };
87
+
88
+ export type BeeperMessageLikeMeBundleProgress =
89
+ | Readonly<{
90
+ phase: "bundle-building";
91
+ elapsedSeconds: number;
92
+ records: number;
93
+ bytes: number;
94
+ }>
95
+ | Readonly<{
96
+ phase: "bundle-validating";
97
+ elapsedSeconds: number;
98
+ records: number;
99
+ bytes: number;
100
+ }>
101
+ | Readonly<{
102
+ phase: "bundle-publishing";
103
+ elapsedSeconds: number;
104
+ records: number;
105
+ bytes: number;
106
+ }>
107
+ | Readonly<{
108
+ phase: "private-cleanup";
109
+ elapsedSeconds: number;
110
+ }>;
111
+
112
+ export type BeeperMessageLikeMeExportSource = {
113
+ /** Parsed as foreign data before any output directory is created. */
114
+ readonly descriptor: unknown;
115
+ /** Each yielded value is parsed strictly and written once. */
116
+ readonly records: AsyncIterable<unknown>;
117
+ /** Called only after the record stream ends successfully. */
118
+ readonly completion: () => Promise<unknown>;
119
+ /** Called exactly once after publication or failure cleanup. */
120
+ readonly dispose?: (published: boolean) => Promise<void>;
121
+ };
122
+
123
+ export type BeeperMessageLikeMeExportRequest = {
124
+ readonly outputRoot: string;
125
+ readonly source: BeeperMessageLikeMeExportSource;
126
+ readonly limits?: Partial<BeeperMessageLikeMeExportLimits>;
127
+ readonly signal?: AbortSignal;
128
+ readonly onProgress?: (progress: BeeperMessageLikeMeBundleProgress) => void;
129
+ /** Internal CLI composition seam for durable private-stage recovery. */
130
+ readonly recoveryEnvironment?: Readonly<Record<string, string | undefined>>;
131
+ /** Test seam. Production callers should omit it. */
132
+ readonly clock?: () => Date;
133
+ };
134
+
135
+ export type BeeperMessageLikeMeProvenance = LocalMessageBundleV1Provenance;
136
+ export type BeeperMessageLikeMeAccount = LocalMessageBundleV1AccountRecord;
137
+ export type BeeperMessageLikeMeParticipant = LocalMessageBundleV1ParticipantRecord;
138
+ export type BeeperMessageLikeMeConversation = LocalMessageBundleV1ConversationRecord;
139
+ export type BeeperMessageLikeMeAttachment = LocalMessageBundleV1AttachmentRecord;
140
+ export type BeeperMessageLikeMeMessage = LocalMessageBundleV1MessageRecord;
141
+ export type BeeperMessageLikeMeReaction = LocalMessageBundleV1ReactionRecord;
142
+ export type BeeperMessageLikeMeTombstone = LocalMessageBundleV1TombstoneRecord;
143
+ export type BeeperMessageLikeMeRecord = LocalMessageBundleV1Record;
144
+
145
+ export type BeeperMessageLikeMeArtifact = {
146
+ readonly path: string;
147
+ readonly mediaType: "application/x-ndjson";
148
+ readonly recordKind:
149
+ | "account"
150
+ | "participant"
151
+ | "conversation"
152
+ | "message"
153
+ | "reaction"
154
+ | "tombstone";
155
+ readonly records: number;
156
+ readonly bytes: number;
157
+ readonly sha256: string;
158
+ };
159
+
160
+ export type BeeperMessageLikeMeManifest = {
161
+ readonly schemaVersion: 1;
162
+ readonly format: typeof LOCAL_MESSAGE_BUNDLE_V1_FORMAT;
163
+ readonly source: {
164
+ readonly id: typeof LOCAL_MESSAGE_BUNDLE_V1_SOURCE_ID;
165
+ readonly version: string;
166
+ };
167
+ readonly provider: {
168
+ readonly id: typeof LOCAL_MESSAGE_BUNDLE_V1_PROVIDER_ID;
169
+ readonly version: string;
170
+ };
171
+ readonly timestamps: {
172
+ readonly startedAt: string;
173
+ readonly finishedAt: string;
174
+ readonly createdAt: string;
175
+ };
176
+ readonly completeness: {
177
+ readonly kind: "bounded-local" | "truncated" | "unknown";
178
+ readonly reason: string | null;
179
+ readonly observedFrom: string | null;
180
+ readonly observedThrough: string | null;
181
+ };
182
+ readonly warnings: readonly string[];
183
+ readonly privacy: {
184
+ readonly classification: "private-local";
185
+ readonly attachments: "metadata-only";
186
+ readonly providerUrls: "excluded";
187
+ readonly credentials: "excluded";
188
+ };
189
+ readonly counts: Readonly<Record<BeeperMessageLikeMeArtifact["recordKind"], number>>;
190
+ readonly artifacts: readonly BeeperMessageLikeMeArtifact[];
191
+ readonly integrity: {
192
+ readonly algorithm: "sha256";
193
+ readonly bundleSha256: string;
194
+ };
195
+ };
196
+
197
+ export type BeeperMessageLikeMeExportResult = {
198
+ readonly outputRoot: string;
199
+ readonly manifestPath: string;
200
+ readonly manifestSha256: string;
201
+ readonly manifest: BeeperMessageLikeMeManifest;
202
+ };
203
+
204
+ type JsonRecord = Record<string, unknown>;
205
+ type RecordKind = LocalMessageBundleV1RecordKind;
206
+
207
+ type ParsedDescriptor = {
208
+ readonly source: {
209
+ readonly id: typeof LOCAL_MESSAGE_BUNDLE_V1_SOURCE_ID;
210
+ readonly version: string;
211
+ };
212
+ readonly provider: {
213
+ readonly id: typeof LOCAL_MESSAGE_BUNDLE_V1_PROVIDER_ID;
214
+ readonly version: string;
215
+ };
216
+ };
217
+
218
+ type ParsedCompletion = Pick<BeeperMessageLikeMeManifest, "completeness" | "warnings">;
219
+
220
+ type ParsedProvenance = {
221
+ readonly providerId: string;
222
+ readonly providerRevision: string | null;
223
+ readonly observedAt: string;
224
+ readonly connectedAccountProviderId: string;
225
+ };
226
+
227
+ type ParsedRecord = {
228
+ readonly kind: RecordKind;
229
+ readonly id: string;
230
+ readonly accountId?: string;
231
+ readonly network?: string;
232
+ readonly connectedAccountProviderId?: string;
233
+ readonly value: BeeperMessageLikeMeRecord;
234
+ };
235
+
236
+ type BundleGraphFact =
237
+ | Readonly<{
238
+ kind: "account";
239
+ id: string;
240
+ accountId: string;
241
+ providerId: string;
242
+ network: string;
243
+ selfParticipantId: string;
244
+ }>
245
+ | Readonly<{
246
+ kind: "participant";
247
+ id: string;
248
+ accountId: string;
249
+ providerId: string;
250
+ isSelf: boolean;
251
+ }>
252
+ | Readonly<{
253
+ kind: "conversation";
254
+ id: string;
255
+ accountId: string;
256
+ providerId: string;
257
+ type: BeeperMessageLikeMeConversation["type"];
258
+ participantIds: readonly string[];
259
+ participantsComplete: boolean | null;
260
+ }>
261
+ | Readonly<{
262
+ kind: "message";
263
+ id: string;
264
+ accountId: string;
265
+ providerId: string;
266
+ conversationId: string;
267
+ senderParticipantId: string | null;
268
+ direction: BeeperMessageLikeMeMessage["direction"];
269
+ replyTo: BeeperMessageLikeMeMessage["replyTo"];
270
+ edit: BeeperMessageLikeMeMessage["edit"];
271
+ }>
272
+ | Readonly<{
273
+ kind: "reaction";
274
+ id: string;
275
+ accountId: string;
276
+ providerId: string;
277
+ messageId: string | null;
278
+ messageProviderId: string;
279
+ participantId: string | null;
280
+ }>
281
+ | Readonly<{
282
+ kind: "tombstone";
283
+ id: string;
284
+ accountId: string;
285
+ providerId: string;
286
+ entityKind: BeeperMessageLikeMeTombstone["entityKind"];
287
+ entityId: string | null;
288
+ entityProviderId: string;
289
+ }>;
290
+
291
+ type BundleGraphInventory = ReadonlyMap<
292
+ RecordKind,
293
+ ReadonlyMap<string, BundleGraphFact>
294
+ >;
295
+
296
+ type ArtifactWriter = {
297
+ readonly kind: RecordKind;
298
+ readonly fileName: string;
299
+ readonly partPath: string;
300
+ readonly handle: FileHandle;
301
+ readonly hash: ReturnType<typeof createHash>;
302
+ records: number;
303
+ bytes: number;
304
+ closed: boolean;
305
+ };
306
+
307
+ type PrivateDirectoryIdentity = {
308
+ readonly device: number;
309
+ readonly inode: number;
310
+ };
311
+
312
+ type StagedManifest = {
313
+ readonly bytes: number;
314
+ readonly sha256: string;
315
+ };
316
+
317
+ type PublishedDirectory = {
318
+ readonly path: string;
319
+ readonly parent: string;
320
+ readonly identity: PrivateDirectoryIdentity;
321
+ readonly parentIdentity: PrivateDirectoryIdentity;
322
+ };
323
+
324
+ type PublishedBundle = {
325
+ readonly result: BeeperMessageLikeMeExportResult;
326
+ readonly directory: PublishedDirectory;
327
+ readonly directoryLease?: BeeperMessageLikeMeDirectoryLease;
328
+ };
329
+
330
+ const ARTIFACTS = Object.freeze(LOCAL_MESSAGE_BUNDLE_V1_ARTIFACTS.map(
331
+ ({ kind, path }) => Object.freeze({ kind, fileName: path }),
332
+ ));
333
+
334
+ function fail(message: string): never {
335
+ throw new Error(`Beeper Message Like Me export: ${message}`);
336
+ }
337
+
338
+ function isErrno(error: unknown, code: string): boolean {
339
+ return typeof error === "object" && error !== null && "code" in error
340
+ && (error as { readonly code?: unknown }).code === code;
341
+ }
342
+
343
+ function foreignRecord(value: unknown, label: string): JsonRecord {
344
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
345
+ return fail(`${label} must be a plain object`);
346
+ }
347
+ if (nodeTypes.isProxy(value)) return fail(`${label} must be a plain object`);
348
+ const prototype = Object.getPrototypeOf(value) as unknown;
349
+ if (prototype !== Object.prototype && prototype !== null) {
350
+ return fail(`${label} must be a plain object`);
351
+ }
352
+ const keys = Reflect.ownKeys(value);
353
+ if (keys.some((key) => typeof key !== "string")) {
354
+ return fail(`${label} must not contain symbol keys`);
355
+ }
356
+ const descriptors = Object.getOwnPropertyDescriptors(value);
357
+ for (const key of keys as string[]) {
358
+ const descriptor = descriptors[key];
359
+ if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor)) {
360
+ return fail(`${label}.${key} must be an enumerable data property`);
361
+ }
362
+ }
363
+ return value as JsonRecord;
364
+ }
365
+
366
+ function exactKeys(value: JsonRecord, expected: readonly string[], label: string): void {
367
+ const observed = Object.keys(value).sort();
368
+ const wanted = [...expected].sort();
369
+ if (
370
+ observed.length !== wanted.length
371
+ || observed.some((key, index) => key !== wanted[index])
372
+ ) {
373
+ fail(`${label} must contain exactly: ${expected.join(", ")}`);
374
+ }
375
+ }
376
+
377
+ function utf8Length(value: string): number {
378
+ return Buffer.byteLength(value, "utf8");
379
+ }
380
+
381
+ function text(value: unknown, label: string, maximumBytes: number): string {
382
+ if (typeof value !== "string" || utf8Length(value) > maximumBytes || value.includes("\0")) {
383
+ return fail(`${label} must be a NUL-free string of at most ${String(maximumBytes)} UTF-8 bytes`);
384
+ }
385
+ return value;
386
+ }
387
+
388
+ function nullableText(value: unknown, label: string, maximumBytes: number): string | null {
389
+ return value === null ? null : text(value, label, maximumBytes);
390
+ }
391
+
392
+ function identifier(value: unknown, label: string): string {
393
+ const parsed = text(value, label, MAX_IDENTIFIER_BYTES);
394
+ if (parsed.length === 0 || /[\u0000-\u001f\u007f]/u.test(parsed)) {
395
+ return fail(`${label} must be a non-empty identifier without ASCII control characters`);
396
+ }
397
+ return parsed;
398
+ }
399
+
400
+ function nullableIdentifier(value: unknown, label: string): string | null {
401
+ return value === null ? null : identifier(value, label);
402
+ }
403
+
404
+ function token(value: unknown, label: string, maximumBytes = 128): string {
405
+ const parsed = text(value, label, maximumBytes);
406
+ if (!/^[a-z0-9](?:[a-z0-9._+-]*[a-z0-9])?$/u.test(parsed)) {
407
+ return fail(`${label} must be a lowercase categorical token`);
408
+ }
409
+ return parsed;
410
+ }
411
+
412
+ function version(value: unknown, label: string): string {
413
+ const parsed = text(value, label, 128);
414
+ if (!/^[A-Za-z0-9](?:[A-Za-z0-9._+-]*[A-Za-z0-9])?$/u.test(parsed)) {
415
+ return fail(`${label} must be a bounded version token`);
416
+ }
417
+ return parsed;
418
+ }
419
+
420
+ function oneOf<const Values extends readonly string[]>(
421
+ value: unknown,
422
+ values: Values,
423
+ label: string,
424
+ ): Values[number] {
425
+ if (typeof value !== "string" || !values.includes(value)) {
426
+ return fail(`${label} must be one of: ${values.join(", ")}`);
427
+ }
428
+ return value as Values[number];
429
+ }
430
+
431
+ function boolean(value: unknown, label: string): boolean {
432
+ if (typeof value !== "boolean") return fail(`${label} must be a boolean`);
433
+ return value;
434
+ }
435
+
436
+ function nullableBoolean(value: unknown, label: string): boolean | null {
437
+ return value === null ? null : boolean(value, label);
438
+ }
439
+
440
+ function integer(value: unknown, label: string, maximum = Number.MAX_SAFE_INTEGER): number {
441
+ if (!Number.isSafeInteger(value) || (value as number) < 0 || (value as number) > maximum) {
442
+ return fail(`${label} must be a non-negative safe integer at most ${String(maximum)}`);
443
+ }
444
+ return value as number;
445
+ }
446
+
447
+ function nullableInteger(value: unknown, label: string): number | null {
448
+ return value === null ? null : integer(value, label);
449
+ }
450
+
451
+ function timestamp(value: unknown, label: string): string {
452
+ const parsed = text(value, label, 64);
453
+ const instant = new Date(parsed);
454
+ if (!Number.isFinite(instant.getTime()) || instant.toISOString() !== parsed) {
455
+ return fail(`${label} must be a canonical UTC timestamp with millisecond precision`);
456
+ }
457
+ return parsed;
458
+ }
459
+
460
+ function nullableTimestamp(value: unknown, label: string): string | null {
461
+ return value === null ? null : timestamp(value, label);
462
+ }
463
+
464
+ function array(value: unknown, label: string, maximum: number): readonly unknown[] {
465
+ if (!Array.isArray(value) || value.length > maximum) {
466
+ return fail(`${label} must be an array of at most ${String(maximum)} items`);
467
+ }
468
+ return value;
469
+ }
470
+
471
+ function uniqueIdentifiers(value: unknown, label: string, maximum: number): readonly string[] {
472
+ const parsed = array(value, label, maximum).map((item, index) =>
473
+ identifier(item, `${label}[${String(index)}]`));
474
+ if (new Set(parsed).size !== parsed.length) fail(`${label} must not contain duplicates`);
475
+ return Object.freeze(parsed);
476
+ }
477
+
478
+ function parseProvenance(value: unknown, label: string): ParsedProvenance {
479
+ const source = foreignRecord(value, label);
480
+ exactKeys(source, [
481
+ "providerId",
482
+ "providerRevision",
483
+ "observedAt",
484
+ "connectedAccountProviderId",
485
+ ], label);
486
+ return Object.freeze({
487
+ providerId: identifier(source.providerId, `${label}.providerId`),
488
+ providerRevision: nullableIdentifier(source.providerRevision, `${label}.providerRevision`),
489
+ observedAt: timestamp(source.observedAt, `${label}.observedAt`),
490
+ connectedAccountProviderId: identifier(
491
+ source.connectedAccountProviderId,
492
+ `${label}.connectedAccountProviderId`,
493
+ ),
494
+ });
495
+ }
496
+
497
+ function parseCommon(
498
+ source: JsonRecord,
499
+ kind: RecordKind,
500
+ expected: readonly string[],
501
+ label: string,
502
+ ): {
503
+ readonly id: string;
504
+ readonly accountId: string;
505
+ readonly network: string;
506
+ readonly provenance: ParsedProvenance;
507
+ } {
508
+ exactKeys(source, ["schemaVersion", "kind", "id", "accountId", "network", "provenance", ...expected], label);
509
+ if (source.schemaVersion !== 1) fail(`${label}.schemaVersion must equal 1`);
510
+ if (source.kind !== kind) fail(`${label}.kind must equal ${kind}`);
511
+ return {
512
+ id: identifier(source.id, `${label}.id`),
513
+ accountId: identifier(source.accountId, `${label}.accountId`),
514
+ network: token(source.network, `${label}.network`, 64),
515
+ provenance: parseProvenance(source.provenance, `${label}.provenance`),
516
+ };
517
+ }
518
+
519
+ function parseAccount(source: JsonRecord, label: string): ParsedRecord {
520
+ const common = parseCommon(source, "account", ["displayName", "handle", "selfParticipantId"], label);
521
+ const value = Object.freeze({
522
+ schemaVersion: 1,
523
+ kind: "account",
524
+ ...common,
525
+ displayName: nullableText(source.displayName, `${label}.displayName`, MAX_SHORT_TEXT_BYTES),
526
+ handle: nullableText(source.handle, `${label}.handle`, MAX_SHORT_TEXT_BYTES),
527
+ selfParticipantId: identifier(source.selfParticipantId, `${label}.selfParticipantId`),
528
+ });
529
+ if (common.id !== common.accountId) fail(`${label}.id and ${label}.accountId must match`);
530
+ if (common.provenance.providerId !== common.provenance.connectedAccountProviderId) {
531
+ fail(`${label}.provenance.providerId must identify the connected account`);
532
+ }
533
+ return {
534
+ kind: "account",
535
+ id: common.id,
536
+ accountId: common.accountId,
537
+ network: common.network,
538
+ connectedAccountProviderId: common.provenance.connectedAccountProviderId,
539
+ value,
540
+ };
541
+ }
542
+
543
+ function parseParticipant(source: JsonRecord, label: string): ParsedRecord {
544
+ const common = parseCommon(source, "participant", ["displayName", "handle", "isSelf"], label);
545
+ return {
546
+ kind: "participant",
547
+ id: common.id,
548
+ accountId: common.accountId,
549
+ network: common.network,
550
+ connectedAccountProviderId: common.provenance.connectedAccountProviderId,
551
+ value: Object.freeze({
552
+ schemaVersion: 1,
553
+ kind: "participant",
554
+ ...common,
555
+ displayName: nullableText(source.displayName, `${label}.displayName`, MAX_SHORT_TEXT_BYTES),
556
+ handle: nullableText(source.handle, `${label}.handle`, MAX_SHORT_TEXT_BYTES),
557
+ isSelf: boolean(source.isSelf, `${label}.isSelf`),
558
+ }),
559
+ };
560
+ }
561
+
562
+ function parseConversation(source: JsonRecord, label: string): ParsedRecord {
563
+ const common = parseCommon(source, "conversation", [
564
+ "type", "title", "participantIds", "participantsComplete", "startedAt", "lastMessageAt",
565
+ ], label);
566
+ const startedAt = nullableTimestamp(source.startedAt, `${label}.startedAt`);
567
+ const lastMessageAt = nullableTimestamp(source.lastMessageAt, `${label}.lastMessageAt`);
568
+ if (startedAt !== null && lastMessageAt !== null && startedAt > lastMessageAt) {
569
+ fail(`${label}.startedAt must not be after lastMessageAt`);
570
+ }
571
+ return {
572
+ kind: "conversation",
573
+ id: common.id,
574
+ accountId: common.accountId,
575
+ network: common.network,
576
+ connectedAccountProviderId: common.provenance.connectedAccountProviderId,
577
+ value: Object.freeze({
578
+ schemaVersion: 1,
579
+ kind: "conversation",
580
+ ...common,
581
+ type: oneOf(source.type, ["direct", "group", "channel", "unknown"] as const, `${label}.type`),
582
+ title: nullableText(source.title, `${label}.title`, MAX_SHORT_TEXT_BYTES),
583
+ participantIds: uniqueIdentifiers(source.participantIds, `${label}.participantIds`, MAX_PARTICIPANTS),
584
+ participantsComplete: nullableBoolean(
585
+ source.participantsComplete,
586
+ `${label}.participantsComplete`,
587
+ ),
588
+ startedAt,
589
+ lastMessageAt,
590
+ }),
591
+ };
592
+ }
593
+
594
+ function parseReply(value: unknown, label: string): BeeperMessageLikeMeMessage["replyTo"] {
595
+ if (value === null) return null;
596
+ const source = foreignRecord(value, label);
597
+ exactKeys(source, ["messageId", "providerId"], label);
598
+ return Object.freeze({
599
+ messageId: source.messageId === null ? null : identifier(source.messageId, `${label}.messageId`),
600
+ providerId: identifier(source.providerId, `${label}.providerId`),
601
+ });
602
+ }
603
+
604
+ function parseEdit(value: unknown, label: string): BeeperMessageLikeMeMessage["edit"] {
605
+ if (value === null) return null;
606
+ const source = foreignRecord(value, label);
607
+ if (source.kind === "in-place") {
608
+ exactKeys(source, ["kind", "editedAt", "providerRevision"], label);
609
+ return Object.freeze({
610
+ kind: "in-place",
611
+ editedAt: timestamp(source.editedAt, `${label}.editedAt`),
612
+ providerRevision: identifier(source.providerRevision, `${label}.providerRevision`),
613
+ });
614
+ }
615
+ if (source.kind !== "replacement") fail(`${label}.kind is unsupported`);
616
+ exactKeys(source, [
617
+ "kind",
618
+ "replacesMessageId",
619
+ "replacesProviderId",
620
+ "editedAt",
621
+ "providerRevision",
622
+ ], label);
623
+ return Object.freeze({
624
+ kind: "replacement",
625
+ replacesMessageId: source.replacesMessageId === null
626
+ ? null
627
+ : identifier(source.replacesMessageId, `${label}.replacesMessageId`),
628
+ replacesProviderId: identifier(
629
+ source.replacesProviderId,
630
+ `${label}.replacesProviderId`,
631
+ ),
632
+ editedAt: timestamp(source.editedAt, `${label}.editedAt`),
633
+ providerRevision: identifier(source.providerRevision, `${label}.providerRevision`),
634
+ });
635
+ }
636
+
637
+ function parseDeletion(value: unknown, label: string): BeeperMessageLikeMeMessage["deletion"] {
638
+ if (value === null) return null;
639
+ const source = foreignRecord(value, label);
640
+ exactKeys(source, ["state", "observedAt", "providerRevision"], label);
641
+ return Object.freeze({
642
+ state: oneOf(source.state, [
643
+ "revoked", "deleted-for-me", "revoked-and-deleted-for-me",
644
+ ] as const, `${label}.state`),
645
+ observedAt: timestamp(source.observedAt, `${label}.observedAt`),
646
+ providerRevision: nullableIdentifier(source.providerRevision, `${label}.providerRevision`),
647
+ });
648
+ }
649
+
650
+ function parseAttachments(value: unknown, label: string): readonly BeeperMessageLikeMeAttachment[] {
651
+ return Object.freeze(array(value, label, MAX_ATTACHMENTS).map((item, index) => {
652
+ const itemLabel = `${label}[${String(index)}]`;
653
+ const source = foreignRecord(item, itemLabel);
654
+ exactKeys(source, ["kind", "mimeType", "name", "sizeBytes"], itemLabel);
655
+ const name = nullableText(source.name, `${itemLabel}.name`, MAX_SHORT_TEXT_BYTES);
656
+ if (name !== null && (name === "." || name === ".." || name.includes("/") || name.includes("\\"))) {
657
+ fail(`${itemLabel}.name must be a base name, not a local path`);
658
+ }
659
+ return Object.freeze({
660
+ kind: oneOf(source.kind, [
661
+ "audio", "document", "image", "link", "sticker", "video", "unknown",
662
+ ] as const, `${itemLabel}.kind`),
663
+ mimeType: nullableText(source.mimeType, `${itemLabel}.mimeType`, 256),
664
+ name,
665
+ sizeBytes: nullableInteger(source.sizeBytes, `${itemLabel}.sizeBytes`),
666
+ });
667
+ }));
668
+ }
669
+
670
+ function parseMessage(source: JsonRecord, label: string): ParsedRecord {
671
+ const common = parseCommon(source, "message", [
672
+ "conversationId",
673
+ "senderParticipantId",
674
+ "direction",
675
+ "sentAt",
676
+ "sortKey",
677
+ "body",
678
+ "bodyTruncated",
679
+ "replyTo",
680
+ "edit",
681
+ "deletion",
682
+ "attachments",
683
+ ], label);
684
+ const sentAt = timestamp(source.sentAt, `${label}.sentAt`);
685
+ const edit = parseEdit(source.edit, `${label}.edit`);
686
+ if (edit !== null && edit.editedAt < sentAt) {
687
+ fail(`${label}.edit.editedAt must not be before sentAt`);
688
+ }
689
+ const deletion = parseDeletion(source.deletion, `${label}.deletion`);
690
+ const body = nullableText(source.body, `${label}.body`, MAX_BODY_BYTES);
691
+ if (deletion !== null && body !== null) {
692
+ fail(`${label}.body must be null when deletion is present`);
693
+ }
694
+ return {
695
+ kind: "message",
696
+ id: common.id,
697
+ accountId: common.accountId,
698
+ network: common.network,
699
+ connectedAccountProviderId: common.provenance.connectedAccountProviderId,
700
+ value: Object.freeze({
701
+ schemaVersion: 1,
702
+ kind: "message",
703
+ ...common,
704
+ conversationId: identifier(source.conversationId, `${label}.conversationId`),
705
+ senderParticipantId: source.senderParticipantId === null
706
+ ? null
707
+ : identifier(source.senderParticipantId, `${label}.senderParticipantId`),
708
+ direction: oneOf(source.direction, ["incoming", "outgoing", "unknown"] as const, `${label}.direction`),
709
+ sentAt,
710
+ sortKey: identifier(source.sortKey, `${label}.sortKey`),
711
+ body,
712
+ bodyTruncated: nullableBoolean(source.bodyTruncated, `${label}.bodyTruncated`),
713
+ replyTo: parseReply(source.replyTo, `${label}.replyTo`),
714
+ edit,
715
+ deletion,
716
+ attachments: parseAttachments(source.attachments, `${label}.attachments`),
717
+ }),
718
+ };
719
+ }
720
+
721
+ function parseReaction(source: JsonRecord, label: string): ParsedRecord {
722
+ const common = parseCommon(source, "reaction", [
723
+ "messageId", "messageProviderId", "participantId", "body", "reactedAt", "state",
724
+ ], label);
725
+ return {
726
+ kind: "reaction",
727
+ id: common.id,
728
+ accountId: common.accountId,
729
+ network: common.network,
730
+ connectedAccountProviderId: common.provenance.connectedAccountProviderId,
731
+ value: Object.freeze({
732
+ schemaVersion: 1,
733
+ kind: "reaction",
734
+ ...common,
735
+ messageId: source.messageId === null
736
+ ? null
737
+ : identifier(source.messageId, `${label}.messageId`),
738
+ messageProviderId: identifier(
739
+ source.messageProviderId,
740
+ `${label}.messageProviderId`,
741
+ ),
742
+ participantId: source.participantId === null
743
+ ? null
744
+ : identifier(source.participantId, `${label}.participantId`),
745
+ body: text(source.body, `${label}.body`, MAX_SHORT_TEXT_BYTES),
746
+ reactedAt: nullableTimestamp(source.reactedAt, `${label}.reactedAt`),
747
+ state: oneOf(source.state, ["active", "removed"] as const, `${label}.state`),
748
+ }),
749
+ };
750
+ }
751
+
752
+ function parseTombstone(source: JsonRecord, label: string): ParsedRecord {
753
+ const common = parseCommon(source, "tombstone", [
754
+ "entityKind", "entityId", "entityProviderId", "deletedAt", "scope", "providerRevision",
755
+ ], label);
756
+ return {
757
+ kind: "tombstone",
758
+ id: common.id,
759
+ accountId: common.accountId,
760
+ network: common.network,
761
+ connectedAccountProviderId: common.provenance.connectedAccountProviderId,
762
+ value: Object.freeze({
763
+ schemaVersion: 1,
764
+ kind: "tombstone",
765
+ ...common,
766
+ entityKind: oneOf(source.entityKind, [
767
+ "conversation", "message", "reaction",
768
+ ] as const, `${label}.entityKind`),
769
+ entityId: source.entityId === null ? null : identifier(source.entityId, `${label}.entityId`),
770
+ entityProviderId: identifier(source.entityProviderId, `${label}.entityProviderId`),
771
+ deletedAt: timestamp(source.deletedAt, `${label}.deletedAt`),
772
+ scope: oneOf(source.scope, ["remote", "local", "unknown"] as const, `${label}.scope`),
773
+ providerRevision: nullableIdentifier(source.providerRevision, `${label}.providerRevision`),
774
+ }),
775
+ };
776
+ }
777
+
778
+ function parseRecordWithKind(
779
+ source: JsonRecord,
780
+ kind: RecordKind,
781
+ label: string,
782
+ ): ParsedRecord {
783
+ switch (kind) {
784
+ case "account": return parseAccount(source, label);
785
+ case "participant": return parseParticipant(source, label);
786
+ case "conversation": return parseConversation(source, label);
787
+ case "message": return parseMessage(source, label);
788
+ case "reaction": return parseReaction(source, label);
789
+ case "tombstone": return parseTombstone(source, label);
790
+ }
791
+ }
792
+
793
+ function parseRecord(value: unknown, index: number): ParsedRecord {
794
+ const label = `record[${String(index)}]`;
795
+ const candidate = foreignRecord(value, label);
796
+ const kind = oneOf(candidate.kind, [
797
+ "account",
798
+ "participant",
799
+ "conversation",
800
+ "message",
801
+ "reaction",
802
+ "tombstone",
803
+ ] as const, `${label}.kind`);
804
+ let contractRecord: unknown;
805
+ try {
806
+ contractRecord = parseLocalMessageBundleV1Record(value, kind, label);
807
+ } catch {
808
+ // Preserve the legacy Ghostget diagnostic when the legacy parser can name
809
+ // the same invalid field. A value that legacy Ghostget accepted but the
810
+ // shared v1 authority rejects still fails at the generic contract gate.
811
+ parseRecordWithKind(candidate, kind, label);
812
+ return fail(`${label} violates the Message Like Me bundle v1 contract`);
813
+ }
814
+ return parseRecordWithKind(foreignRecord(contractRecord, label), kind, label);
815
+ }
816
+
817
+ /**
818
+ * Strict internal/publication-boundary parser shared by body-bearing bundle
819
+ * publication and content-free derivatives. Returning the normalized record
820
+ * prevents a derivative from trusting the producer's in-memory TypeScript
821
+ * type alone.
822
+ */
823
+ export function parseBeeperMessageLikeMeRecord(
824
+ value: unknown,
825
+ index: number,
826
+ ): BeeperMessageLikeMeRecord {
827
+ if (!Number.isSafeInteger(index) || index < 0 || index >= HARD_LIMITS.maxRecords) {
828
+ fail("record index is outside the bundle bound");
829
+ }
830
+ return parseRecord(value, index).value;
831
+ }
832
+
833
+ function bundleGraphFact(record: BeeperMessageLikeMeRecord): BundleGraphFact {
834
+ const common = {
835
+ id: record.id,
836
+ accountId: record.accountId,
837
+ providerId: record.provenance.providerId,
838
+ } as const;
839
+ switch (record.kind) {
840
+ case "account":
841
+ return Object.freeze({
842
+ kind: record.kind,
843
+ ...common,
844
+ network: record.network,
845
+ selfParticipantId: record.selfParticipantId,
846
+ });
847
+ case "participant":
848
+ return Object.freeze({
849
+ kind: record.kind,
850
+ ...common,
851
+ isSelf: record.isSelf,
852
+ });
853
+ case "conversation":
854
+ return Object.freeze({
855
+ kind: record.kind,
856
+ ...common,
857
+ type: record.type,
858
+ participantIds: record.participantIds,
859
+ participantsComplete: record.participantsComplete,
860
+ });
861
+ case "message":
862
+ return Object.freeze({
863
+ kind: record.kind,
864
+ ...common,
865
+ conversationId: record.conversationId,
866
+ senderParticipantId: record.senderParticipantId,
867
+ direction: record.direction,
868
+ replyTo: record.replyTo,
869
+ edit: record.edit,
870
+ });
871
+ case "reaction":
872
+ return Object.freeze({
873
+ kind: record.kind,
874
+ ...common,
875
+ messageId: record.messageId,
876
+ messageProviderId: record.messageProviderId,
877
+ participantId: record.participantId,
878
+ });
879
+ case "tombstone":
880
+ return Object.freeze({
881
+ kind: record.kind,
882
+ ...common,
883
+ entityKind: record.entityKind,
884
+ entityId: record.entityId,
885
+ entityProviderId: record.entityProviderId,
886
+ });
887
+ }
888
+ }
889
+
890
+ function graphRecords<Kind extends RecordKind>(
891
+ inventory: BundleGraphInventory,
892
+ kind: Kind,
893
+ ): ReadonlyMap<string, Extract<BundleGraphFact, { readonly kind: Kind }>> {
894
+ return (inventory.get(kind) ?? new Map()) as ReadonlyMap<
895
+ string,
896
+ Extract<BundleGraphFact, { readonly kind: Kind }>
897
+ >;
898
+ }
899
+
900
+ function assertSameAccount(
901
+ fact: BundleGraphFact | undefined,
902
+ accountId: string,
903
+ label: string,
904
+ ): asserts fact is BundleGraphFact {
905
+ if (fact === undefined || fact.accountId !== accountId) {
906
+ fail(`${label} does not resolve inside its account realm`);
907
+ }
908
+ }
909
+
910
+ function graphProviderCoordinate(accountId: string, providerId: string): string {
911
+ return sha256(canonicalJson([accountId, providerId]));
912
+ }
913
+
914
+ function providerGraphRecords<Kind extends RecordKind>(
915
+ records: ReadonlyMap<
916
+ string,
917
+ Extract<BundleGraphFact, { readonly kind: Kind }>
918
+ >,
919
+ ): ReadonlyMap<
920
+ string,
921
+ Extract<BundleGraphFact, { readonly kind: Kind }>
922
+ > {
923
+ const providers = new Map<
924
+ string,
925
+ Extract<BundleGraphFact, { readonly kind: Kind }>
926
+ >();
927
+ for (const fact of records.values()) {
928
+ providers.set(
929
+ graphProviderCoordinate(fact.accountId, fact.providerId),
930
+ fact,
931
+ );
932
+ }
933
+ return providers;
934
+ }
935
+
936
+ function validateBundleGraph(inventory: BundleGraphInventory): void {
937
+ const accounts = graphRecords(inventory, "account");
938
+ const participants = graphRecords(inventory, "participant");
939
+ const conversations = graphRecords(inventory, "conversation");
940
+ const messages = graphRecords(inventory, "message");
941
+ const reactions = graphRecords(inventory, "reaction");
942
+ const messagesByProvider = providerGraphRecords(messages);
943
+ const conversationRosters = new Map(
944
+ [...conversations.values()].map((conversation) => [
945
+ conversation.id,
946
+ new Set(conversation.participantIds),
947
+ ]),
948
+ );
949
+ const providerInventories = new Map<RecordKind, ReadonlyMap<string, BundleGraphFact>>(
950
+ ARTIFACTS.map(({ kind }) => {
951
+ const records = graphRecords(inventory, kind);
952
+ return [kind, providerGraphRecords(records)];
953
+ }),
954
+ );
955
+ const resolveMessageTarget = (
956
+ accountId: string,
957
+ localId: string | null,
958
+ providerId: string,
959
+ label: string,
960
+ ): Extract<BundleGraphFact, { readonly kind: "message" }> | undefined => {
961
+ const localTarget = localId === null ? undefined : messages.get(localId);
962
+ if (localId !== null) {
963
+ assertSameAccount(localTarget, accountId, label);
964
+ if (localTarget.kind !== "message" || localTarget.providerId !== providerId) {
965
+ fail(`${label} provider coordinate does not match`);
966
+ }
967
+ }
968
+ const providerTarget = messagesByProvider.get(
969
+ graphProviderCoordinate(accountId, providerId),
970
+ );
971
+ if (
972
+ localTarget !== undefined
973
+ && providerTarget !== undefined
974
+ && localTarget.id !== providerTarget.id
975
+ ) fail(`${label} local and provider coordinates disagree`);
976
+ return localTarget ?? providerTarget;
977
+ };
978
+
979
+ const stableAccountRealms = new Set<string>();
980
+ for (const account of accounts.values()) {
981
+ const self = participants.get(account.selfParticipantId);
982
+ assertSameAccount(self, account.accountId, "account self participant");
983
+ if (self.kind !== "participant" || !self.isSelf) {
984
+ fail("account self participant is not marked as self");
985
+ }
986
+ const stableRealm = sha256(canonicalJson([
987
+ "beeper",
988
+ account.providerId,
989
+ self.providerId,
990
+ ]));
991
+ if (stableAccountRealms.has(stableRealm)) {
992
+ fail("account records repeat one stable provider realm");
993
+ }
994
+ stableAccountRealms.add(stableRealm);
995
+ }
996
+ for (const participant of participants.values()) {
997
+ const account = accounts.get(participant.accountId);
998
+ assertSameAccount(account, participant.accountId, "participant account");
999
+ if (participant.isSelf && account.kind === "account"
1000
+ && account.selfParticipantId !== participant.id) {
1001
+ fail("account realm contains an unreferenced self participant");
1002
+ }
1003
+ }
1004
+ for (const conversation of conversations.values()) {
1005
+ const account = accounts.get(conversation.accountId);
1006
+ assertSameAccount(account, conversation.accountId, "conversation account");
1007
+ for (const participantId of conversation.participantIds) {
1008
+ assertSameAccount(
1009
+ participants.get(participantId),
1010
+ conversation.accountId,
1011
+ "conversation participant",
1012
+ );
1013
+ }
1014
+ if (conversation.type === "direct" && conversation.participantsComplete === true) {
1015
+ const roster = conversation.participantIds.map((participantId) => {
1016
+ const participant = participants.get(participantId);
1017
+ if (participant?.kind !== "participant") {
1018
+ return fail("complete direct conversation participant has the wrong record kind");
1019
+ }
1020
+ return participant;
1021
+ });
1022
+ const selfCount = roster.filter((participant) => participant.isSelf).length;
1023
+ if (
1024
+ account.kind !== "account"
1025
+ || conversation.participantIds.length !== 2
1026
+ || selfCount !== 1
1027
+ || !conversationRosters.get(conversation.id)?.has(account.selfParticipantId)
1028
+ ) {
1029
+ fail("complete direct conversation must contain exactly one self participant and one peer");
1030
+ }
1031
+ }
1032
+ }
1033
+ const replacementEdges = new Map<string, string>();
1034
+ const replacedProviderCoordinates = new Set<string>();
1035
+ for (const message of messages.values()) {
1036
+ const conversation = conversations.get(message.conversationId);
1037
+ assertSameAccount(conversation, message.accountId, "message conversation");
1038
+ if (conversation.kind !== "conversation") {
1039
+ fail("message conversation has the wrong record kind");
1040
+ }
1041
+ if (message.senderParticipantId !== null) {
1042
+ const sender = participants.get(message.senderParticipantId);
1043
+ assertSameAccount(sender, message.accountId, "message sender");
1044
+ if (sender.kind !== "participant") fail("message sender has the wrong record kind");
1045
+ const expectedDirection = sender.isSelf ? "outgoing" : "incoming";
1046
+ if (message.direction !== expectedDirection) {
1047
+ fail("message direction conflicts with its sender participant");
1048
+ }
1049
+ if (
1050
+ conversation.participantsComplete === true
1051
+ && !conversationRosters.get(conversation.id)?.has(sender.id)
1052
+ ) fail("message sender is absent from the complete conversation roster");
1053
+ }
1054
+ if (message.replyTo !== null) {
1055
+ const target = resolveMessageTarget(
1056
+ message.accountId,
1057
+ message.replyTo.messageId,
1058
+ message.replyTo.providerId,
1059
+ "message reply target",
1060
+ );
1061
+ if (target !== undefined) {
1062
+ if (target.id === message.id) fail("message must not reply to itself");
1063
+ if (target.conversationId !== message.conversationId) {
1064
+ fail("message reply target belongs to a different conversation");
1065
+ }
1066
+ }
1067
+ }
1068
+ if (message.edit?.kind === "replacement") {
1069
+ if (message.edit.replacesProviderId === message.providerId) {
1070
+ fail("replacement edit must not replace its own provider coordinate");
1071
+ }
1072
+ const targetCoordinate = graphProviderCoordinate(
1073
+ message.accountId,
1074
+ message.edit.replacesProviderId,
1075
+ );
1076
+ if (replacedProviderCoordinates.has(targetCoordinate)) {
1077
+ fail("one provider message has more than one replacement");
1078
+ }
1079
+ replacedProviderCoordinates.add(targetCoordinate);
1080
+ const target = resolveMessageTarget(
1081
+ message.accountId,
1082
+ message.edit.replacesMessageId,
1083
+ message.edit.replacesProviderId,
1084
+ "replacement edit target",
1085
+ );
1086
+ if (target !== undefined) {
1087
+ if (target.conversationId !== message.conversationId) {
1088
+ fail("replacement edit target belongs to a different conversation");
1089
+ }
1090
+ replacementEdges.set(message.id, target.id);
1091
+ }
1092
+ }
1093
+ }
1094
+ const completedReplacementNodes = new Set<string>();
1095
+ for (const start of replacementEdges.keys()) {
1096
+ if (completedReplacementNodes.has(start)) continue;
1097
+ const path: string[] = [];
1098
+ const positions = new Map<string, number>();
1099
+ let current: string | undefined = start;
1100
+ while (current !== undefined && !completedReplacementNodes.has(current)) {
1101
+ if (positions.has(current)) fail("replacement edit graph contains a cycle");
1102
+ positions.set(current, path.length);
1103
+ path.push(current);
1104
+ current = replacementEdges.get(current);
1105
+ }
1106
+ for (const id of path) completedReplacementNodes.add(id);
1107
+ }
1108
+ for (const reaction of reactions.values()) {
1109
+ let participant: Extract<BundleGraphFact, { readonly kind: "participant" }>
1110
+ | undefined;
1111
+ if (reaction.participantId !== null) {
1112
+ participant = participants.get(reaction.participantId);
1113
+ assertSameAccount(
1114
+ participant,
1115
+ reaction.accountId,
1116
+ "reaction participant",
1117
+ );
1118
+ }
1119
+ const target = resolveMessageTarget(
1120
+ reaction.accountId,
1121
+ reaction.messageId,
1122
+ reaction.messageProviderId,
1123
+ "reaction message target",
1124
+ );
1125
+ if (participant !== undefined && target !== undefined) {
1126
+ const conversation = conversations.get(target.conversationId);
1127
+ assertSameAccount(
1128
+ conversation,
1129
+ reaction.accountId,
1130
+ "reaction target conversation",
1131
+ );
1132
+ if (
1133
+ conversation.kind === "conversation"
1134
+ && conversation.participantsComplete === true
1135
+ && !conversationRosters.get(conversation.id)?.has(participant.id)
1136
+ ) fail("reaction participant is absent from the complete conversation roster");
1137
+ }
1138
+ }
1139
+ for (const tombstone of graphRecords(inventory, "tombstone").values()) {
1140
+ const providerTargets = providerInventories.get(tombstone.entityKind);
1141
+ if (providerTargets === undefined) fail("internal provider inventory is incomplete");
1142
+ const providerTarget = providerTargets.get(graphProviderCoordinate(
1143
+ tombstone.accountId,
1144
+ tombstone.entityProviderId,
1145
+ ));
1146
+ if (tombstone.entityId !== null) {
1147
+ const target = graphRecords(inventory, tombstone.entityKind).get(
1148
+ tombstone.entityId,
1149
+ );
1150
+ assertSameAccount(target, tombstone.accountId, "tombstone entity target");
1151
+ if (target.providerId !== tombstone.entityProviderId) {
1152
+ fail("tombstone entity provider coordinate does not match");
1153
+ }
1154
+ if (providerTarget !== undefined && target.id !== providerTarget.id) {
1155
+ fail("tombstone local and provider coordinates disagree");
1156
+ }
1157
+ }
1158
+ }
1159
+ }
1160
+
1161
+ function parseDescriptor(value: unknown): ParsedDescriptor {
1162
+ const descriptor = foreignRecord(value, "source descriptor");
1163
+ exactKeys(descriptor, ["source", "provider"], "source descriptor");
1164
+ const source = foreignRecord(descriptor.source, "source descriptor.source");
1165
+ exactKeys(source, ["id", "version"], "source descriptor.source");
1166
+ if (source.id !== LOCAL_MESSAGE_BUNDLE_V1_SOURCE_ID) {
1167
+ fail(`source descriptor.source.id must equal ${LOCAL_MESSAGE_BUNDLE_V1_SOURCE_ID}`);
1168
+ }
1169
+ const provider = foreignRecord(descriptor.provider, "source descriptor.provider");
1170
+ exactKeys(provider, ["id", "version"], "source descriptor.provider");
1171
+ if (provider.id !== LOCAL_MESSAGE_BUNDLE_V1_PROVIDER_ID) {
1172
+ fail(`source descriptor.provider.id must equal ${LOCAL_MESSAGE_BUNDLE_V1_PROVIDER_ID}`);
1173
+ }
1174
+ return Object.freeze({
1175
+ source: Object.freeze({
1176
+ id: LOCAL_MESSAGE_BUNDLE_V1_SOURCE_ID,
1177
+ version: version(source.version, "source descriptor.source.version"),
1178
+ }),
1179
+ provider: Object.freeze({
1180
+ id: LOCAL_MESSAGE_BUNDLE_V1_PROVIDER_ID,
1181
+ version: version(provider.version, "source descriptor.provider.version"),
1182
+ }),
1183
+ });
1184
+ }
1185
+
1186
+ export function parseBeeperMessageLikeMeDescriptor(
1187
+ value: unknown,
1188
+ ): ParsedDescriptor {
1189
+ return parseDescriptor(value);
1190
+ }
1191
+
1192
+ function parseCompletion(value: unknown): ParsedCompletion {
1193
+ const source = foreignRecord(value, "source completion");
1194
+ exactKeys(source, ["completeness", "warnings"], "source completion");
1195
+ const completeness = foreignRecord(source.completeness, "source completion.completeness");
1196
+ exactKeys(
1197
+ completeness,
1198
+ ["kind", "reason", "observedFrom", "observedThrough"],
1199
+ "source completion.completeness",
1200
+ );
1201
+ const observedFrom = nullableTimestamp(
1202
+ completeness.observedFrom,
1203
+ "source completion.completeness.observedFrom",
1204
+ );
1205
+ const observedThrough = nullableTimestamp(
1206
+ completeness.observedThrough,
1207
+ "source completion.completeness.observedThrough",
1208
+ );
1209
+ if (observedFrom !== null && observedThrough !== null && observedFrom > observedThrough) {
1210
+ fail("source completion observedFrom must not be after observedThrough");
1211
+ }
1212
+ const warnings = array(source.warnings, "source completion.warnings", MAX_WARNING_CODES)
1213
+ .map((warning, index) => token(warning, `source completion.warnings[${String(index)}]`));
1214
+ if (new Set(warnings).size !== warnings.length) fail("source completion.warnings must not contain duplicates");
1215
+ return Object.freeze({
1216
+ completeness: Object.freeze({
1217
+ kind: oneOf(completeness.kind, ["bounded-local", "truncated", "unknown"] as const, "source completion.completeness.kind"),
1218
+ reason: completeness.reason === null
1219
+ ? null
1220
+ : token(completeness.reason, "source completion.completeness.reason"),
1221
+ observedFrom,
1222
+ observedThrough,
1223
+ }),
1224
+ warnings: Object.freeze(warnings),
1225
+ });
1226
+ }
1227
+
1228
+ export function parseBeeperMessageLikeMeCompletion(
1229
+ value: unknown,
1230
+ ): ParsedCompletion {
1231
+ return parseCompletion(value);
1232
+ }
1233
+
1234
+ function parseLimits(value: unknown): BeeperMessageLikeMeExportLimits {
1235
+ if (value === undefined) return HARD_LIMITS;
1236
+ const source = foreignRecord(value, "export limits");
1237
+ const permitted = ["maxRecords", "maxRecordBytes", "maxTotalBytes"];
1238
+ if (Object.keys(source).some((key) => !permitted.includes(key))) {
1239
+ fail(`export limits may contain only: ${permitted.join(", ")}`);
1240
+ }
1241
+ const parse = (key: keyof BeeperMessageLikeMeExportLimits): number => {
1242
+ const candidate = source[key] ?? HARD_LIMITS[key];
1243
+ const parsed = integer(candidate, `export limits.${key}`, HARD_LIMITS[key]);
1244
+ if (parsed === 0) fail(`export limits.${key} must be greater than zero`);
1245
+ return parsed;
1246
+ };
1247
+ return Object.freeze({
1248
+ maxRecords: parse("maxRecords"),
1249
+ maxRecordBytes: parse("maxRecordBytes"),
1250
+ maxTotalBytes: parse("maxTotalBytes"),
1251
+ });
1252
+ }
1253
+
1254
+ function now(clock: (() => Date) | undefined, label: string): string {
1255
+ const value = (clock ?? (() => new Date()))();
1256
+ if (!(value instanceof Date) || !Number.isFinite(value.getTime())) {
1257
+ return fail(`${label} clock value must be a valid Date`);
1258
+ }
1259
+ return value.toISOString();
1260
+ }
1261
+
1262
+ async function assertAbsent(path: string, label: string): Promise<void> {
1263
+ try {
1264
+ await lstat(path);
1265
+ } catch (error) {
1266
+ if (isErrno(error, "ENOENT")) return;
1267
+ throw error;
1268
+ }
1269
+ fail(`${label} already exists`);
1270
+ }
1271
+
1272
+ function nativeExclusiveRename(): NativeExclusiveRename {
1273
+ if (cachedNativeExclusiveRename !== undefined) {
1274
+ return cachedNativeExclusiveRename;
1275
+ }
1276
+ if (process.platform === "darwin") {
1277
+ const library = dlopen("/usr/lib/libSystem.B.dylib", {
1278
+ renamex_np: {
1279
+ args: ["cstring", "cstring", "u32"],
1280
+ returns: "int",
1281
+ },
1282
+ } as const);
1283
+ cachedNativeExclusiveRename = (source, destination) =>
1284
+ library.symbols.renamex_np(
1285
+ ptr(source),
1286
+ ptr(destination),
1287
+ DARWIN_RENAME_EXCL,
1288
+ );
1289
+ return cachedNativeExclusiveRename;
1290
+ }
1291
+ if (process.platform === "linux") {
1292
+ const library = dlopen("libc.so.6", {
1293
+ renameat2: {
1294
+ args: ["int", "cstring", "int", "cstring", "u32"],
1295
+ returns: "int",
1296
+ },
1297
+ } as const);
1298
+ cachedNativeExclusiveRename = (source, destination) =>
1299
+ library.symbols.renameat2(
1300
+ AT_FDCWD,
1301
+ ptr(source),
1302
+ AT_FDCWD,
1303
+ ptr(destination),
1304
+ LINUX_RENAME_NOREPLACE,
1305
+ );
1306
+ return cachedNativeExclusiveRename;
1307
+ }
1308
+ return fail("atomic no-clobber publication is unsupported on this platform");
1309
+ }
1310
+
1311
+ async function renameDirectoryExclusive(
1312
+ source: string,
1313
+ destination: string,
1314
+ ): Promise<void> {
1315
+ const encodePath = (path: string): Buffer => {
1316
+ if (path.includes("\0")) return fail("atomic publication path was invalid");
1317
+ return Buffer.from(`${path}\0`, "utf8");
1318
+ };
1319
+ const result = nativeExclusiveRename()(
1320
+ encodePath(source),
1321
+ encodePath(destination),
1322
+ );
1323
+ if (result === 0) return;
1324
+ let destinationExists = false;
1325
+ try {
1326
+ await lstat(destination);
1327
+ destinationExists = true;
1328
+ } catch (error) {
1329
+ if (!isErrno(error, "ENOENT")) throw error;
1330
+ }
1331
+ if (destinationExists) {
1332
+ return fail("outputRoot appeared before atomic publication");
1333
+ }
1334
+ return fail("atomic no-clobber publication failed");
1335
+ }
1336
+
1337
+ async function validateOutputRoot(outputRoot: unknown): Promise<{
1338
+ readonly outputRoot: string;
1339
+ readonly parent: string;
1340
+ readonly parentDevice: number;
1341
+ readonly parentInode: number;
1342
+ }> {
1343
+ if (typeof outputRoot !== "string" || !isAbsolute(outputRoot) || resolve(outputRoot) !== outputRoot) {
1344
+ return fail("outputRoot must be a normalized absolute path");
1345
+ }
1346
+ if (outputRoot === sep || utf8Length(outputRoot) > 4_096 || basename(outputRoot).includes("\0")) {
1347
+ return fail("outputRoot is unsafe");
1348
+ }
1349
+ const parent = dirname(outputRoot);
1350
+ const parentMetadata = await lstat(parent);
1351
+ if (!parentMetadata.isDirectory() || parentMetadata.isSymbolicLink()) {
1352
+ return fail("outputRoot parent must be a real directory");
1353
+ }
1354
+ if (await realpath(parent) !== parent) {
1355
+ return fail("outputRoot parent path must not traverse a symbolic link");
1356
+ }
1357
+ const uid = process.getuid?.();
1358
+ if (uid === undefined) fail("private exports require a POSIX user identity");
1359
+ if (parentMetadata.uid !== uid) fail("outputRoot parent must be owned by the current user");
1360
+ if ((parentMetadata.mode & 0o022) !== 0) {
1361
+ fail("outputRoot parent must not be writable by the group or other users");
1362
+ }
1363
+ await assertAbsent(outputRoot, "outputRoot");
1364
+ return {
1365
+ outputRoot,
1366
+ parent,
1367
+ parentDevice: parentMetadata.dev,
1368
+ parentInode: parentMetadata.ino,
1369
+ };
1370
+ }
1371
+
1372
+ async function assertParentUnchanged(snapshot: Awaited<ReturnType<typeof validateOutputRoot>>): Promise<void> {
1373
+ const current = await lstat(snapshot.parent);
1374
+ const uid = process.getuid?.();
1375
+ if (
1376
+ !current.isDirectory()
1377
+ || current.isSymbolicLink()
1378
+ || uid === undefined
1379
+ || current.uid !== uid
1380
+ || (current.mode & 0o022) !== 0
1381
+ || current.dev !== snapshot.parentDevice
1382
+ || current.ino !== snapshot.parentInode
1383
+ || await realpath(snapshot.parent) !== snapshot.parent
1384
+ ) {
1385
+ fail("outputRoot parent changed during export setup");
1386
+ }
1387
+ }
1388
+
1389
+ async function assertPrivateDirectory(
1390
+ path: string,
1391
+ expected?: PrivateDirectoryIdentity,
1392
+ ): Promise<PrivateDirectoryIdentity> {
1393
+ const uid = process.getuid?.();
1394
+ const metadata = await lstat(path);
1395
+ if (
1396
+ !metadata.isDirectory()
1397
+ || metadata.isSymbolicLink()
1398
+ || uid === undefined
1399
+ || metadata.uid !== uid
1400
+ || (metadata.mode & 0o777) !== PRIVATE_DIRECTORY_MODE
1401
+ || (expected !== undefined
1402
+ && (metadata.dev !== expected.device || metadata.ino !== expected.inode))
1403
+ || await realpath(path) !== path
1404
+ ) {
1405
+ fail("private staging directory changed");
1406
+ }
1407
+ return Object.freeze({ device: metadata.dev, inode: metadata.ino });
1408
+ }
1409
+
1410
+ async function assertPrivateFile(path: string, expectedBytes: number): Promise<void> {
1411
+ const uid = process.getuid?.();
1412
+ const metadata = await lstat(path);
1413
+ if (
1414
+ !metadata.isFile()
1415
+ || metadata.isSymbolicLink()
1416
+ || uid === undefined
1417
+ || metadata.uid !== uid
1418
+ || metadata.nlink !== 1
1419
+ || (metadata.mode & 0o777) !== PRIVATE_FILE_MODE
1420
+ || metadata.size !== expectedBytes
1421
+ ) {
1422
+ fail("private staged artifact changed");
1423
+ }
1424
+ }
1425
+
1426
+ async function createPrivateStagingDirectory(
1427
+ output: Awaited<ReturnType<typeof validateOutputRoot>>,
1428
+ ): Promise<{ readonly path: string; readonly identity: PrivateDirectoryIdentity }> {
1429
+ for (let attempt = 0; attempt < 8; attempt += 1) {
1430
+ await assertParentUnchanged(output);
1431
+ const candidate = await mkdtemp(resolve(output.parent, ".message-like-me-staging-"));
1432
+ let identity: PrivateDirectoryIdentity | undefined;
1433
+ try {
1434
+ await chmod(candidate, PRIVATE_DIRECTORY_MODE);
1435
+ identity = await assertPrivateDirectory(candidate);
1436
+ const metadata = await lstat(candidate);
1437
+ if (dirname(candidate) !== output.parent || metadata.dev !== output.parentDevice) {
1438
+ fail("private staging directory must share the output parent and filesystem");
1439
+ }
1440
+ if (candidate === output.outputRoot) {
1441
+ await removeOwnedPrivateDirectory(candidate, identity);
1442
+ continue;
1443
+ }
1444
+ return Object.freeze({ path: candidate, identity });
1445
+ } catch (error) {
1446
+ if (identity === undefined) {
1447
+ try {
1448
+ await rmdir(candidate);
1449
+ } catch {
1450
+ // Never recursively remove a directory whose identity was not captured.
1451
+ }
1452
+ } else {
1453
+ await removeOwnedPrivateDirectory(candidate, identity);
1454
+ }
1455
+ throw error;
1456
+ }
1457
+ }
1458
+ return fail("could not allocate a private staging directory distinct from outputRoot");
1459
+ }
1460
+
1461
+ async function removeOwnedPrivateDirectory(
1462
+ path: string,
1463
+ identity: PrivateDirectoryIdentity,
1464
+ ): Promise<void> {
1465
+ try {
1466
+ removePrivateDirectoryTree(path, Object.freeze({
1467
+ device: String(identity.device),
1468
+ inode: String(identity.inode),
1469
+ }));
1470
+ } catch {
1471
+ return fail("private directory could not be removed from quarantine safely");
1472
+ }
1473
+ }
1474
+
1475
+ async function syncDirectory(
1476
+ path: string,
1477
+ expected: PrivateDirectoryIdentity,
1478
+ ): Promise<void> {
1479
+ const handle = await open(
1480
+ path,
1481
+ constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
1482
+ );
1483
+ try {
1484
+ const metadata = await handle.stat();
1485
+ if (
1486
+ !metadata.isDirectory()
1487
+ || metadata.dev !== expected.device
1488
+ || metadata.ino !== expected.inode
1489
+ ) {
1490
+ fail("directory changed before synchronization");
1491
+ }
1492
+ await handle.sync();
1493
+ } finally {
1494
+ await handle.close();
1495
+ }
1496
+ }
1497
+
1498
+ async function privateFileSha256(path: string, expectedBytes: number): Promise<string> {
1499
+ const handle = await open(
1500
+ path,
1501
+ constants.O_RDONLY | constants.O_NOFOLLOW,
1502
+ );
1503
+ try {
1504
+ const uid = process.getuid?.();
1505
+ const before = await handle.stat();
1506
+ const entryBefore = await lstat(path);
1507
+ if (
1508
+ !before.isFile()
1509
+ || uid === undefined
1510
+ || before.uid !== uid
1511
+ || before.nlink !== 1
1512
+ || (before.mode & 0o777) !== PRIVATE_FILE_MODE
1513
+ || before.size !== expectedBytes
1514
+ || !entryBefore.isFile()
1515
+ || entryBefore.isSymbolicLink()
1516
+ || entryBefore.dev !== before.dev
1517
+ || entryBefore.ino !== before.ino
1518
+ || entryBefore.nlink !== 1
1519
+ ) {
1520
+ fail("private staged artifact changed before bundle validation");
1521
+ }
1522
+ const hash = createHash("sha256");
1523
+ const buffer = Buffer.allocUnsafe(64 * 1024);
1524
+ let offset = 0;
1525
+ while (offset < expectedBytes) {
1526
+ const length = Math.min(buffer.byteLength, expectedBytes - offset);
1527
+ const { bytesRead } = await handle.read(buffer, 0, length, offset);
1528
+ if (bytesRead === 0) fail("private staged artifact ended during bundle validation");
1529
+ hash.update(buffer.subarray(0, bytesRead));
1530
+ offset += bytesRead;
1531
+ }
1532
+ const after = await handle.stat();
1533
+ const entryAfter = await lstat(path);
1534
+ if (
1535
+ after.dev !== before.dev
1536
+ || after.ino !== before.ino
1537
+ || !after.isFile()
1538
+ || after.uid !== before.uid
1539
+ || after.nlink !== 1
1540
+ || after.size !== before.size
1541
+ || after.mtimeMs !== before.mtimeMs
1542
+ || after.ctimeMs !== before.ctimeMs
1543
+ || (after.mode & 0o777) !== PRIVATE_FILE_MODE
1544
+ || !entryAfter.isFile()
1545
+ || entryAfter.isSymbolicLink()
1546
+ || entryAfter.dev !== after.dev
1547
+ || entryAfter.ino !== after.ino
1548
+ || entryAfter.nlink !== 1
1549
+ ) {
1550
+ fail("private staged artifact changed during bundle validation");
1551
+ }
1552
+ return hash.digest("hex");
1553
+ } finally {
1554
+ await handle.close();
1555
+ }
1556
+ }
1557
+
1558
+ async function createWriter(staging: string, kind: RecordKind, fileName: string): Promise<ArtifactWriter> {
1559
+ const partPath = resolve(staging, `${fileName}.part`);
1560
+ if (!partPath.startsWith(`${staging}${sep}`)) fail("internal artifact path escaped staging");
1561
+ const handle = await open(
1562
+ partPath,
1563
+ constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW,
1564
+ PRIVATE_FILE_MODE,
1565
+ );
1566
+ try {
1567
+ await handle.chmod(PRIVATE_FILE_MODE);
1568
+ const uid = process.getuid?.();
1569
+ const metadata = await handle.stat();
1570
+ if (
1571
+ !metadata.isFile()
1572
+ || uid === undefined
1573
+ || metadata.uid !== uid
1574
+ || metadata.nlink !== 1
1575
+ || (metadata.mode & 0o777) !== PRIVATE_FILE_MODE
1576
+ ) {
1577
+ fail(`could not create private staging file ${fileName}`);
1578
+ }
1579
+ } catch (error) {
1580
+ await handle.close();
1581
+ throw error;
1582
+ }
1583
+ return {
1584
+ kind,
1585
+ fileName,
1586
+ partPath,
1587
+ handle,
1588
+ hash: createHash("sha256"),
1589
+ records: 0,
1590
+ bytes: 0,
1591
+ closed: false,
1592
+ };
1593
+ }
1594
+
1595
+ async function writeAll(handle: FileHandle, bytes: Uint8Array): Promise<void> {
1596
+ let offset = 0;
1597
+ while (offset < bytes.byteLength) {
1598
+ const result = await handle.write(bytes, offset, bytes.byteLength - offset, null);
1599
+ if (result.bytesWritten === 0) fail("staging file stopped accepting bytes");
1600
+ offset += result.bytesWritten;
1601
+ }
1602
+ }
1603
+
1604
+ async function writeRecord(
1605
+ writer: ArtifactWriter,
1606
+ value: Readonly<Record<string, unknown>>,
1607
+ limits: BeeperMessageLikeMeExportLimits,
1608
+ totalBytes: number,
1609
+ ): Promise<number> {
1610
+ const bytes = Buffer.from(`${canonicalJson(value)}\n`, "utf8");
1611
+ if (bytes.byteLength > limits.maxRecordBytes) {
1612
+ fail(`one ${writer.kind} record exceeds the configured byte bound`);
1613
+ }
1614
+ if (totalBytes + bytes.byteLength > limits.maxTotalBytes) {
1615
+ fail("bundle records exceed the configured total byte bound");
1616
+ }
1617
+ await writeAll(writer.handle, bytes);
1618
+ writer.hash.update(bytes);
1619
+ writer.records += 1;
1620
+ writer.bytes += bytes.byteLength;
1621
+ return totalBytes + bytes.byteLength;
1622
+ }
1623
+
1624
+ async function closeWriter(writer: ArtifactWriter): Promise<void> {
1625
+ if (writer.closed) return;
1626
+ writer.closed = true;
1627
+ await writer.handle.close();
1628
+ }
1629
+
1630
+ async function finalizeWriter(writer: ArtifactWriter, staging: string): Promise<BeeperMessageLikeMeArtifact> {
1631
+ await writer.handle.sync();
1632
+ const uid = process.getuid?.();
1633
+ const opened = await writer.handle.stat();
1634
+ if (
1635
+ !opened.isFile()
1636
+ || uid === undefined
1637
+ || opened.uid !== uid
1638
+ || opened.nlink !== 1
1639
+ || opened.size !== writer.bytes
1640
+ || (opened.mode & 0o777) !== PRIVATE_FILE_MODE
1641
+ ) {
1642
+ fail(`${writer.fileName} changed before finalization`);
1643
+ }
1644
+ await closeWriter(writer);
1645
+ await assertPrivateFile(writer.partPath, writer.bytes);
1646
+ const finalPath = resolve(staging, writer.fileName);
1647
+ await assertAbsent(finalPath, writer.fileName);
1648
+ await rename(writer.partPath, finalPath);
1649
+ await assertPrivateFile(finalPath, writer.bytes);
1650
+ return Object.freeze({
1651
+ path: writer.fileName,
1652
+ mediaType: "application/x-ndjson",
1653
+ recordKind: writer.kind,
1654
+ records: writer.records,
1655
+ bytes: writer.bytes,
1656
+ sha256: writer.hash.digest("hex"),
1657
+ });
1658
+ }
1659
+
1660
+ async function writeManifest(
1661
+ staging: string,
1662
+ manifest: BeeperMessageLikeMeManifest,
1663
+ ): Promise<StagedManifest> {
1664
+ const partPath = resolve(staging, "manifest.json.part");
1665
+ const finalPath = resolve(staging, "manifest.json");
1666
+ const bytes = Buffer.from(`${canonicalJson(manifest)}\n`, "utf8");
1667
+ const handle = await open(
1668
+ partPath,
1669
+ constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW,
1670
+ PRIVATE_FILE_MODE,
1671
+ );
1672
+ try {
1673
+ await handle.chmod(PRIVATE_FILE_MODE);
1674
+ await writeAll(handle, bytes);
1675
+ await handle.sync();
1676
+ const uid = process.getuid?.();
1677
+ const metadata = await handle.stat();
1678
+ if (
1679
+ !metadata.isFile()
1680
+ || uid === undefined
1681
+ || metadata.uid !== uid
1682
+ || metadata.nlink !== 1
1683
+ || metadata.size !== bytes.byteLength
1684
+ || (metadata.mode & 0o777) !== PRIVATE_FILE_MODE
1685
+ ) {
1686
+ fail("manifest changed before finalization");
1687
+ }
1688
+ } finally {
1689
+ await handle.close();
1690
+ }
1691
+ await assertPrivateFile(partPath, bytes.byteLength);
1692
+ await assertAbsent(finalPath, "manifest.json");
1693
+ await rename(partPath, finalPath);
1694
+ await assertPrivateFile(finalPath, bytes.byteLength);
1695
+ return Object.freeze({
1696
+ bytes: bytes.byteLength,
1697
+ sha256: sha256(bytes.toString("utf8")),
1698
+ });
1699
+ }
1700
+
1701
+ async function validateCompleteBundle(
1702
+ root: string,
1703
+ identity: PrivateDirectoryIdentity,
1704
+ artifacts: readonly BeeperMessageLikeMeArtifact[],
1705
+ manifest: StagedManifest,
1706
+ ): Promise<void> {
1707
+ await assertPrivateDirectory(root, identity);
1708
+ const expectedNames = [
1709
+ ...artifacts.map((artifact) => artifact.path),
1710
+ "manifest.json",
1711
+ ].sort();
1712
+ const observedNames = (await readdir(root)).sort();
1713
+ if (
1714
+ observedNames.length !== expectedNames.length
1715
+ || observedNames.some((name, index) => name !== expectedNames[index])
1716
+ ) {
1717
+ fail("private staging directory does not contain the exact complete bundle");
1718
+ }
1719
+ for (const artifact of artifacts) {
1720
+ const path = resolve(root, artifact.path);
1721
+ await assertPrivateFile(path, artifact.bytes);
1722
+ if (await privateFileSha256(path, artifact.bytes) !== artifact.sha256) {
1723
+ fail(`${artifact.path} changed before publication`);
1724
+ }
1725
+ }
1726
+ const manifestPath = resolve(root, "manifest.json");
1727
+ await assertPrivateFile(manifestPath, manifest.bytes);
1728
+ if (await privateFileSha256(manifestPath, manifest.bytes) !== manifest.sha256) {
1729
+ fail("manifest.json changed before publication");
1730
+ }
1731
+ }
1732
+
1733
+ function assertSource(source: BeeperMessageLikeMeExportSource): void {
1734
+ if (
1735
+ typeof source !== "object"
1736
+ || source === null
1737
+ || typeof source.completion !== "function"
1738
+ || typeof source.records !== "object"
1739
+ || source.records === null
1740
+ || typeof source.records[Symbol.asyncIterator] !== "function"
1741
+ || (source.dispose !== undefined && typeof source.dispose !== "function")
1742
+ ) {
1743
+ fail("source must expose an async record stream, completion function, and optional dispose function");
1744
+ }
1745
+ }
1746
+
1747
+ function throwIfAborted(signal: AbortSignal | undefined): void {
1748
+ if (signal?.aborted === true) fail("export was aborted");
1749
+ }
1750
+
1751
+ type BundlePhaseHeartbeat = Readonly<{
1752
+ stop: () => void;
1753
+ assertHealthy: () => void;
1754
+ }>;
1755
+
1756
+ function startBundlePhaseHeartbeat(
1757
+ request: BeeperMessageLikeMeExportRequest,
1758
+ phase: "bundle-validating" | "bundle-publishing",
1759
+ records: number,
1760
+ bytes: number,
1761
+ ): BundlePhaseHeartbeat {
1762
+ const startedAt = Date.now();
1763
+ let failed = false;
1764
+ const report = (elapsedSeconds: number): void => {
1765
+ request.onProgress?.(Object.freeze({
1766
+ phase,
1767
+ elapsedSeconds,
1768
+ records,
1769
+ bytes,
1770
+ }));
1771
+ };
1772
+ report(0);
1773
+ const heartbeat = request.onProgress === undefined
1774
+ ? null
1775
+ : setInterval(() => {
1776
+ try {
1777
+ report(Math.max(1, Math.floor((Date.now() - startedAt) / 1_000)));
1778
+ } catch {
1779
+ failed = true;
1780
+ }
1781
+ }, BUNDLE_HEARTBEAT_INTERVAL_MS);
1782
+ return Object.freeze({
1783
+ stop: () => {
1784
+ if (heartbeat !== null) clearInterval(heartbeat);
1785
+ },
1786
+ assertHealthy: () => {
1787
+ if (failed) fail("export progress reporting failed");
1788
+ },
1789
+ });
1790
+ }
1791
+
1792
+ function startPrivateCleanupHeartbeat(
1793
+ request: BeeperMessageLikeMeExportRequest,
1794
+ ): BundlePhaseHeartbeat {
1795
+ const startedAt = Date.now();
1796
+ let failed = false;
1797
+ const report = (elapsedSeconds: number): void => {
1798
+ request.onProgress?.(Object.freeze({
1799
+ phase: "private-cleanup",
1800
+ elapsedSeconds,
1801
+ }));
1802
+ };
1803
+ try {
1804
+ report(0);
1805
+ } catch {
1806
+ failed = true;
1807
+ }
1808
+ const heartbeat = request.onProgress === undefined
1809
+ ? null
1810
+ : setInterval(() => {
1811
+ try {
1812
+ report(Math.max(1, Math.floor((Date.now() - startedAt) / 1_000)));
1813
+ } catch {
1814
+ failed = true;
1815
+ }
1816
+ }, BUNDLE_HEARTBEAT_INTERVAL_MS);
1817
+ return Object.freeze({
1818
+ stop: () => {
1819
+ if (heartbeat !== null) clearInterval(heartbeat);
1820
+ },
1821
+ assertHealthy: () => {
1822
+ if (failed) fail("export progress reporting failed");
1823
+ },
1824
+ });
1825
+ }
1826
+
1827
+ /**
1828
+ * Writes a private, local Message Like Me interchange bundle. The source owns
1829
+ * provider access; this function accepts only bounded foreign records and
1830
+ * never invokes Beeper, follows media references, or receives credentials.
1831
+ *
1832
+ * The complete bundle is built and validated in a mode-0700 sibling directory
1833
+ * on the destination filesystem. `outputRoot` remains absent until one final
1834
+ * atomic rename publishes all seven files together. Failure and cancellation
1835
+ * remove the owned staging directory without publishing a partial bundle.
1836
+ */
1837
+ async function publishBeeperMessageLikeMeBundle(
1838
+ request: BeeperMessageLikeMeExportRequest,
1839
+ ): Promise<PublishedBundle> {
1840
+ let published = false;
1841
+ const descriptor = parseDescriptor(request.source.descriptor);
1842
+ const limits = parseLimits(request.limits);
1843
+ throwIfAborted(request.signal);
1844
+ const startedAt = now(request.clock, "startedAt");
1845
+ const output = await validateOutputRoot(request.outputRoot);
1846
+ const stagedDirectory = await createPrivateStagingDirectory(output);
1847
+ const staging = stagedDirectory.path;
1848
+ let directoryLease: BeeperMessageLikeMeDirectoryLease | undefined;
1849
+
1850
+ if (request.recoveryEnvironment !== undefined) {
1851
+ try {
1852
+ const leaseCreatedAtMs = Date.now();
1853
+ directoryLease = await createBeeperMessageLikeMeDirectoryLease({
1854
+ role: "bundle-stage",
1855
+ path: staging,
1856
+ outputRoot: output.outputRoot,
1857
+ recoverAfterMs: leaseCreatedAtMs,
1858
+ nowMs: leaseCreatedAtMs,
1859
+ environment: request.recoveryEnvironment,
1860
+ });
1861
+ } catch (leaseError) {
1862
+ const cleanupErrors: unknown[] = [];
1863
+ try {
1864
+ await removeOwnedPrivateDirectory(staging, stagedDirectory.identity);
1865
+ } catch (error) {
1866
+ cleanupErrors.push(error);
1867
+ }
1868
+ try {
1869
+ await syncDirectory(output.parent, Object.freeze({
1870
+ device: output.parentDevice,
1871
+ inode: output.parentInode,
1872
+ }));
1873
+ } catch (error) {
1874
+ cleanupErrors.push(error);
1875
+ }
1876
+ if (cleanupErrors.length > 0) {
1877
+ throw new AggregateError(
1878
+ [leaseError, ...cleanupErrors],
1879
+ "Beeper Message Like Me export: recovery setup and cleanup both failed",
1880
+ );
1881
+ }
1882
+ throw leaseError;
1883
+ }
1884
+ }
1885
+
1886
+ const writers = new Map<RecordKind, ArtifactWriter>();
1887
+ let bundlePhaseHeartbeat: BundlePhaseHeartbeat | undefined;
1888
+ let renamed = false;
1889
+ let operationFailed = false;
1890
+ let operationError: unknown;
1891
+ try {
1892
+ for (const artifact of ARTIFACTS) {
1893
+ writers.set(artifact.kind, await createWriter(staging, artifact.kind, artifact.fileName));
1894
+ }
1895
+
1896
+ const graphInventory = new Map<RecordKind, Map<string, BundleGraphFact>>(
1897
+ ARTIFACTS.map((artifact) => [artifact.kind, new Map<string, BundleGraphFact>()]),
1898
+ );
1899
+ const providerCoordinates = new Map<RecordKind, Set<string>>(
1900
+ ARTIFACTS.map((artifact) => [artifact.kind, new Set<string>()]),
1901
+ );
1902
+ const accountRealms = new Map<string, { readonly network: string; readonly connectedAccountProviderId: string }>();
1903
+ const referencedAccountRealms = new Map<string, { readonly network: string; readonly connectedAccountProviderId: string }>();
1904
+ let totalRecords = 0;
1905
+ let totalBytes = 0;
1906
+ const bundleStartedAt = Date.now();
1907
+ let lastBundleProgressAt = bundleStartedAt;
1908
+
1909
+ for await (const candidate of request.source.records) {
1910
+ throwIfAborted(request.signal);
1911
+ totalRecords += 1;
1912
+ if (totalRecords > limits.maxRecords) fail("record stream exceeds the configured record bound");
1913
+ const parsed = parseRecord(candidate, totalRecords - 1);
1914
+ const records = graphInventory.get(parsed.kind);
1915
+ const coordinates = providerCoordinates.get(parsed.kind);
1916
+ if (records === undefined || coordinates === undefined) {
1917
+ fail("internal record-kind inventory is incomplete");
1918
+ }
1919
+ if (records.has(parsed.id)) {
1920
+ fail(`${parsed.kind} record repeats a bundle-local identity`);
1921
+ }
1922
+ const providerCoordinate = graphProviderCoordinate(
1923
+ parsed.value.accountId,
1924
+ parsed.value.provenance.providerId,
1925
+ );
1926
+ if (coordinates.has(providerCoordinate)) {
1927
+ fail(`${parsed.kind} record repeats an account-scoped provider identity`);
1928
+ }
1929
+ records.set(parsed.id, bundleGraphFact(parsed.value));
1930
+ coordinates.add(providerCoordinate);
1931
+ if (parsed.kind === "account" && records.size > MAX_CONNECTED_ACCOUNTS) {
1932
+ fail("record stream exceeds the connected-account bound");
1933
+ }
1934
+
1935
+ if (
1936
+ parsed.accountId !== undefined
1937
+ && parsed.network !== undefined
1938
+ && parsed.connectedAccountProviderId !== undefined
1939
+ ) {
1940
+ const realm = Object.freeze({
1941
+ network: parsed.network,
1942
+ connectedAccountProviderId: parsed.connectedAccountProviderId,
1943
+ });
1944
+ if (parsed.kind === "account") {
1945
+ const current = accountRealms.get(parsed.accountId);
1946
+ if (
1947
+ current !== undefined
1948
+ && (
1949
+ current.network !== realm.network
1950
+ || current.connectedAccountProviderId !== realm.connectedAccountProviderId
1951
+ )
1952
+ ) {
1953
+ fail("account record has conflicting source identity");
1954
+ }
1955
+ accountRealms.set(parsed.accountId, realm);
1956
+ } else {
1957
+ const current = referencedAccountRealms.get(parsed.accountId);
1958
+ if (
1959
+ current !== undefined
1960
+ && (
1961
+ current.network !== realm.network
1962
+ || current.connectedAccountProviderId !== realm.connectedAccountProviderId
1963
+ )
1964
+ ) {
1965
+ fail("account is referenced with conflicting source identity");
1966
+ }
1967
+ referencedAccountRealms.set(parsed.accountId, realm);
1968
+ }
1969
+ }
1970
+
1971
+ const writer = writers.get(parsed.kind);
1972
+ if (writer === undefined) fail("internal artifact writer is missing");
1973
+ totalBytes = await writeRecord(writer, parsed.value, limits, totalBytes);
1974
+ const progressAt = Date.now();
1975
+ if (
1976
+ totalRecords === 1
1977
+ || progressAt - lastBundleProgressAt >= 30_000
1978
+ ) {
1979
+ request.onProgress?.(Object.freeze({
1980
+ phase: "bundle-building",
1981
+ elapsedSeconds: Math.max(
1982
+ 0,
1983
+ Math.floor((progressAt - bundleStartedAt) / 1_000),
1984
+ ),
1985
+ records: totalRecords,
1986
+ bytes: totalBytes,
1987
+ }));
1988
+ lastBundleProgressAt = progressAt;
1989
+ }
1990
+ }
1991
+
1992
+ for (const [accountId, realm] of referencedAccountRealms) {
1993
+ const accountRealm = accountRealms.get(accountId);
1994
+ if (accountRealm === undefined) {
1995
+ fail("record stream references a missing account");
1996
+ }
1997
+ if (
1998
+ accountRealm.network !== realm.network
1999
+ || accountRealm.connectedAccountProviderId !== realm.connectedAccountProviderId
2000
+ ) {
2001
+ fail("account does not match its referenced source identity");
2002
+ }
2003
+ }
2004
+
2005
+ bundlePhaseHeartbeat = startBundlePhaseHeartbeat(
2006
+ request,
2007
+ "bundle-validating",
2008
+ totalRecords,
2009
+ totalBytes,
2010
+ );
2011
+ validateBundleGraph(graphInventory);
2012
+
2013
+ throwIfAborted(request.signal);
2014
+ const completion = parseCompletion(await request.source.completion());
2015
+ const finishedAt = now(request.clock, "finishedAt");
2016
+ if (finishedAt < startedAt) fail("finishedAt must not be before startedAt");
2017
+
2018
+ const artifacts: BeeperMessageLikeMeArtifact[] = [];
2019
+ for (const artifact of ARTIFACTS) {
2020
+ const writer = writers.get(artifact.kind);
2021
+ if (writer === undefined) fail("internal artifact writer is missing during finalization");
2022
+ artifacts.push(await finalizeWriter(writer, staging));
2023
+ }
2024
+
2025
+ const counts = Object.freeze(Object.fromEntries(
2026
+ artifacts.map((artifact) => [artifact.recordKind, artifact.records]),
2027
+ )) as Readonly<Record<RecordKind, number>>;
2028
+ const manifestProjection = Object.freeze({
2029
+ schemaVersion: BEEPER_MESSAGE_LIKE_ME_SCHEMA_VERSION,
2030
+ format: LOCAL_MESSAGE_BUNDLE_V1_FORMAT,
2031
+ source: descriptor.source,
2032
+ provider: descriptor.provider,
2033
+ timestamps: Object.freeze({
2034
+ startedAt,
2035
+ finishedAt,
2036
+ createdAt: finishedAt,
2037
+ }),
2038
+ completeness: completion.completeness,
2039
+ warnings: completion.warnings,
2040
+ privacy: Object.freeze({
2041
+ classification: "private-local",
2042
+ attachments: "metadata-only",
2043
+ providerUrls: "excluded",
2044
+ credentials: "excluded",
2045
+ }),
2046
+ counts,
2047
+ artifacts: Object.freeze(artifacts),
2048
+ });
2049
+ const manifest: BeeperMessageLikeMeManifest = Object.freeze({
2050
+ ...manifestProjection,
2051
+ integrity: Object.freeze({
2052
+ algorithm: "sha256",
2053
+ bundleSha256: sha256(canonicalJson(manifestProjection)),
2054
+ }),
2055
+ });
2056
+ if (manifest.source.version === LOCAL_MESSAGE_BUNDLE_V1_SOURCE_TRANSFORM_VERSION) {
2057
+ try {
2058
+ parseLocalMessageBundleV1Manifest(manifest);
2059
+ } catch {
2060
+ fail("constructed manifest violates the Message Like Me bundle v1 contract");
2061
+ }
2062
+ }
2063
+ const stagedManifest = await writeManifest(staging, manifest);
2064
+ await validateCompleteBundle(
2065
+ staging,
2066
+ stagedDirectory.identity,
2067
+ artifacts,
2068
+ stagedManifest,
2069
+ );
2070
+ await syncDirectory(staging, stagedDirectory.identity);
2071
+ throwIfAborted(request.signal);
2072
+ await assertParentUnchanged(output);
2073
+ bundlePhaseHeartbeat.stop();
2074
+ bundlePhaseHeartbeat.assertHealthy();
2075
+ bundlePhaseHeartbeat = startBundlePhaseHeartbeat(
2076
+ request,
2077
+ "bundle-publishing",
2078
+ totalRecords,
2079
+ totalBytes,
2080
+ );
2081
+
2082
+ const result = Object.freeze({
2083
+ outputRoot: output.outputRoot,
2084
+ manifestPath: resolve(output.outputRoot, "manifest.json"),
2085
+ manifestSha256: stagedManifest.sha256,
2086
+ manifest,
2087
+ });
2088
+
2089
+ await renameDirectoryExclusive(staging, output.outputRoot);
2090
+ renamed = true;
2091
+ await validateCompleteBundle(
2092
+ output.outputRoot,
2093
+ stagedDirectory.identity,
2094
+ artifacts,
2095
+ stagedManifest,
2096
+ );
2097
+ throwIfAborted(request.signal);
2098
+ await assertParentUnchanged(output);
2099
+ await syncDirectory(output.parent, Object.freeze({
2100
+ device: output.parentDevice,
2101
+ inode: output.parentInode,
2102
+ }));
2103
+ bundlePhaseHeartbeat.stop();
2104
+ bundlePhaseHeartbeat.assertHealthy();
2105
+ bundlePhaseHeartbeat = undefined;
2106
+ const publishedDirectory = Object.freeze({
2107
+ path: output.outputRoot,
2108
+ parent: output.parent,
2109
+ identity: stagedDirectory.identity,
2110
+ parentIdentity: Object.freeze({
2111
+ device: output.parentDevice,
2112
+ inode: output.parentInode,
2113
+ }),
2114
+ });
2115
+ published = true;
2116
+ return Object.freeze({
2117
+ result,
2118
+ directory: publishedDirectory,
2119
+ ...(directoryLease === undefined ? {} : { directoryLease }),
2120
+ });
2121
+ } catch (error) {
2122
+ operationFailed = true;
2123
+ operationError = error;
2124
+ throw error;
2125
+ } finally {
2126
+ const cleanupErrors: unknown[] = [];
2127
+ bundlePhaseHeartbeat?.stop();
2128
+ const closeResults = await Promise.allSettled(
2129
+ [...writers.values()].map((writer) => closeWriter(writer)),
2130
+ );
2131
+ for (const result of closeResults) {
2132
+ if (result.status === "rejected") cleanupErrors.push(result.reason);
2133
+ }
2134
+ if (!published) {
2135
+ let removalDurable = true;
2136
+ try {
2137
+ await removeOwnedPrivateDirectory(
2138
+ renamed ? output.outputRoot : staging,
2139
+ stagedDirectory.identity,
2140
+ );
2141
+ } catch (error) {
2142
+ removalDurable = false;
2143
+ cleanupErrors.push(error);
2144
+ }
2145
+ try {
2146
+ await syncDirectory(output.parent, Object.freeze({
2147
+ device: output.parentDevice,
2148
+ inode: output.parentInode,
2149
+ }));
2150
+ } catch (error) {
2151
+ removalDurable = false;
2152
+ cleanupErrors.push(error);
2153
+ }
2154
+ if (directoryLease !== undefined && removalDurable) {
2155
+ try {
2156
+ releaseBeeperMessageLikeMeDirectoryLease(directoryLease);
2157
+ } catch (error) {
2158
+ cleanupErrors.push(error);
2159
+ }
2160
+ }
2161
+ }
2162
+ if (cleanupErrors.length > 0) {
2163
+ if (operationFailed) {
2164
+ throw new AggregateError(
2165
+ [operationError, ...cleanupErrors],
2166
+ "Beeper Message Like Me export: publication and cleanup both failed",
2167
+ );
2168
+ }
2169
+ if (cleanupErrors.length === 1) throw cleanupErrors[0];
2170
+ throw new AggregateError(
2171
+ cleanupErrors,
2172
+ "Beeper Message Like Me export: cleanup failed",
2173
+ );
2174
+ }
2175
+ }
2176
+ }
2177
+
2178
+ export async function exportBeeperMessageLikeMeBundle(
2179
+ request: BeeperMessageLikeMeExportRequest,
2180
+ ): Promise<BeeperMessageLikeMeExportResult> {
2181
+ assertSource(request.source);
2182
+ let publication: PublishedBundle | undefined;
2183
+ let publicationFailed = false;
2184
+ let publicationError: unknown;
2185
+ try {
2186
+ publication = await publishBeeperMessageLikeMeBundle(request);
2187
+ return publication.result;
2188
+ } catch (error) {
2189
+ publicationFailed = true;
2190
+ publicationError = error;
2191
+ throw error;
2192
+ } finally {
2193
+ const finalizationErrors: unknown[] = [];
2194
+ const cleanupHeartbeat = request.source.dispose === undefined
2195
+ ? undefined
2196
+ : startPrivateCleanupHeartbeat(request);
2197
+ try {
2198
+ await request.source.dispose?.(publication !== undefined);
2199
+ } catch (error) {
2200
+ finalizationErrors.push(error);
2201
+ }
2202
+ let releaseAttempted = false;
2203
+ if (
2204
+ publication !== undefined
2205
+ && finalizationErrors.length === 0
2206
+ && publication.directoryLease !== undefined
2207
+ ) {
2208
+ releaseAttempted = true;
2209
+ try {
2210
+ releaseBeeperMessageLikeMeDirectoryLease(publication.directoryLease);
2211
+ } catch (error) {
2212
+ finalizationErrors.push(error);
2213
+ }
2214
+ }
2215
+ cleanupHeartbeat?.stop();
2216
+ try {
2217
+ cleanupHeartbeat?.assertHealthy();
2218
+ } catch (error) {
2219
+ finalizationErrors.push(error);
2220
+ }
2221
+ if (finalizationErrors.length > 0) {
2222
+ const errors = publicationFailed
2223
+ ? [publicationError, ...finalizationErrors]
2224
+ : [...finalizationErrors];
2225
+ if (publication !== undefined) {
2226
+ let rollbackDurable = true;
2227
+ try {
2228
+ await removeOwnedPrivateDirectory(
2229
+ publication.directory.path,
2230
+ publication.directory.identity,
2231
+ );
2232
+ } catch (error) {
2233
+ rollbackDurable = false;
2234
+ errors.push(error);
2235
+ }
2236
+ try {
2237
+ await syncDirectory(
2238
+ publication.directory.parent,
2239
+ publication.directory.parentIdentity,
2240
+ );
2241
+ } catch (error) {
2242
+ rollbackDurable = false;
2243
+ errors.push(error);
2244
+ }
2245
+ if (
2246
+ rollbackDurable
2247
+ && !releaseAttempted
2248
+ && publication.directoryLease !== undefined
2249
+ ) {
2250
+ try {
2251
+ releaseBeeperMessageLikeMeDirectoryLease(
2252
+ publication.directoryLease,
2253
+ );
2254
+ } catch (error) {
2255
+ errors.push(error);
2256
+ }
2257
+ }
2258
+ if (errors.length > finalizationErrors.length) {
2259
+ throw new AggregateError(
2260
+ errors,
2261
+ "Beeper Message Like Me export: finalization and published output rollback failed",
2262
+ );
2263
+ }
2264
+ }
2265
+ if (errors.length === 1) throw errors[0];
2266
+ throw new AggregateError(
2267
+ errors,
2268
+ publicationFailed
2269
+ ? "Beeper Message Like Me export: publication and source disposal both failed"
2270
+ : "Beeper Message Like Me export: finalization failed",
2271
+ );
2272
+ }
2273
+ }
2274
+ }