@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,931 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { lstat, realpath } from "node:fs/promises";
3
+ import { dirname, isAbsolute, join, resolve } from "node:path";
4
+
5
+ import { canonicalJson, sha256 } from "./canonical-json";
6
+ import {
7
+ captureProcessOwnerIdentity,
8
+ currentProcessStartIdentity,
9
+ processOwnerStatus,
10
+ type ProcessOwnerIdentity,
11
+ type ProcessOwnerStatus,
12
+ } from "./process-identity";
13
+ import {
14
+ createPrivateJsonIfAbsent,
15
+ ensurePrivateDirectory,
16
+ readPrivateStateFileIfPresent,
17
+ removePrivateDirectoryTree,
18
+ removePrivateStateFileIfUnchanged,
19
+ snapshotPrivateStateDirectory,
20
+ readPrivateStateFilesBatch,
21
+ ghostgetStateHome,
22
+ writePrivateJsonIfUnchanged,
23
+ } from "./storage";
24
+
25
+ const CLAIM_KIND = "beeper-message-like-me-directory-lease";
26
+ const CLAIM_SCHEMA_VERSION = 2;
27
+ const EXPORT_ADMISSION_KIND = "beeper-message-like-me-export-admission";
28
+ const EXPORT_ADMISSION_SCHEMA_VERSION = 2;
29
+ const EXPORT_ADMISSION_FILE = "active.json";
30
+ const MAX_EXPORT_ADMISSION_ACQUIRE_ATTEMPTS = 8;
31
+ const MAX_CLAIMS = 64;
32
+ const MAX_CLAIM_BYTES = 16 * 1024;
33
+ const UUID_PATTERN =
34
+ /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
35
+ const DIGEST_PATTERN = /^[a-f0-9]{64}$/u;
36
+
37
+ type LeaseRole = "raw-working" | "bundle-stage";
38
+ type RawPhase = "preparing" | "launching" | "running" | "settled";
39
+
40
+ type LegacyExportAdmissionClaim = Readonly<{
41
+ schemaVersion: 1;
42
+ kind: typeof EXPORT_ADMISSION_KIND;
43
+ id: string;
44
+ owner: ProcessOwnerIdentity;
45
+ }>;
46
+
47
+ export type BeeperMessageLikeMeExportAdmissionPhase =
48
+ | "parent-owned"
49
+ | "helper-launching"
50
+ | "helper-active"
51
+ | "cleanup-unsafe";
52
+
53
+ type LegacyExportAdmissionClaimV2 = Readonly<{
54
+ schemaVersion: 2;
55
+ kind: typeof EXPORT_ADMISSION_KIND;
56
+ id: string;
57
+ owner: ProcessOwnerIdentity;
58
+ helperOwner: ProcessOwnerIdentity | null;
59
+ phase: BeeperMessageLikeMeExportAdmissionPhase;
60
+ }>;
61
+
62
+ type ExportAdmissionClaimV2 = Readonly<{
63
+ schemaVersion: 2;
64
+ kind: typeof EXPORT_ADMISSION_KIND;
65
+ id: string;
66
+ owner: ProcessOwnerIdentity;
67
+ helperOwner: ProcessOwnerIdentity | null;
68
+ phase: BeeperMessageLikeMeExportAdmissionPhase;
69
+ /** Rotated on every lifecycle write so a stale controller cannot win an ABA CAS. */
70
+ revision: string;
71
+ }>;
72
+
73
+ type ExportAdmissionClaim =
74
+ | LegacyExportAdmissionClaim
75
+ | LegacyExportAdmissionClaimV2
76
+ | ExportAdmissionClaimV2;
77
+
78
+ type ExportAdmissionSnapshot = Readonly<{
79
+ claim: ExportAdmissionClaim;
80
+ contentSha256: string;
81
+ }>;
82
+
83
+ export type BeeperMessageLikeMeExportAdmission = {
84
+ readonly claimPath: string;
85
+ readonly environment: Readonly<Record<string, string | undefined>>;
86
+ readonly claimId: string;
87
+ claim: ExportAdmissionClaimV2;
88
+ contentSha256: string;
89
+ released: boolean;
90
+ };
91
+
92
+ type LeaseDirectoryIdentity = Readonly<{
93
+ device: string;
94
+ inode: string;
95
+ /** Stable across rename and distinct across inode reuse. */
96
+ birthtimeNs: string;
97
+ }>;
98
+
99
+ type DirectoryLeaseClaim = Readonly<{
100
+ schemaVersion: 2;
101
+ kind: typeof CLAIM_KIND;
102
+ id: string;
103
+ role: LeaseRole;
104
+ path: string;
105
+ parentPath: string;
106
+ parentIdentity: LeaseDirectoryIdentity;
107
+ directoryIdentity: LeaseDirectoryIdentity;
108
+ outputRoot: string | null;
109
+ owner: ProcessOwnerIdentity;
110
+ childOwner: ProcessOwnerIdentity | null;
111
+ phase: RawPhase;
112
+ createdAtMs: number;
113
+ recoverAfterMs: number;
114
+ }>;
115
+
116
+ export type BeeperMessageLikeMeDirectoryLease = {
117
+ readonly claimPath: string;
118
+ readonly environment: Readonly<Record<string, string | undefined>>;
119
+ claim: DirectoryLeaseClaim;
120
+ contentSha256: string;
121
+ released: boolean;
122
+ };
123
+
124
+ export type BeeperMessageLikeMeRecoveryReport = Readonly<{
125
+ recovered: number;
126
+ published: number;
127
+ active: number;
128
+ indeterminate: number;
129
+ }>;
130
+
131
+ function fail(message: string): never {
132
+ throw new Error(`Beeper Message Like Me recovery: ${message}`);
133
+ }
134
+
135
+ function isErrno(error: unknown, code: string): boolean {
136
+ return typeof error === "object" && error !== null && "code" in error
137
+ && (error as { readonly code?: unknown }).code === code;
138
+ }
139
+
140
+ function record(value: unknown, label: string): Readonly<Record<string, unknown>> {
141
+ if (
142
+ typeof value !== "object"
143
+ || value === null
144
+ || Array.isArray(value)
145
+ || Object.getPrototypeOf(value) !== Object.prototype
146
+ ) return fail(`${label} must be a plain object`);
147
+ return value as Readonly<Record<string, unknown>>;
148
+ }
149
+
150
+ function exactKeys(
151
+ value: Readonly<Record<string, unknown>>,
152
+ keys: readonly string[],
153
+ label: string,
154
+ ): void {
155
+ const actual = Object.keys(value).sort();
156
+ const expected = [...keys].sort();
157
+ if (
158
+ actual.length !== expected.length
159
+ || actual.some((key, index) => key !== expected[index])
160
+ ) fail(`${label} has an unsupported shape`);
161
+ }
162
+
163
+ function boundedPath(value: unknown, label: string): string {
164
+ if (
165
+ typeof value !== "string"
166
+ || !isAbsolute(value)
167
+ || resolve(value) !== value
168
+ || value.length > 4_096
169
+ || /[\0\r\n]/u.test(value)
170
+ ) return fail(`${label} is invalid`);
171
+ return value;
172
+ }
173
+
174
+ function identity(value: unknown, label: string): LeaseDirectoryIdentity {
175
+ const source = record(value, label);
176
+ exactKeys(source, ["birthtimeNs", "device", "inode"], label);
177
+ if (
178
+ typeof source.device !== "string"
179
+ || !/^\d{1,40}$/u.test(source.device)
180
+ || typeof source.inode !== "string"
181
+ || !/^\d{1,40}$/u.test(source.inode)
182
+ || typeof source.birthtimeNs !== "string"
183
+ || !/^[1-9]\d{0,39}$/u.test(source.birthtimeNs)
184
+ ) return fail(`${label} is invalid`);
185
+ return Object.freeze({
186
+ device: source.device,
187
+ inode: source.inode,
188
+ birthtimeNs: source.birthtimeNs,
189
+ });
190
+ }
191
+
192
+ function owner(value: unknown, label: string): ProcessOwnerIdentity {
193
+ const source = record(value, label);
194
+ exactKeys(source, ["bootId", "pid", "processStartId"], label);
195
+ if (
196
+ typeof source.pid !== "number"
197
+ || !Number.isSafeInteger(source.pid)
198
+ || source.pid < 1
199
+ || source.pid > 2_147_483_647
200
+ || typeof source.bootId !== "string"
201
+ || !DIGEST_PATTERN.test(source.bootId)
202
+ || typeof source.processStartId !== "string"
203
+ || !DIGEST_PATTERN.test(source.processStartId)
204
+ ) return fail(`${label} is invalid`);
205
+ return Object.freeze({
206
+ pid: source.pid,
207
+ bootId: source.bootId,
208
+ processStartId: source.processStartId,
209
+ });
210
+ }
211
+
212
+ function parseExportAdmission(value: unknown): ExportAdmissionClaim {
213
+ const source = record(value, "export admission");
214
+ const legacy = source.schemaVersion === 1;
215
+ const currentV2 = source.schemaVersion === 2 && Object.hasOwn(source, "revision");
216
+ exactKeys(
217
+ source,
218
+ legacy
219
+ ? ["id", "kind", "owner", "schemaVersion"]
220
+ : currentV2
221
+ ? ["helperOwner", "id", "kind", "owner", "phase", "revision", "schemaVersion"]
222
+ : ["helperOwner", "id", "kind", "owner", "phase", "schemaVersion"],
223
+ "export admission",
224
+ );
225
+ if (
226
+ (source.schemaVersion !== 1 && source.schemaVersion !== EXPORT_ADMISSION_SCHEMA_VERSION)
227
+ || source.kind !== EXPORT_ADMISSION_KIND
228
+ || typeof source.id !== "string"
229
+ || !UUID_PATTERN.test(source.id)
230
+ ) return fail("export admission identity is invalid");
231
+ if (legacy) {
232
+ return Object.freeze({
233
+ schemaVersion: 1 as const,
234
+ kind: EXPORT_ADMISSION_KIND,
235
+ id: source.id,
236
+ owner: owner(source.owner, "export admission owner"),
237
+ });
238
+ }
239
+ if (
240
+ !["parent-owned", "helper-launching", "helper-active", "cleanup-unsafe"].includes(
241
+ typeof source.phase === "string" ? source.phase : "",
242
+ )
243
+ || (source.phase === "helper-active" && source.helperOwner === null)
244
+ || ((source.phase === "parent-owned" || source.phase === "helper-launching")
245
+ && source.helperOwner !== null)
246
+ ) return fail("export admission lifecycle is invalid");
247
+ const parsed = {
248
+ schemaVersion: 2 as const,
249
+ kind: EXPORT_ADMISSION_KIND,
250
+ id: source.id,
251
+ owner: owner(source.owner, "export admission owner"),
252
+ helperOwner: source.helperOwner === null
253
+ ? null
254
+ : owner(source.helperOwner, "export admission helper owner"),
255
+ phase: source.phase as BeeperMessageLikeMeExportAdmissionPhase,
256
+ } as const;
257
+ if (!currentV2) return Object.freeze(parsed);
258
+ if (typeof source.revision !== "string" || !UUID_PATTERN.test(source.revision)) {
259
+ return fail("export admission revision is invalid");
260
+ }
261
+ return Object.freeze({ ...parsed, revision: source.revision });
262
+ }
263
+
264
+ function milliseconds(value: unknown, label: string): number {
265
+ if (
266
+ typeof value !== "number"
267
+ || !Number.isSafeInteger(value)
268
+ || value < 0
269
+ ) return fail(`${label} is invalid`);
270
+ return value;
271
+ }
272
+
273
+ function parseClaim(value: unknown): DirectoryLeaseClaim {
274
+ const source = record(value, "directory lease");
275
+ exactKeys(source, [
276
+ "childOwner",
277
+ "createdAtMs",
278
+ "directoryIdentity",
279
+ "id",
280
+ "kind",
281
+ "outputRoot",
282
+ "owner",
283
+ "parentIdentity",
284
+ "parentPath",
285
+ "path",
286
+ "phase",
287
+ "recoverAfterMs",
288
+ "role",
289
+ "schemaVersion",
290
+ ], "directory lease");
291
+ if (
292
+ source.schemaVersion !== CLAIM_SCHEMA_VERSION
293
+ || source.kind !== CLAIM_KIND
294
+ || typeof source.id !== "string"
295
+ || !UUID_PATTERN.test(source.id)
296
+ || (source.role !== "raw-working" && source.role !== "bundle-stage")
297
+ || !["preparing", "launching", "running", "settled"].includes(
298
+ typeof source.phase === "string" ? source.phase : "",
299
+ )
300
+ ) return fail("directory lease identity is invalid");
301
+ const path = boundedPath(source.path, "directory lease path");
302
+ const parentPath = boundedPath(source.parentPath, "directory lease parent");
303
+ const outputRoot = source.outputRoot === null
304
+ ? null
305
+ : boundedPath(source.outputRoot, "directory lease output");
306
+ const createdAtMs = milliseconds(source.createdAtMs, "directory lease creation time");
307
+ const recoverAfterMs = milliseconds(
308
+ source.recoverAfterMs,
309
+ "directory lease recovery time",
310
+ );
311
+ if (
312
+ dirname(path) !== parentPath
313
+ || recoverAfterMs < createdAtMs
314
+ || (source.role === "bundle-stage") !== (outputRoot !== null)
315
+ || (outputRoot !== null && dirname(outputRoot) !== parentPath)
316
+ || (source.role === "bundle-stage" && source.phase !== "preparing")
317
+ || (source.role === "bundle-stage" && source.childOwner !== null)
318
+ || (source.phase === "running") !== (source.childOwner !== null)
319
+ ) return fail("directory lease coordinates are inconsistent");
320
+ return Object.freeze({
321
+ schemaVersion: 2,
322
+ kind: CLAIM_KIND,
323
+ id: source.id,
324
+ role: source.role,
325
+ path,
326
+ parentPath,
327
+ parentIdentity: identity(source.parentIdentity, "directory lease parent identity"),
328
+ directoryIdentity: identity(
329
+ source.directoryIdentity,
330
+ "directory lease directory identity",
331
+ ),
332
+ outputRoot,
333
+ owner: owner(source.owner, "directory lease owner"),
334
+ childOwner: source.childOwner === null
335
+ ? null
336
+ : owner(source.childOwner, "directory lease child owner"),
337
+ phase: source.phase as RawPhase,
338
+ createdAtMs,
339
+ recoverAfterMs,
340
+ });
341
+ }
342
+
343
+ function claimBytes(claim: DirectoryLeaseClaim): string {
344
+ return `${canonicalJson(claim)}\n`;
345
+ }
346
+
347
+ function claimSha256(claim: DirectoryLeaseClaim): string {
348
+ return sha256(claimBytes(claim));
349
+ }
350
+
351
+ function leaseRoot(
352
+ environment: Readonly<Record<string, string | undefined>>,
353
+ ): string {
354
+ return join(
355
+ ghostgetStateHome(environment),
356
+ "recovery",
357
+ "beeper-message-like-me-directory-leases",
358
+ );
359
+ }
360
+
361
+ function exportAdmissionRoot(
362
+ environment: Readonly<Record<string, string | undefined>>,
363
+ ): string {
364
+ return join(
365
+ ghostgetStateHome(environment),
366
+ "recovery",
367
+ "beeper-message-like-me-export-admission",
368
+ );
369
+ }
370
+
371
+ function exportAdmissionBytes(claim: ExportAdmissionClaim): string {
372
+ return `${canonicalJson(claim)}\n`;
373
+ }
374
+
375
+ function readExportAdmission(
376
+ root: string,
377
+ environment: Readonly<Record<string, string | undefined>>,
378
+ ): ExportAdmissionSnapshot | null {
379
+ const content = readPrivateStateFileIfPresent(
380
+ join(root, EXPORT_ADMISSION_FILE),
381
+ MAX_CLAIM_BYTES,
382
+ "Beeper Message Like Me export admission",
383
+ environment,
384
+ );
385
+ if (content === null) return null;
386
+ let decoded: unknown;
387
+ try {
388
+ decoded = JSON.parse(content) as unknown;
389
+ } catch {
390
+ return fail("export admission is not valid JSON");
391
+ }
392
+ const claim = parseExportAdmission(decoded);
393
+ if (content !== exportAdmissionBytes(claim)) {
394
+ return fail("export admission is not canonical");
395
+ }
396
+ return Object.freeze({ claim, contentSha256: sha256(content) });
397
+ }
398
+
399
+ function stateMutationCreateContention(error: unknown): boolean {
400
+ return error instanceof Error
401
+ && error.message.includes("state file mutation is already active");
402
+ }
403
+
404
+ function acquiredExportAdmission(
405
+ root: string,
406
+ environment: Readonly<Record<string, string | undefined>>,
407
+ snapshot: ExportAdmissionSnapshot,
408
+ ): BeeperMessageLikeMeExportAdmission {
409
+ if (snapshot.claim.schemaVersion !== 2 || !("revision" in snapshot.claim)) {
410
+ return fail("a legacy export admission cannot be acquired as current ownership");
411
+ }
412
+ return {
413
+ claimPath: join(root, EXPORT_ADMISSION_FILE),
414
+ environment,
415
+ claimId: snapshot.claim.id,
416
+ claim: snapshot.claim,
417
+ contentSha256: snapshot.contentSha256,
418
+ released: false,
419
+ };
420
+ }
421
+
422
+ function exportAdmissionDisposition(
423
+ claim: ExportAdmissionClaim,
424
+ inspectOwner: (owner: ProcessOwnerIdentity) => ProcessOwnerStatus,
425
+ currentBootId: string,
426
+ ): "active" | "indeterminate" | "recoverable" {
427
+ const parent = inspectOwner(claim.owner);
428
+ if (parent === "exact-live-owner") return "active";
429
+ if (parent === "unknown") return "indeterminate";
430
+ if (claim.schemaVersion === 1) {
431
+ // The released v1 admission recorded only the parent even though its
432
+ // detached per-page helper could outlive that parent. Only a kernel reboot
433
+ // proves that an unrecorded legacy helper cannot still be running.
434
+ return claim.owner.bootId === currentBootId ? "indeterminate" : "recoverable";
435
+ }
436
+ if (
437
+ claim.helperOwner !== null
438
+ && claim.helperOwner.bootId !== claim.owner.bootId
439
+ ) return "indeterminate";
440
+ if (claim.phase === "cleanup-unsafe") {
441
+ // cleanup-unsafe means the recorded helper is not a complete ownership
442
+ // proof: a descendant or process-group member may have survived after the
443
+ // recorded child exited. Only a reboot proves every such process is gone.
444
+ return claim.owner.bootId === currentBootId ? "indeterminate" : "recoverable";
445
+ }
446
+ if (claim.phase === "parent-owned") return "recoverable";
447
+ // Only a reboot proves that an unjoined helper, its streams, and its process
448
+ // group cannot still retain private work after the supervising parent exits.
449
+ if (claim.owner.bootId !== currentBootId) return "recoverable";
450
+ if (claim.helperOwner === null) return "indeterminate";
451
+ const helper = inspectOwner(claim.helperOwner);
452
+ return helper === "exact-live-owner"
453
+ ? "active"
454
+ : "indeterminate";
455
+ }
456
+
457
+ export function acquireBeeperMessageLikeMeExportAdmission(options: Readonly<{
458
+ environment?: Readonly<Record<string, string | undefined>>;
459
+ /** Test-only process-liveness seam. */
460
+ inspectOwnerForTest?: (owner: ProcessOwnerIdentity) => ProcessOwnerStatus;
461
+ /** Test-only exact kernel boot identity seam. */
462
+ currentBootIdForTest?: string;
463
+ }> = {}): BeeperMessageLikeMeExportAdmission {
464
+ if (
465
+ (options.inspectOwnerForTest !== undefined || options.currentBootIdForTest !== undefined)
466
+ && process.env.NODE_ENV !== "test"
467
+ ) return fail("export admission liveness injection is available only in tests");
468
+ const environment = options.environment ?? process.env;
469
+ const inspectOwner = options.inspectOwnerForTest ?? processOwnerStatus;
470
+ const currentBootId = options.currentBootIdForTest
471
+ ?? currentProcessStartIdentity().bootId;
472
+ if (!DIGEST_PATTERN.test(currentBootId)) return fail("current boot identity is invalid");
473
+ const root = exportAdmissionRoot(environment);
474
+ try {
475
+ ensurePrivateDirectory(root);
476
+ for (
477
+ let attempt = 0;
478
+ attempt < MAX_EXPORT_ADMISSION_ACQUIRE_ATTEMPTS;
479
+ attempt += 1
480
+ ) {
481
+ const observed = readExportAdmission(root, environment);
482
+ if (observed !== null) {
483
+ const disposition = exportAdmissionDisposition(
484
+ observed.claim,
485
+ inspectOwner,
486
+ currentBootId,
487
+ );
488
+ if (disposition === "active") {
489
+ return fail("another export is active");
490
+ }
491
+ if (disposition === "indeterminate") {
492
+ return fail("prior export owner cannot be inspected safely");
493
+ }
494
+ if (!removePrivateStateFileIfUnchanged(
495
+ join(root, EXPORT_ADMISSION_FILE),
496
+ { expectedCurrentContentSha256: observed.contentSha256 },
497
+ environment,
498
+ )) continue;
499
+ }
500
+
501
+ const processIdentity = currentProcessStartIdentity();
502
+ const claim = Object.freeze({
503
+ schemaVersion: 2 as const,
504
+ kind: EXPORT_ADMISSION_KIND,
505
+ id: randomUUID(),
506
+ owner: Object.freeze({ pid: process.pid, ...processIdentity }),
507
+ helperOwner: null,
508
+ phase: "parent-owned" as const,
509
+ revision: randomUUID(),
510
+ });
511
+ const snapshot = Object.freeze({
512
+ claim,
513
+ contentSha256: sha256(exportAdmissionBytes(claim)),
514
+ });
515
+ let created = false;
516
+ try {
517
+ created = createPrivateJsonIfAbsent(
518
+ join(root, EXPORT_ADMISSION_FILE),
519
+ claim,
520
+ { environment },
521
+ ).created;
522
+ } catch (error) {
523
+ const committed = readExportAdmission(root, environment);
524
+ if (
525
+ committed !== null
526
+ && committed.contentSha256 === snapshot.contentSha256
527
+ ) return acquiredExportAdmission(root, environment, snapshot);
528
+ if (!stateMutationCreateContention(error)) throw error;
529
+ }
530
+ if (created) return acquiredExportAdmission(root, environment, snapshot);
531
+ }
532
+ return fail("export admission could not be acquired after bounded contention");
533
+ } catch (error) {
534
+ if (
535
+ error instanceof Error
536
+ && error.message.startsWith("Beeper Message Like Me recovery:")
537
+ ) throw error;
538
+ return fail("export admission could not be acquired safely");
539
+ }
540
+ }
541
+
542
+ function updateExportAdmission(
543
+ admission: BeeperMessageLikeMeExportAdmission,
544
+ phase: BeeperMessageLikeMeExportAdmissionPhase,
545
+ helperOwner: ProcessOwnerIdentity | null,
546
+ ): void {
547
+ if (admission.released) return fail("released export admission cannot change lifecycle");
548
+ const current = admission.claim.phase;
549
+ const valid =
550
+ (phase === "helper-launching" && current === "parent-owned" && helperOwner === null)
551
+ || (phase === "helper-active" && current === "helper-launching" && helperOwner !== null)
552
+ || (phase === "parent-owned"
553
+ && (current === "helper-launching" || current === "helper-active")
554
+ && helperOwner === null)
555
+ || (phase === "cleanup-unsafe"
556
+ && current !== "parent-owned"
557
+ && (helperOwner === null || current === "helper-active"));
558
+ if (!valid) return fail("export admission lifecycle transition is invalid");
559
+ const next = Object.freeze({
560
+ ...admission.claim,
561
+ phase,
562
+ helperOwner,
563
+ revision: randomUUID(),
564
+ });
565
+ try {
566
+ if (!writePrivateJsonIfUnchanged(admission.claimPath, next, {
567
+ expectedCurrentContentSha256: admission.contentSha256,
568
+ })) return fail("export admission changed before lifecycle update");
569
+ admission.claim = next;
570
+ admission.contentSha256 = sha256(exportAdmissionBytes(next));
571
+ } catch (error) {
572
+ if (error instanceof Error && error.message.startsWith("Beeper Message Like Me recovery:")) {
573
+ throw error;
574
+ }
575
+ return fail("export admission lifecycle could not be updated safely");
576
+ }
577
+ }
578
+
579
+ export function beginBeeperMessageLikeMeHelperLaunch(
580
+ admission: BeeperMessageLikeMeExportAdmission,
581
+ ): void {
582
+ updateExportAdmission(admission, "helper-launching", null);
583
+ }
584
+
585
+ export function bindBeeperMessageLikeMeHelperOwner(
586
+ admission: BeeperMessageLikeMeExportAdmission,
587
+ childPid: number,
588
+ ): void {
589
+ if (!Number.isSafeInteger(childPid) || childPid < 1 || childPid > 2_147_483_647) {
590
+ return fail("export admission helper PID is invalid");
591
+ }
592
+ updateExportAdmission(admission, "helper-active", captureProcessOwnerIdentity(childPid));
593
+ }
594
+
595
+ export function settleBeeperMessageLikeMeHelper(
596
+ admission: BeeperMessageLikeMeExportAdmission,
597
+ ): void {
598
+ updateExportAdmission(admission, "parent-owned", null);
599
+ }
600
+
601
+ export function markBeeperMessageLikeMeHelperCleanupUnsafe(
602
+ admission: BeeperMessageLikeMeExportAdmission,
603
+ ): void {
604
+ updateExportAdmission(admission, "cleanup-unsafe", admission.claim.helperOwner);
605
+ }
606
+
607
+ export function releaseBeeperMessageLikeMeExportAdmission(
608
+ admission: BeeperMessageLikeMeExportAdmission,
609
+ ): void {
610
+ if (admission.released) return;
611
+ if (admission.claim.phase !== "parent-owned" || admission.claim.helperOwner !== null) {
612
+ return fail("export admission cannot release an unsettled helper lifecycle");
613
+ }
614
+ try {
615
+ if (!removePrivateStateFileIfUnchanged(admission.claimPath, {
616
+ expectedCurrentContentSha256: admission.contentSha256,
617
+ }, admission.environment)) return fail("export admission changed before release");
618
+ admission.released = true;
619
+ } catch (error) {
620
+ if (
621
+ error instanceof Error
622
+ && error.message.startsWith("Beeper Message Like Me recovery:")
623
+ ) throw error;
624
+ return fail("export admission could not be released safely");
625
+ }
626
+ }
627
+
628
+ async function physicalDirectoryIdentity(
629
+ path: string,
630
+ privateMode: boolean,
631
+ ): Promise<LeaseDirectoryIdentity> {
632
+ const canonical = await realpath(path);
633
+ const metadata = await lstat(path, { bigint: true });
634
+ const uid = process.getuid?.();
635
+ if (
636
+ canonical !== path
637
+ || !metadata.isDirectory()
638
+ || metadata.isSymbolicLink()
639
+ || uid === undefined
640
+ || metadata.uid !== BigInt(uid)
641
+ || metadata.birthtimeNs <= 0n
642
+ || (privateMode
643
+ ? (metadata.mode & 0o777n) !== 0o700n
644
+ : (metadata.mode & 0o022n) !== 0n)
645
+ ) return fail("directory lease target is not an owned physical directory");
646
+ return Object.freeze({
647
+ device: metadata.dev.toString(),
648
+ inode: metadata.ino.toString(),
649
+ birthtimeNs: metadata.birthtimeNs.toString(),
650
+ });
651
+ }
652
+
653
+ function sameIdentity(
654
+ left: LeaseDirectoryIdentity,
655
+ right: LeaseDirectoryIdentity,
656
+ ): boolean {
657
+ return left.device === right.device
658
+ && left.inode === right.inode
659
+ && left.birthtimeNs === right.birthtimeNs;
660
+ }
661
+
662
+ export async function createBeeperMessageLikeMeDirectoryLease(request: Readonly<{
663
+ role: LeaseRole;
664
+ path: string;
665
+ outputRoot?: string;
666
+ recoverAfterMs: number;
667
+ environment?: Readonly<Record<string, string | undefined>>;
668
+ nowMs?: number;
669
+ }>): Promise<BeeperMessageLikeMeDirectoryLease> {
670
+ const environment = request.environment ?? process.env;
671
+ const path = boundedPath(request.path, "directory lease path");
672
+ const parentPath = dirname(path);
673
+ const outputRoot = request.outputRoot === undefined
674
+ ? null
675
+ : boundedPath(request.outputRoot, "directory lease output");
676
+ const nowMs = milliseconds(request.nowMs ?? Date.now(), "directory lease creation time");
677
+ if (
678
+ !Number.isSafeInteger(request.recoverAfterMs)
679
+ || request.recoverAfterMs < nowMs
680
+ || (request.role === "bundle-stage") !== (outputRoot !== null)
681
+ || (outputRoot !== null && dirname(outputRoot) !== parentPath)
682
+ ) return fail("directory lease request is invalid");
683
+ const processIdentity = currentProcessStartIdentity();
684
+ const claim = Object.freeze({
685
+ schemaVersion: 2 as const,
686
+ kind: CLAIM_KIND,
687
+ id: randomUUID(),
688
+ role: request.role,
689
+ path,
690
+ parentPath,
691
+ parentIdentity: await physicalDirectoryIdentity(parentPath, false),
692
+ directoryIdentity: await physicalDirectoryIdentity(path, true),
693
+ outputRoot,
694
+ owner: Object.freeze({ pid: process.pid, ...processIdentity }),
695
+ childOwner: null,
696
+ phase: "preparing" as const,
697
+ createdAtMs: nowMs,
698
+ recoverAfterMs: request.recoverAfterMs,
699
+ });
700
+ const root = leaseRoot(environment);
701
+ try {
702
+ ensurePrivateDirectory(root);
703
+ const claimPath = join(root, `${claim.id}.json`);
704
+ if (!createPrivateJsonIfAbsent(claimPath, claim, { environment }).created) {
705
+ return fail("directory lease ID was already present");
706
+ }
707
+ return {
708
+ claimPath,
709
+ environment,
710
+ claim,
711
+ contentSha256: claimSha256(claim),
712
+ released: false,
713
+ };
714
+ } catch (error) {
715
+ if (
716
+ error instanceof Error
717
+ && error.message.startsWith("Beeper Message Like Me recovery:")
718
+ ) throw error;
719
+ return fail("directory lease could not be created safely");
720
+ }
721
+ }
722
+
723
+ export function updateBeeperMessageLikeMeDirectoryLease(
724
+ lease: BeeperMessageLikeMeDirectoryLease,
725
+ phase: RawPhase,
726
+ childPid?: number,
727
+ ): void {
728
+ if (lease.released || lease.claim.role !== "raw-working") {
729
+ return fail("only an active raw directory lease can change phase");
730
+ }
731
+ const currentPhase = lease.claim.phase;
732
+ const transitionIsValid =
733
+ (phase === "launching"
734
+ && (currentPhase === "preparing" || currentPhase === "settled"))
735
+ || (phase === "running" && currentPhase === "launching")
736
+ || (
737
+ phase === "settled"
738
+ && (currentPhase === "launching" || currentPhase === "running")
739
+ );
740
+ if (!transitionIsValid) {
741
+ return fail("raw directory lease lifecycle transition is invalid");
742
+ }
743
+ let childOwner: ProcessOwnerIdentity | null = null;
744
+ if (phase === "running") {
745
+ if (
746
+ childPid === undefined
747
+ || !Number.isSafeInteger(childPid)
748
+ || childPid < 1
749
+ || childPid > 2_147_483_647
750
+ ) return fail("running directory lease requires a child process identity");
751
+ childOwner = captureProcessOwnerIdentity(childPid);
752
+ } else if (childPid !== undefined) {
753
+ return fail("only a running directory lease accepts a child process identity");
754
+ }
755
+ const next = Object.freeze({ ...lease.claim, phase, childOwner });
756
+ try {
757
+ if (!writePrivateJsonIfUnchanged(lease.claimPath, next, {
758
+ expectedCurrentContentSha256: lease.contentSha256,
759
+ })) return fail("directory lease changed before its lifecycle update");
760
+ lease.claim = next;
761
+ lease.contentSha256 = claimSha256(next);
762
+ } catch (error) {
763
+ if (
764
+ error instanceof Error
765
+ && error.message.startsWith("Beeper Message Like Me recovery:")
766
+ ) throw error;
767
+ return fail("directory lease lifecycle could not be updated safely");
768
+ }
769
+ }
770
+
771
+ export function releaseBeeperMessageLikeMeDirectoryLease(
772
+ lease: BeeperMessageLikeMeDirectoryLease,
773
+ ): void {
774
+ if (lease.released) return;
775
+ try {
776
+ if (!removePrivateStateFileIfUnchanged(lease.claimPath, {
777
+ expectedCurrentContentSha256: lease.contentSha256,
778
+ }, lease.environment)) return fail("directory lease changed before release");
779
+ lease.released = true;
780
+ } catch (error) {
781
+ if (
782
+ error instanceof Error
783
+ && error.message.startsWith("Beeper Message Like Me recovery:")
784
+ ) throw error;
785
+ return fail("directory lease could not be released safely");
786
+ }
787
+ }
788
+
789
+ async function identityIfPresent(path: string): Promise<LeaseDirectoryIdentity | null> {
790
+ try {
791
+ return await physicalDirectoryIdentity(path, true);
792
+ } catch (error) {
793
+ if (isErrno(error, "ENOENT")) return null;
794
+ throw error;
795
+ }
796
+ }
797
+
798
+ function rawLeaseRecoveryDisposition(
799
+ claim: DirectoryLeaseClaim,
800
+ nowMs: number,
801
+ inspectOwner: (owner: ProcessOwnerIdentity) => ProcessOwnerStatus,
802
+ ): "active" | "indeterminate" | "recoverable" {
803
+ if (claim.phase === "preparing" || claim.phase === "settled") {
804
+ return "recoverable";
805
+ }
806
+ if (claim.phase === "running" && claim.childOwner !== null) {
807
+ const childStatus = inspectOwner(claim.childOwner);
808
+ return childStatus === "exact-live-owner"
809
+ ? "active"
810
+ : childStatus === "unknown"
811
+ ? "indeterminate"
812
+ : "recoverable";
813
+ }
814
+ return nowMs >= claim.recoverAfterMs ? "recoverable" : "indeterminate";
815
+ }
816
+
817
+ export async function recoverBeeperMessageLikeMeDirectoryLeases(options: Readonly<{
818
+ environment?: Readonly<Record<string, string | undefined>>;
819
+ nowMs?: number;
820
+ inspectOwner?: (owner: ProcessOwnerIdentity) => ProcessOwnerStatus;
821
+ }> = {}): Promise<BeeperMessageLikeMeRecoveryReport> {
822
+ const environment = options.environment ?? process.env;
823
+ const nowMs = milliseconds(options.nowMs ?? Date.now(), "directory recovery time");
824
+ const inspectOwner = options.inspectOwner ?? processOwnerStatus;
825
+ const root = leaseRoot(environment);
826
+ try {
827
+ ensurePrivateDirectory(root);
828
+ const snapshot = snapshotPrivateStateDirectory(
829
+ root,
830
+ environment,
831
+ undefined,
832
+ { recoverOrphanedMutationClaims: true },
833
+ );
834
+ if (snapshot.identity === null || snapshot.entries.length > MAX_CLAIMS) {
835
+ return fail("directory lease collection exceeded its reviewed bound");
836
+ }
837
+ const names = snapshot.entries.map((entry) => {
838
+ if (
839
+ entry.kind !== "file"
840
+ || !UUID_PATTERN.test(entry.name.replace(/\.json$/u, ""))
841
+ || !entry.name.endsWith(".json")
842
+ ) return fail("directory lease collection contains an unsupported entry");
843
+ return entry.name;
844
+ }).sort();
845
+ const files = readPrivateStateFilesBatch(root, names, {
846
+ maximumBytesPerFile: MAX_CLAIM_BYTES,
847
+ maximumTotalBytes: MAX_CLAIM_BYTES * Math.max(1, names.length),
848
+ environment,
849
+ expectedDirectoryIdentity: snapshot.identity,
850
+ });
851
+ let recovered = 0;
852
+ let published = 0;
853
+ let active = 0;
854
+ let indeterminate = 0;
855
+ for (const file of files) {
856
+ if (file.status !== "present") {
857
+ return fail("directory lease changed during recovery inspection");
858
+ }
859
+ let decoded: unknown;
860
+ try {
861
+ decoded = JSON.parse(file.content) as unknown;
862
+ } catch {
863
+ return fail("directory lease is not valid JSON");
864
+ }
865
+ const claim = parseClaim(decoded);
866
+ if (
867
+ file.name !== `${claim.id}.json`
868
+ || file.content !== claimBytes(claim)
869
+ ) return fail("directory lease is not canonical");
870
+ const claimPath = join(root, file.name);
871
+ const ownerStatus = inspectOwner(claim.owner);
872
+ if (ownerStatus === "exact-live-owner") {
873
+ active += 1;
874
+ continue;
875
+ }
876
+ if (ownerStatus === "unknown") {
877
+ indeterminate += 1;
878
+ continue;
879
+ }
880
+ if (claim.role === "raw-working") {
881
+ const disposition = rawLeaseRecoveryDisposition(
882
+ claim,
883
+ nowMs,
884
+ inspectOwner,
885
+ );
886
+ if (disposition === "active") {
887
+ active += 1;
888
+ continue;
889
+ }
890
+ if (disposition === "indeterminate") {
891
+ indeterminate += 1;
892
+ continue;
893
+ }
894
+ }
895
+ const parentIdentity = await physicalDirectoryIdentity(claim.parentPath, false);
896
+ if (!sameIdentity(parentIdentity, claim.parentIdentity)) {
897
+ return fail("directory lease parent changed before recovery");
898
+ }
899
+ const current = await identityIfPresent(claim.path);
900
+ if (current !== null && !sameIdentity(current, claim.directoryIdentity)) {
901
+ return fail("directory lease target changed before recovery");
902
+ }
903
+ if (current !== null) {
904
+ removePrivateDirectoryTree(claim.path, claim.directoryIdentity);
905
+ recovered += 1;
906
+ } else {
907
+ // Completes an interrupted identity-bound helper quarantine when present.
908
+ if (removePrivateDirectoryTree(claim.path, claim.directoryIdentity)) {
909
+ recovered += 1;
910
+ } else if (claim.outputRoot !== null) {
911
+ const outputIdentity = await identityIfPresent(claim.outputRoot);
912
+ if (
913
+ outputIdentity !== null
914
+ && !sameIdentity(outputIdentity, claim.directoryIdentity)
915
+ ) return fail("directory lease output changed before recovery");
916
+ if (outputIdentity !== null) published += 1;
917
+ }
918
+ }
919
+ if (!removePrivateStateFileIfUnchanged(claimPath, {
920
+ expectedCurrentContentSha256: sha256(file.content),
921
+ }, environment)) return fail("directory lease changed before recovery release");
922
+ }
923
+ return Object.freeze({ recovered, published, active, indeterminate });
924
+ } catch (error) {
925
+ if (
926
+ error instanceof Error
927
+ && error.message.startsWith("Beeper Message Like Me recovery:")
928
+ ) throw error;
929
+ return fail("directory lease recovery failed safely");
930
+ }
931
+ }