@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,1272 @@
1
+ import { createHash } from "node:crypto";
2
+ import { join } from "node:path";
3
+
4
+ import type { GhostgetAuth } from "./auth";
5
+ import { canonicalJson } from "./canonical-json";
6
+ import type { OperationRisk } from "./model";
7
+ import {
8
+ isProviderPluginOperationName,
9
+ type ProviderPluginOperationName,
10
+ } from "./provider-plugin-identifiers";
11
+ import {
12
+ parsePortableOperationIdentityV1,
13
+ type PortableOperationIdentityV1,
14
+ } from "./provider-plugin-portable-identity";
15
+ import {
16
+ parseLocalCliContractIdentityV1,
17
+ type LocalCliContractIdentityV1,
18
+ } from "./local-cli-contracts";
19
+ import {
20
+ createPrivateJsonIfAbsent,
21
+ ensurePrivateStateDirectory,
22
+ ghostgetStateHome,
23
+ readPrivateStateFilesBatched,
24
+ readPrivateStateFileIfPresent,
25
+ snapshotPrivateStateDirectory,
26
+ writePrivateJsonIfUnchanged,
27
+ } from "./storage";
28
+ import {
29
+ processOwnerStatus,
30
+ type ProcessOwnerStatus,
31
+ } from "./process-identity";
32
+
33
+ type Environment = Readonly<Record<string, string | undefined>>;
34
+ type JsonRecord = Record<string, unknown>;
35
+
36
+ const RUN_JOURNAL_DIRECTORY = "run-journals";
37
+ const MAX_RUN_JOURNAL_BYTES = 64 * 1024;
38
+ const MAX_JOURNAL_ERROR_BYTES = 16 * 1024;
39
+ const MAX_JOURNAL_ORIGIN_BYTES = 8 * 1024;
40
+
41
+ export type RunJournalContract =
42
+ | {
43
+ readonly transport: "provider-api";
44
+ readonly hash: string;
45
+ }
46
+ | {
47
+ readonly transport: "web-session-api";
48
+ readonly hash: string;
49
+ }
50
+ | {
51
+ readonly transport: "reviewed-template-api";
52
+ readonly hash: string;
53
+ }
54
+ | {
55
+ readonly transport: "portable-provider-plugin";
56
+ readonly identity: PortableOperationIdentityV1;
57
+ }
58
+ | {
59
+ readonly transport: "local-cli";
60
+ readonly identity: LocalCliContractIdentityV1;
61
+ };
62
+
63
+ export type RunJournalDispatch = {
64
+ readonly planned: number;
65
+ readonly started: number;
66
+ readonly verified: number;
67
+ };
68
+
69
+ export type DuplicateIntentV1 = {
70
+ readonly schemaVersion: 1;
71
+ readonly intentHash: string;
72
+ readonly sourceRunId: string;
73
+ };
74
+
75
+ export type DuplicateSuccessorV1 = DuplicateIntentV1 & {
76
+ readonly runId: string;
77
+ readonly claimedAt: string;
78
+ };
79
+
80
+ export type RunJournal = {
81
+ readonly schemaVersion: 1;
82
+ readonly revision: number;
83
+ readonly runId: string;
84
+ readonly planDigest: string;
85
+ readonly adapter: {
86
+ readonly id: string;
87
+ readonly version: string;
88
+ readonly hash: string;
89
+ };
90
+ readonly operation: ProviderPluginOperationName;
91
+ readonly risk: Extract<OperationRisk, "R2" | "R3">;
92
+ readonly inputHash: string;
93
+ readonly auth: {
94
+ readonly id: string;
95
+ readonly hash: string;
96
+ readonly kind: GhostgetAuth["kind"];
97
+ };
98
+ readonly contract: RunJournalContract;
99
+ readonly duplicateIntent?: DuplicateIntentV1;
100
+ readonly duplicateSuccessor?: DuplicateSuccessorV1;
101
+ readonly planHasAssets: boolean;
102
+ /** Whether the encrypted confirmation plan is still independently usable. */
103
+ readonly planState: "available" | "consumed";
104
+ readonly phase: "prepared" | "claimed" | "ready" | "dispatching" | "terminal";
105
+ readonly status: "pending" | "succeeded" | "submitted" | "failed" | "partial" | "indeterminate";
106
+ readonly dispatch: RunJournalDispatch;
107
+ readonly ledgerRelativePath: string | null;
108
+ readonly ledgerState: "unclaimed" | "pending" | "succeeded" | "partial" | "indeterminate" | "released";
109
+ readonly recoveryState: "absent" | "present" | "retained" | "released";
110
+ readonly assetState: "none" | "bound" | "retained" | "released";
111
+ readonly owner: {
112
+ readonly pid: number;
113
+ readonly token: string;
114
+ readonly bootId: string;
115
+ readonly processStartId: string;
116
+ readonly leaseUntil: string;
117
+ };
118
+ readonly startedAt: string;
119
+ readonly updatedAt: string;
120
+ readonly dedupeExpiresAt: string;
121
+ readonly finalOrigin: string | null;
122
+ readonly error: string | null;
123
+ };
124
+
125
+ export type RunJournalSnapshot = {
126
+ readonly journal: RunJournal;
127
+ readonly contentSha256: string;
128
+ };
129
+
130
+ export type StartRunJournal = {
131
+ readonly runId: string;
132
+ readonly planDigest: string;
133
+ readonly adapter: RunJournal["adapter"];
134
+ readonly operation: ProviderPluginOperationName;
135
+ readonly risk: Extract<OperationRisk, "R2" | "R3">;
136
+ readonly inputHash: string;
137
+ readonly auth: RunJournal["auth"];
138
+ readonly contract: RunJournalContract;
139
+ readonly duplicateIntent?: DuplicateIntentV1;
140
+ readonly plannedDispatches: number;
141
+ readonly hasPlanAssets: boolean;
142
+ readonly owner: RunJournal["owner"];
143
+ readonly startedAt: string;
144
+ readonly dedupeExpiresAt: string;
145
+ };
146
+
147
+ export type RunJournalEvent =
148
+ | {
149
+ readonly type: "confirmation-consumed";
150
+ readonly at: string;
151
+ }
152
+ | {
153
+ readonly type: "ledger-claimed";
154
+ readonly ledgerRelativePath: string;
155
+ readonly at: string;
156
+ }
157
+ | {
158
+ readonly type: "recovery-stored";
159
+ readonly at: string;
160
+ }
161
+ | {
162
+ readonly type: "dispatch-started";
163
+ readonly index: number;
164
+ readonly at: string;
165
+ }
166
+ | {
167
+ readonly type: "dispatch-verified";
168
+ readonly index: number;
169
+ readonly at: string;
170
+ }
171
+ | {
172
+ readonly type: "finished";
173
+ readonly status: "succeeded" | "submitted" | "failed" | "partial" | "indeterminate";
174
+ readonly finalOrigin: string | null;
175
+ readonly error: string | null;
176
+ readonly noOp?: true;
177
+ readonly at: string;
178
+ }
179
+ | {
180
+ /** A successful read-only reconciliation authorized local cleanup. */
181
+ readonly type: "recovery-released";
182
+ /**
183
+ * Explicit not-applied evidence also releases the idempotency ledger,
184
+ * authorizing a fresh confirmed attempt. Omitted means the provider
185
+ * effect was observed/applied and the at-most-once fence remains.
186
+ */
187
+ readonly outcome?: "applied" | "not-applied";
188
+ readonly at: string;
189
+ }
190
+ | {
191
+ readonly type: "lease-renewed";
192
+ readonly leaseUntil: string;
193
+ readonly at: string;
194
+ }
195
+ | {
196
+ /** Permanently elect the sole duplicate-tolerant successor intent. */
197
+ readonly type: "duplicate-successor-claimed";
198
+ readonly intentHash: string;
199
+ readonly runId: string;
200
+ readonly at: string;
201
+ };
202
+
203
+ function dataRecord(value: unknown, label: string): JsonRecord {
204
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
205
+ throw new Error(`${label} must be an object`);
206
+ }
207
+ const prototype: unknown = Object.getPrototypeOf(value) as unknown;
208
+ if (prototype !== Object.prototype && prototype !== null) {
209
+ throw new Error(`${label} has an unsupported prototype`);
210
+ }
211
+ const descriptors = Object.getOwnPropertyDescriptors(value);
212
+ if (Reflect.ownKeys(descriptors).some((key) => typeof key !== "string")) {
213
+ throw new Error(`${label} has unsupported symbol fields`);
214
+ }
215
+ const result: JsonRecord = {};
216
+ for (const [key, descriptor] of Object.entries(descriptors)) {
217
+ if (!descriptor.enumerable || !("value" in descriptor)) {
218
+ throw new Error(`${label} has unsupported accessor fields`);
219
+ }
220
+ Object.defineProperty(result, key, {
221
+ value: descriptor.value,
222
+ enumerable: true,
223
+ configurable: false,
224
+ writable: false,
225
+ });
226
+ }
227
+ return result;
228
+ }
229
+
230
+ function exactKeys(
231
+ value: JsonRecord,
232
+ expected: readonly string[],
233
+ label: string,
234
+ ): void {
235
+ const actual = Object.keys(value).sort();
236
+ const sortedExpected = [...expected].sort();
237
+ if (
238
+ actual.length !== sortedExpected.length
239
+ || actual.some((key, index) => key !== sortedExpected[index])
240
+ ) {
241
+ throw new Error(`${label} has unsupported fields`);
242
+ }
243
+ }
244
+
245
+ function timestamp(value: unknown, label: string): string {
246
+ if (
247
+ typeof value !== "string"
248
+ || value.length > 64
249
+ || !Number.isFinite(Date.parse(value))
250
+ ) {
251
+ throw new Error(`${label} is malformed`);
252
+ }
253
+ return value;
254
+ }
255
+
256
+ function digest(value: unknown, label: string): string {
257
+ if (typeof value !== "string" || !/^[a-f0-9]{64}$/u.test(value)) {
258
+ throw new Error(`${label} is malformed`);
259
+ }
260
+ return value;
261
+ }
262
+
263
+ function runId(value: unknown, label: string): string {
264
+ if (
265
+ typeof value !== "string"
266
+ || !/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u.test(value)
267
+ ) throw new Error(`${label} is malformed`);
268
+ return value;
269
+ }
270
+
271
+ function parseDuplicateIntent(
272
+ value: unknown,
273
+ label: string,
274
+ ): DuplicateIntentV1 {
275
+ const record = dataRecord(value, label);
276
+ exactKeys(record, ["schemaVersion", "intentHash", "sourceRunId"], label);
277
+ if (record.schemaVersion !== 1) throw new Error(`${label} is malformed`);
278
+ return Object.freeze({
279
+ schemaVersion: 1,
280
+ intentHash: digest(record.intentHash, `${label} intent hash`),
281
+ sourceRunId: runId(record.sourceRunId, `${label} source run ID`),
282
+ });
283
+ }
284
+
285
+ function parseDuplicateSuccessor(
286
+ value: unknown,
287
+ ): DuplicateSuccessorV1 {
288
+ const label = "run journal duplicate successor";
289
+ const record = dataRecord(value, label);
290
+ exactKeys(
291
+ record,
292
+ ["schemaVersion", "intentHash", "sourceRunId", "runId", "claimedAt"],
293
+ label,
294
+ );
295
+ if (record.schemaVersion !== 1) throw new Error(`${label} is malformed`);
296
+ return Object.freeze({
297
+ schemaVersion: 1,
298
+ intentHash: digest(record.intentHash, `${label} intent hash`),
299
+ sourceRunId: runId(record.sourceRunId, `${label} source run ID`),
300
+ runId: runId(record.runId, `${label} run ID`),
301
+ claimedAt: timestamp(record.claimedAt, `${label} claim time`),
302
+ });
303
+ }
304
+
305
+ function boundedString(
306
+ value: unknown,
307
+ label: string,
308
+ maximumBytes: number,
309
+ ): string {
310
+ const hasUnsafeControl = typeof value === "string"
311
+ && [...value].some((character) => {
312
+ const codePoint = character.codePointAt(0);
313
+ return codePoint !== undefined
314
+ && (
315
+ codePoint <= 0x08
316
+ || codePoint === 0x0b
317
+ || codePoint === 0x0c
318
+ || (codePoint >= 0x0e && codePoint <= 0x1f)
319
+ || codePoint === 0x7f
320
+ );
321
+ });
322
+ if (
323
+ typeof value !== "string"
324
+ || value.length < 1
325
+ || Buffer.byteLength(value, "utf8") > maximumBytes
326
+ || hasUnsafeControl
327
+ ) {
328
+ throw new Error(`${label} is malformed`);
329
+ }
330
+ return value;
331
+ }
332
+
333
+ function nullableBoundedString(
334
+ value: unknown,
335
+ label: string,
336
+ maximumBytes: number,
337
+ ): string | null {
338
+ return value === null ? null : boundedString(value, label, maximumBytes);
339
+ }
340
+
341
+ function parseAdapter(value: unknown): RunJournal["adapter"] {
342
+ const record = dataRecord(value, "run journal adapter");
343
+ exactKeys(record, ["id", "version", "hash"], "run journal adapter");
344
+ const id = boundedString(record.id, "run journal adapter ID", 128);
345
+ const version = boundedString(record.version, "run journal adapter version", 128);
346
+ if (!/^[a-z][a-z0-9-]{0,127}$/u.test(id)) {
347
+ throw new Error("run journal adapter ID is malformed");
348
+ }
349
+ return { id, version, hash: digest(record.hash, "run journal adapter hash") };
350
+ }
351
+
352
+ const authKinds = new Set<GhostgetAuth["kind"]>([
353
+ "browser-profile",
354
+ "cookie-source",
355
+ "cookies-file",
356
+ "linked-device-store",
357
+ "oauth-token-file",
358
+ ]);
359
+
360
+ function parseAuth(value: unknown): RunJournal["auth"] {
361
+ const record = dataRecord(value, "run journal auth");
362
+ exactKeys(record, ["id", "hash", "kind"], "run journal auth");
363
+ const id = boundedString(record.id, "run journal auth ID", 48);
364
+ if (!/^[a-z][a-z0-9-]{0,47}$/u.test(id) || !authKinds.has(record.kind as GhostgetAuth["kind"])) {
365
+ throw new Error("run journal auth is malformed");
366
+ }
367
+ return {
368
+ id,
369
+ hash: digest(record.hash, "run journal auth hash"),
370
+ kind: record.kind as GhostgetAuth["kind"],
371
+ };
372
+ }
373
+
374
+ function parseContract(value: unknown): RunJournalContract {
375
+ const record = dataRecord(value, "run journal contract");
376
+ if (record.transport === "portable-provider-plugin") {
377
+ exactKeys(
378
+ record,
379
+ ["transport", "identity"],
380
+ "run journal contract",
381
+ );
382
+ return Object.freeze({
383
+ transport: "portable-provider-plugin",
384
+ identity: parsePortableOperationIdentityV1(record.identity),
385
+ });
386
+ }
387
+ if (record.transport === "local-cli") {
388
+ exactKeys(record, ["transport", "identity"], "run journal contract");
389
+ return Object.freeze({
390
+ transport: "local-cli",
391
+ identity: parseLocalCliContractIdentityV1(record.identity),
392
+ });
393
+ }
394
+ exactKeys(record, ["transport", "hash"], "run journal contract");
395
+ if (
396
+ record.transport !== "provider-api"
397
+ && record.transport !== "web-session-api"
398
+ && record.transport !== "reviewed-template-api"
399
+ ) {
400
+ throw new Error("run journal contract transport is malformed");
401
+ }
402
+ return Object.freeze({
403
+ transport: record.transport,
404
+ hash: digest(record.hash, "run journal contract hash"),
405
+ });
406
+ }
407
+
408
+ function parseDispatch(value: unknown): RunJournalDispatch {
409
+ const record = dataRecord(value, "run journal dispatch");
410
+ exactKeys(record, ["planned", "started", "verified"], "run journal dispatch");
411
+ if (
412
+ !Number.isSafeInteger(record.planned)
413
+ || typeof record.planned !== "number"
414
+ || record.planned < 1
415
+ || record.planned > 25
416
+ || !Number.isSafeInteger(record.started)
417
+ || typeof record.started !== "number"
418
+ || record.started < 0
419
+ || record.started > record.planned
420
+ || !Number.isSafeInteger(record.verified)
421
+ || typeof record.verified !== "number"
422
+ || record.verified < 0
423
+ || record.verified > record.started
424
+ ) {
425
+ throw new Error("run journal dispatch is malformed");
426
+ }
427
+ return {
428
+ planned: record.planned,
429
+ started: record.started,
430
+ verified: record.verified,
431
+ };
432
+ }
433
+
434
+ function parseOwner(value: unknown): RunJournal["owner"] {
435
+ const record = dataRecord(value, "run journal owner");
436
+ exactKeys(
437
+ record,
438
+ ["pid", "token", "bootId", "processStartId", "leaseUntil"],
439
+ "run journal owner",
440
+ );
441
+ if (
442
+ !Number.isSafeInteger(record.pid)
443
+ || typeof record.pid !== "number"
444
+ || record.pid < 1
445
+ || record.pid > 2_147_483_647
446
+ || typeof record.token !== "string"
447
+ || !/^[0-9a-f-]{36}$/u.test(record.token)
448
+ || typeof record.bootId !== "string"
449
+ || !/^[a-f0-9]{64}$/u.test(record.bootId)
450
+ || typeof record.processStartId !== "string"
451
+ || !/^[a-f0-9]{64}$/u.test(record.processStartId)
452
+ ) {
453
+ throw new Error("run journal owner is malformed");
454
+ }
455
+ return {
456
+ pid: record.pid,
457
+ token: record.token,
458
+ bootId: record.bootId,
459
+ processStartId: record.processStartId,
460
+ leaseUntil: timestamp(record.leaseUntil, "run journal owner lease"),
461
+ };
462
+ }
463
+
464
+ function ledgerRelativePath(value: unknown): string | null {
465
+ if (value === null) return null;
466
+ if (
467
+ typeof value !== "string"
468
+ || !/^idempotency\/[a-f0-9]{2}\/[a-f0-9]{64}(?:\.[a-f0-9]{64})?\.json$/u.test(value)
469
+ ) {
470
+ throw new Error("run journal ledger path is malformed");
471
+ }
472
+ return value;
473
+ }
474
+
475
+ function assertJournalInvariants(value: RunJournal): void {
476
+ const { dispatch } = value;
477
+ if (
478
+ value.duplicateIntent !== undefined
479
+ && (
480
+ value.duplicateIntent.sourceRunId === value.runId
481
+ || value.operation !== "posts.publish"
482
+ || value.risk !== "R3"
483
+ || value.contract.transport !== "web-session-api"
484
+ || dispatch.planned !== 1
485
+ )
486
+ ) {
487
+ throw new Error("duplicate intent has contradictory successor state");
488
+ }
489
+ if (
490
+ value.duplicateSuccessor !== undefined
491
+ && (
492
+ value.duplicateSuccessor.sourceRunId !== value.runId
493
+ || value.duplicateSuccessor.runId === value.runId
494
+ || Date.parse(value.duplicateSuccessor.claimedAt) < Date.parse(value.updatedAt)
495
+ || value.operation !== "posts.publish"
496
+ || value.risk !== "R3"
497
+ || value.contract.transport !== "web-session-api"
498
+ || value.phase !== "terminal"
499
+ || value.status !== "indeterminate"
500
+ || dispatch.planned !== 1
501
+ || dispatch.started !== 1
502
+ || value.ledgerState !== "indeterminate"
503
+ || value.recoveryState !== "retained"
504
+ )
505
+ ) {
506
+ throw new Error("duplicate successor claim has contradictory source state");
507
+ }
508
+ if (Date.parse(value.updatedAt) < Date.parse(value.startedAt)) {
509
+ throw new Error("run journal update precedes its start");
510
+ }
511
+ if (Date.parse(value.dedupeExpiresAt) < Date.parse(value.startedAt)) {
512
+ throw new Error("run journal dedupe expiry precedes its start");
513
+ }
514
+ if (Date.parse(value.owner.leaseUntil) < Date.parse(value.startedAt)) {
515
+ throw new Error("run journal owner lease precedes its start");
516
+ }
517
+ if (!value.planHasAssets && value.assetState !== "none") {
518
+ throw new Error("asset-free run journal has contradictory asset state");
519
+ }
520
+ if (
521
+ value.planHasAssets
522
+ && value.planState === "available"
523
+ && value.assetState !== "none"
524
+ ) {
525
+ throw new Error("available confirmation cannot own plan assets");
526
+ }
527
+ if (
528
+ value.planHasAssets
529
+ && value.planState === "consumed"
530
+ && value.phase !== "terminal"
531
+ && value.assetState !== "bound"
532
+ ) {
533
+ throw new Error("consumed confirmation lost its bound plan assets");
534
+ }
535
+
536
+ if (value.phase === "prepared") {
537
+ if (
538
+ value.status !== "pending"
539
+ || value.ledgerRelativePath !== null
540
+ || value.ledgerState !== "unclaimed"
541
+ || value.recoveryState !== "absent"
542
+ || (
543
+ value.planState === "available"
544
+ && value.assetState !== "none"
545
+ )
546
+ || (
547
+ value.planState === "consumed"
548
+ && value.assetState !== "none"
549
+ && value.assetState !== "bound"
550
+ )
551
+ || dispatch.started !== 0
552
+ || dispatch.verified !== 0
553
+ ) throw new Error("prepared run journal has contradictory state");
554
+ return;
555
+ }
556
+ if (value.phase === "claimed") {
557
+ if (
558
+ value.status !== "pending"
559
+ || value.planState !== "consumed"
560
+ || value.ledgerRelativePath === null
561
+ || value.ledgerState !== "pending"
562
+ || value.recoveryState !== "absent"
563
+ || dispatch.started !== 0
564
+ || dispatch.verified !== 0
565
+ ) throw new Error("claimed run journal has contradictory state");
566
+ return;
567
+ }
568
+ if (value.phase === "ready") {
569
+ if (
570
+ value.status !== "pending"
571
+ || value.planState !== "consumed"
572
+ || value.ledgerRelativePath === null
573
+ || value.ledgerState !== "pending"
574
+ || value.recoveryState !== "present"
575
+ || dispatch.started !== 0
576
+ || dispatch.verified !== 0
577
+ ) throw new Error("ready run journal has contradictory state");
578
+ return;
579
+ }
580
+ if (value.phase === "dispatching") {
581
+ if (
582
+ value.status !== "pending"
583
+ || value.planState !== "consumed"
584
+ || value.ledgerRelativePath === null
585
+ || value.ledgerState !== "pending"
586
+ || value.recoveryState !== "present"
587
+ || dispatch.started < 1
588
+ ) throw new Error("dispatching run journal has contradictory state");
589
+ return;
590
+ }
591
+
592
+ if (value.status === "pending") {
593
+ throw new Error("terminal run journal cannot remain pending");
594
+ }
595
+ if (value.status === "failed") {
596
+ if (
597
+ dispatch.started !== 0
598
+ || dispatch.verified !== 0
599
+ || value.ledgerState !== "released"
600
+ || value.recoveryState !== "released"
601
+ || (
602
+ value.planState === "available"
603
+ ? value.assetState !== "none"
604
+ : value.planHasAssets
605
+ ? value.assetState !== "released"
606
+ : value.assetState !== "none"
607
+ )
608
+ ) throw new Error("failed run journal has contradictory state");
609
+ return;
610
+ }
611
+ if (value.status === "succeeded" || value.status === "submitted") {
612
+ const verifiedAll = dispatch.started === dispatch.planned
613
+ && dispatch.verified === dispatch.planned;
614
+ const verifiedNoOp = value.status === "succeeded"
615
+ && dispatch.started === 0
616
+ && dispatch.verified === 0;
617
+ if (
618
+ (!verifiedAll && !verifiedNoOp)
619
+ || value.planState !== "consumed"
620
+ || value.ledgerState !== "succeeded"
621
+ || value.recoveryState !== "released"
622
+ || (
623
+ value.planHasAssets
624
+ ? value.assetState !== "released"
625
+ : value.assetState !== "none"
626
+ )
627
+ ) throw new Error("successful run journal has contradictory state");
628
+ return;
629
+ }
630
+ if (value.status === "partial") {
631
+ if (
632
+ dispatch.verified < 1
633
+ || value.planState !== "consumed"
634
+ || dispatch.started !== dispatch.verified
635
+ || dispatch.verified >= dispatch.planned
636
+ || (
637
+ value.ledgerState !== "partial"
638
+ && !(
639
+ value.ledgerState === "released"
640
+ && value.recoveryState === "released"
641
+ )
642
+ )
643
+ || (
644
+ value.recoveryState !== "retained"
645
+ && value.recoveryState !== "released"
646
+ )
647
+ || (
648
+ value.recoveryState === "retained"
649
+ ? value.planHasAssets
650
+ ? value.assetState !== "retained"
651
+ : value.assetState !== "none"
652
+ : value.planHasAssets
653
+ ? value.assetState !== "released"
654
+ : value.assetState !== "none"
655
+ )
656
+ ) throw new Error("partial run journal has contradictory state");
657
+ return;
658
+ }
659
+ if (
660
+ dispatch.started < 1
661
+ || value.planState !== "consumed"
662
+ || (
663
+ value.ledgerState !== "indeterminate"
664
+ && !(
665
+ value.ledgerState === "released"
666
+ && value.recoveryState === "released"
667
+ )
668
+ )
669
+ || (
670
+ value.recoveryState !== "retained"
671
+ && value.recoveryState !== "released"
672
+ )
673
+ || (
674
+ value.recoveryState === "retained"
675
+ ? value.planHasAssets
676
+ ? value.assetState !== "retained"
677
+ : value.assetState !== "none"
678
+ : value.planHasAssets
679
+ ? value.assetState !== "released"
680
+ : value.assetState !== "none"
681
+ )
682
+ ) throw new Error("indeterminate run journal has contradictory state");
683
+ }
684
+
685
+ export function parseRunJournal(value: unknown): RunJournal {
686
+ const record = dataRecord(value, "run journal");
687
+ const keys = [
688
+ "schemaVersion",
689
+ "revision",
690
+ "runId",
691
+ "planDigest",
692
+ "adapter",
693
+ "operation",
694
+ "risk",
695
+ "inputHash",
696
+ "auth",
697
+ "contract",
698
+ "planHasAssets",
699
+ "planState",
700
+ "phase",
701
+ "status",
702
+ "dispatch",
703
+ "ledgerRelativePath",
704
+ "ledgerState",
705
+ "recoveryState",
706
+ "assetState",
707
+ "owner",
708
+ "startedAt",
709
+ "updatedAt",
710
+ "dedupeExpiresAt",
711
+ "finalOrigin",
712
+ "error",
713
+ ];
714
+ if (Object.hasOwn(record, "duplicateIntent")) keys.push("duplicateIntent");
715
+ if (Object.hasOwn(record, "duplicateSuccessor")) keys.push("duplicateSuccessor");
716
+ exactKeys(record, keys, "run journal");
717
+ if (
718
+ record.schemaVersion !== 1
719
+ || !Number.isSafeInteger(record.revision)
720
+ || typeof record.revision !== "number"
721
+ || record.revision < 0
722
+ || typeof record.runId !== "string"
723
+ || !/^[0-9a-f-]{36}$/u.test(record.runId)
724
+ || !isProviderPluginOperationName(record.operation)
725
+ || (record.risk !== "R2" && record.risk !== "R3")
726
+ || (
727
+ record.phase !== "prepared"
728
+ && record.phase !== "claimed"
729
+ && record.phase !== "ready"
730
+ && record.phase !== "dispatching"
731
+ && record.phase !== "terminal"
732
+ )
733
+ || (
734
+ record.status !== "pending"
735
+ && record.status !== "succeeded"
736
+ && record.status !== "submitted"
737
+ && record.status !== "failed"
738
+ && record.status !== "partial"
739
+ && record.status !== "indeterminate"
740
+ )
741
+ || (record.planState !== "available" && record.planState !== "consumed")
742
+ || typeof record.planHasAssets !== "boolean"
743
+ || (
744
+ record.ledgerState !== "unclaimed"
745
+ && record.ledgerState !== "pending"
746
+ && record.ledgerState !== "succeeded"
747
+ && record.ledgerState !== "partial"
748
+ && record.ledgerState !== "indeterminate"
749
+ && record.ledgerState !== "released"
750
+ )
751
+ || (
752
+ record.recoveryState !== "absent"
753
+ && record.recoveryState !== "present"
754
+ && record.recoveryState !== "retained"
755
+ && record.recoveryState !== "released"
756
+ )
757
+ || (
758
+ record.assetState !== "none"
759
+ && record.assetState !== "bound"
760
+ && record.assetState !== "retained"
761
+ && record.assetState !== "released"
762
+ )
763
+ ) {
764
+ throw new Error("run journal is malformed");
765
+ }
766
+ const journal: RunJournal = {
767
+ schemaVersion: 1,
768
+ revision: record.revision,
769
+ runId: record.runId,
770
+ planDigest: digest(record.planDigest, "run journal plan digest"),
771
+ adapter: parseAdapter(record.adapter),
772
+ operation: record.operation,
773
+ risk: record.risk,
774
+ inputHash: digest(record.inputHash, "run journal input hash"),
775
+ auth: parseAuth(record.auth),
776
+ contract: parseContract(record.contract),
777
+ ...(Object.hasOwn(record, "duplicateIntent")
778
+ ? {
779
+ duplicateIntent: parseDuplicateIntent(
780
+ record.duplicateIntent,
781
+ "run journal duplicate intent",
782
+ ),
783
+ }
784
+ : {}),
785
+ ...(Object.hasOwn(record, "duplicateSuccessor")
786
+ ? { duplicateSuccessor: parseDuplicateSuccessor(record.duplicateSuccessor) }
787
+ : {}),
788
+ planHasAssets: record.planHasAssets,
789
+ planState: record.planState,
790
+ phase: record.phase,
791
+ status: record.status,
792
+ dispatch: parseDispatch(record.dispatch),
793
+ ledgerRelativePath: ledgerRelativePath(record.ledgerRelativePath),
794
+ ledgerState: record.ledgerState,
795
+ recoveryState: record.recoveryState,
796
+ assetState: record.assetState,
797
+ owner: parseOwner(record.owner),
798
+ startedAt: timestamp(record.startedAt, "run journal start"),
799
+ updatedAt: timestamp(record.updatedAt, "run journal update"),
800
+ dedupeExpiresAt: timestamp(record.dedupeExpiresAt, "run journal dedupe expiry"),
801
+ finalOrigin: nullableBoundedString(
802
+ record.finalOrigin,
803
+ "run journal final origin",
804
+ MAX_JOURNAL_ORIGIN_BYTES,
805
+ ),
806
+ error: nullableBoundedString(
807
+ record.error,
808
+ "run journal error",
809
+ MAX_JOURNAL_ERROR_BYTES,
810
+ ),
811
+ };
812
+ if (
813
+ journal.finalOrigin !== null
814
+ && new URL(journal.finalOrigin).origin !== journal.finalOrigin
815
+ ) {
816
+ throw new Error("run journal final origin is malformed");
817
+ }
818
+ assertJournalInvariants(journal);
819
+ if (Buffer.byteLength(canonicalJson(journal), "utf8") > MAX_RUN_JOURNAL_BYTES) {
820
+ throw new Error("run journal exceeds its byte bound");
821
+ }
822
+ const frozenContract: RunJournalContract = journal.contract.transport
823
+ === "portable-provider-plugin"
824
+ ? Object.freeze({
825
+ transport: "portable-provider-plugin",
826
+ identity: journal.contract.identity,
827
+ })
828
+ : journal.contract.transport === "local-cli"
829
+ ? Object.freeze({
830
+ transport: "local-cli",
831
+ identity: journal.contract.identity,
832
+ })
833
+ : Object.freeze({ ...journal.contract });
834
+ return Object.freeze({
835
+ ...journal,
836
+ adapter: Object.freeze({ ...journal.adapter }),
837
+ auth: Object.freeze({ ...journal.auth }),
838
+ contract: frozenContract,
839
+ ...(journal.duplicateIntent === undefined
840
+ ? {}
841
+ : { duplicateIntent: Object.freeze({ ...journal.duplicateIntent }) }),
842
+ ...(journal.duplicateSuccessor === undefined
843
+ ? {}
844
+ : { duplicateSuccessor: Object.freeze({ ...journal.duplicateSuccessor }) }),
845
+ dispatch: Object.freeze({ ...journal.dispatch }),
846
+ owner: Object.freeze({ ...journal.owner }),
847
+ });
848
+ }
849
+
850
+ export function initialRunJournal(value: StartRunJournal): RunJournal {
851
+ return parseRunJournal({
852
+ schemaVersion: 1,
853
+ revision: 0,
854
+ runId: value.runId,
855
+ planDigest: value.planDigest,
856
+ adapter: value.adapter,
857
+ operation: value.operation,
858
+ risk: value.risk,
859
+ inputHash: value.inputHash,
860
+ auth: value.auth,
861
+ contract: value.contract,
862
+ ...(value.duplicateIntent === undefined
863
+ ? {}
864
+ : { duplicateIntent: value.duplicateIntent }),
865
+ planHasAssets: value.hasPlanAssets,
866
+ planState: "available",
867
+ phase: "prepared",
868
+ status: "pending",
869
+ dispatch: {
870
+ planned: value.plannedDispatches,
871
+ started: 0,
872
+ verified: 0,
873
+ },
874
+ ledgerRelativePath: null,
875
+ ledgerState: "unclaimed",
876
+ recoveryState: "absent",
877
+ assetState: "none",
878
+ owner: value.owner,
879
+ startedAt: value.startedAt,
880
+ updatedAt: value.startedAt,
881
+ dedupeExpiresAt: value.dedupeExpiresAt,
882
+ finalOrigin: null,
883
+ error: "execution was prepared but no durable final outcome was recorded",
884
+ });
885
+ }
886
+
887
+ function terminalTransition(
888
+ current: RunJournal,
889
+ event: Extract<RunJournalEvent, { readonly type: "finished" }>,
890
+ ): RunJournal {
891
+ if (current.phase === "terminal") {
892
+ throw new Error("run journal already has a terminal outcome");
893
+ }
894
+ const noOp = event.noOp === true;
895
+ const { dispatch } = current;
896
+ if (
897
+ event.status === "failed"
898
+ && dispatch.started !== 0
899
+ ) {
900
+ throw new Error("a run with started dispatches cannot finish as failed");
901
+ }
902
+ if (
903
+ (event.status === "succeeded" || event.status === "submitted")
904
+ && !(
905
+ (event.status === "succeeded" && noOp && dispatch.started === 0 && dispatch.verified === 0)
906
+ || (
907
+ !noOp
908
+ && dispatch.started === dispatch.planned
909
+ && dispatch.verified === dispatch.planned
910
+ )
911
+ )
912
+ ) {
913
+ throw new Error("a successful run must verify its complete dispatch schedule or be an explicit no-op");
914
+ }
915
+ if (
916
+ event.status === "partial"
917
+ && !(
918
+ dispatch.verified > 0
919
+ && dispatch.started === dispatch.verified
920
+ && dispatch.verified < dispatch.planned
921
+ )
922
+ ) {
923
+ throw new Error("a partial run must stop after one or more verified dispatches");
924
+ }
925
+ if (event.status === "indeterminate" && dispatch.started < 1) {
926
+ throw new Error("an indeterminate run must have crossed a dispatch boundary");
927
+ }
928
+ const retainsRecovery = event.status === "partial" || event.status === "indeterminate";
929
+ const ledgerState: RunJournal["ledgerState"] = event.status === "succeeded"
930
+ || event.status === "submitted"
931
+ ? "succeeded"
932
+ : event.status === "partial"
933
+ ? "partial"
934
+ : event.status === "indeterminate" ? "indeterminate" : "released";
935
+ return parseRunJournal({
936
+ ...current,
937
+ revision: current.revision + 1,
938
+ phase: "terminal",
939
+ status: event.status,
940
+ ledgerState,
941
+ recoveryState: retainsRecovery ? "retained" : "released",
942
+ assetState: current.assetState === "none"
943
+ ? "none"
944
+ : retainsRecovery ? "retained" : "released",
945
+ updatedAt: event.at,
946
+ finalOrigin: event.finalOrigin,
947
+ error: event.error,
948
+ });
949
+ }
950
+
951
+ export function transitionRunJournal(
952
+ currentValue: RunJournal,
953
+ event: RunJournalEvent,
954
+ ): RunJournal {
955
+ const current = parseRunJournal(currentValue);
956
+ const at = timestamp(event.at, "run journal transition time");
957
+ if (Date.parse(at) < Date.parse(current.updatedAt)) {
958
+ throw new Error("run journal transition time moved backward");
959
+ }
960
+ if (event.type === "finished") return terminalTransition(current, event);
961
+ if (event.type === "duplicate-successor-claimed") {
962
+ const intentHash = digest(
963
+ event.intentHash,
964
+ "run journal duplicate successor intent hash",
965
+ );
966
+ const successorRunId = runId(
967
+ event.runId,
968
+ "run journal duplicate successor run ID",
969
+ );
970
+ const existing = current.duplicateSuccessor;
971
+ if (existing !== undefined) {
972
+ if (
973
+ existing.intentHash === intentHash
974
+ && existing.runId === successorRunId
975
+ ) return current;
976
+ throw new Error("run journal already elected a different duplicate successor");
977
+ }
978
+ if (
979
+ current.operation !== "posts.publish"
980
+ || current.risk !== "R3"
981
+ || current.contract.transport !== "web-session-api"
982
+ || current.phase !== "terminal"
983
+ || current.status !== "indeterminate"
984
+ || current.dispatch.planned !== 1
985
+ || current.dispatch.started !== 1
986
+ || current.ledgerState !== "indeterminate"
987
+ || current.recoveryState !== "retained"
988
+ ) {
989
+ throw new Error(
990
+ "only one retained terminal indeterminate posts.publish dispatch can elect a duplicate successor",
991
+ );
992
+ }
993
+ return parseRunJournal({
994
+ ...current,
995
+ revision: current.revision + 1,
996
+ duplicateSuccessor: {
997
+ schemaVersion: 1,
998
+ intentHash,
999
+ sourceRunId: current.runId,
1000
+ runId: successorRunId,
1001
+ claimedAt: at,
1002
+ },
1003
+ // Claiming lineage must not rewrite the immutable receipt's finish time.
1004
+ updatedAt: current.updatedAt,
1005
+ });
1006
+ }
1007
+ if (event.type === "recovery-released") {
1008
+ if (current.duplicateSuccessor !== undefined) {
1009
+ throw new Error(
1010
+ "run recovery is retained because a duplicate successor intent was claimed",
1011
+ );
1012
+ }
1013
+ if (
1014
+ current.phase !== "terminal"
1015
+ || (current.status !== "partial" && current.status !== "indeterminate")
1016
+ ) {
1017
+ throw new Error(
1018
+ "only a reconciled partial or indeterminate run can release recovery resources",
1019
+ );
1020
+ }
1021
+ const releaseLedger = event.outcome === "not-applied";
1022
+ if (releaseLedger && current.dispatch.verified !== 0) {
1023
+ throw new Error(
1024
+ "a run with a verified dispatch cannot release its at-most-once ledger",
1025
+ );
1026
+ }
1027
+ if (current.recoveryState === "released") {
1028
+ if (
1029
+ releaseLedger === (current.ledgerState === "released")
1030
+ ) return current;
1031
+ throw new Error(
1032
+ "run recovery was already released with a different reconciliation outcome",
1033
+ );
1034
+ }
1035
+ return parseRunJournal({
1036
+ ...current,
1037
+ revision: current.revision + 1,
1038
+ ledgerState: releaseLedger ? "released" : current.ledgerState,
1039
+ recoveryState: "released",
1040
+ assetState: current.assetState === "retained" ? "released" : "none",
1041
+ // `updatedAt` is the terminal outcome time and is projected into the
1042
+ // immutable receipt's `finishedAt`. Recovery release is later
1043
+ // maintenance, not a new execution outcome. Advancing the revision
1044
+ // fences concurrent writers without rewriting the terminal receipt.
1045
+ updatedAt: current.updatedAt,
1046
+ });
1047
+ }
1048
+ if (current.phase === "terminal") {
1049
+ throw new Error("terminal run journals cannot transition");
1050
+ }
1051
+ if (event.type === "confirmation-consumed") {
1052
+ if (current.phase !== "prepared" || current.planState !== "available") {
1053
+ throw new Error("run journal confirmation can only be consumed once after preparation");
1054
+ }
1055
+ return parseRunJournal({
1056
+ ...current,
1057
+ revision: current.revision + 1,
1058
+ planState: "consumed",
1059
+ assetState: current.planHasAssets ? "bound" : "none",
1060
+ updatedAt: at,
1061
+ });
1062
+ }
1063
+ if (event.type === "ledger-claimed") {
1064
+ if (current.phase !== "prepared" || current.planState !== "consumed") {
1065
+ throw new Error("run journal ledger can only be claimed after preparation");
1066
+ }
1067
+ return parseRunJournal({
1068
+ ...current,
1069
+ revision: current.revision + 1,
1070
+ phase: "claimed",
1071
+ ledgerRelativePath: ledgerRelativePath(event.ledgerRelativePath),
1072
+ ledgerState: "pending",
1073
+ updatedAt: at,
1074
+ });
1075
+ }
1076
+ if (event.type === "recovery-stored") {
1077
+ if (current.phase !== "claimed") {
1078
+ throw new Error("run journal recovery can only be stored after ledger claim");
1079
+ }
1080
+ return parseRunJournal({
1081
+ ...current,
1082
+ revision: current.revision + 1,
1083
+ phase: "ready",
1084
+ recoveryState: "present",
1085
+ updatedAt: at,
1086
+ });
1087
+ }
1088
+ if (event.type === "dispatch-started") {
1089
+ if (
1090
+ (current.phase !== "ready" && current.phase !== "dispatching")
1091
+ || event.index !== current.dispatch.started + 1
1092
+ || current.dispatch.started !== current.dispatch.verified
1093
+ || event.index > current.dispatch.planned
1094
+ ) {
1095
+ throw new Error("run journal dispatch start is out of order");
1096
+ }
1097
+ return parseRunJournal({
1098
+ ...current,
1099
+ revision: current.revision + 1,
1100
+ phase: "dispatching",
1101
+ dispatch: {
1102
+ ...current.dispatch,
1103
+ started: event.index,
1104
+ },
1105
+ updatedAt: at,
1106
+ error: "a dispatch crossed its durable start boundary; a missing final outcome requires reconciliation",
1107
+ });
1108
+ }
1109
+ if (event.type === "dispatch-verified") {
1110
+ if (
1111
+ current.phase !== "dispatching"
1112
+ || event.index !== current.dispatch.verified + 1
1113
+ || event.index !== current.dispatch.started
1114
+ ) {
1115
+ throw new Error("run journal dispatch verification is out of order");
1116
+ }
1117
+ return parseRunJournal({
1118
+ ...current,
1119
+ revision: current.revision + 1,
1120
+ dispatch: {
1121
+ ...current.dispatch,
1122
+ verified: event.index,
1123
+ },
1124
+ updatedAt: at,
1125
+ error: "verified dispatch progress was stored; execution has not reached a durable final outcome",
1126
+ });
1127
+ }
1128
+ const leaseUntil = timestamp(event.leaseUntil, "run journal renewed lease");
1129
+ if (Date.parse(leaseUntil) < Date.parse(at)) {
1130
+ throw new Error("run journal renewed lease precedes its transition");
1131
+ }
1132
+ return parseRunJournal({
1133
+ ...current,
1134
+ revision: current.revision + 1,
1135
+ owner: {
1136
+ ...current.owner,
1137
+ leaseUntil,
1138
+ },
1139
+ updatedAt: at,
1140
+ });
1141
+ }
1142
+
1143
+ function journalDirectory(environment: Environment): string {
1144
+ return join(ghostgetStateHome(environment), RUN_JOURNAL_DIRECTORY);
1145
+ }
1146
+
1147
+ function journalPath(runId: string, environment: Environment): string {
1148
+ if (!/^[0-9a-f-]{36}$/u.test(runId)) throw new Error("run journal ID is invalid");
1149
+ return join(journalDirectory(environment), `${runId}.json`);
1150
+ }
1151
+
1152
+ function snapshot(journal: RunJournal): RunJournalSnapshot {
1153
+ const content = `${canonicalJson(journal)}\n`;
1154
+ return Object.freeze({
1155
+ journal,
1156
+ contentSha256: createHash("sha256").update(content).digest("hex"),
1157
+ });
1158
+ }
1159
+
1160
+ export function createRunJournal(
1161
+ journalValue: RunJournal,
1162
+ environment: Environment = process.env,
1163
+ ): RunJournalSnapshot {
1164
+ const journal = parseRunJournal(journalValue);
1165
+ ensurePrivateStateDirectory(journalDirectory(environment), environment);
1166
+ const created = createPrivateJsonIfAbsent(
1167
+ journalPath(journal.runId, environment),
1168
+ journal,
1169
+ { environment, privateParent: true },
1170
+ );
1171
+ if (!created.created) throw new Error("run journal ID already exists");
1172
+ return snapshot(journal);
1173
+ }
1174
+
1175
+ export function readRunJournal(
1176
+ runId: string,
1177
+ environment: Environment = process.env,
1178
+ ): RunJournalSnapshot | null {
1179
+ ensurePrivateStateDirectory(journalDirectory(environment), environment);
1180
+ const text = readPrivateStateFileIfPresent(
1181
+ journalPath(runId, environment),
1182
+ MAX_RUN_JOURNAL_BYTES,
1183
+ "run journal",
1184
+ environment,
1185
+ );
1186
+ if (text === null) return null;
1187
+ return parseRunJournalSnapshotText(text);
1188
+ }
1189
+
1190
+ function parseRunJournalSnapshotText(text: string): RunJournalSnapshot {
1191
+ let value: unknown;
1192
+ try {
1193
+ value = JSON.parse(text) as unknown;
1194
+ } catch {
1195
+ throw new Error("run journal is malformed");
1196
+ }
1197
+ return Object.freeze({
1198
+ journal: parseRunJournal(value),
1199
+ contentSha256: createHash("sha256").update(text).digest("hex"),
1200
+ });
1201
+ }
1202
+
1203
+ export function updateRunJournal(
1204
+ current: RunJournalSnapshot,
1205
+ event: RunJournalEvent,
1206
+ environment: Environment = process.env,
1207
+ ): RunJournalSnapshot {
1208
+ const next = transitionRunJournal(current.journal, event);
1209
+ const written = writePrivateJsonIfUnchanged(
1210
+ journalPath(current.journal.runId, environment),
1211
+ next,
1212
+ { expectedCurrentContentSha256: current.contentSha256 },
1213
+ );
1214
+ if (!written) {
1215
+ throw new Error("run journal changed concurrently; reload it before continuing");
1216
+ }
1217
+ return snapshot(next);
1218
+ }
1219
+
1220
+ export function listRunJournalSnapshots(
1221
+ environment: Environment = process.env,
1222
+ ): readonly (RunJournalSnapshot | { readonly runId: string; readonly invalid: true })[] {
1223
+ const directory = journalDirectory(environment);
1224
+ const identity = ensurePrivateStateDirectory(directory, environment);
1225
+ const directorySnapshot = snapshotPrivateStateDirectory(
1226
+ directory,
1227
+ environment,
1228
+ identity,
1229
+ );
1230
+ if (directorySnapshot.identity === null) return Object.freeze([]);
1231
+ const names = directorySnapshot.entries
1232
+ .filter((entry) => (
1233
+ entry.kind === "file"
1234
+ && /^[0-9a-f-]{36}\.json$/u.test(entry.name)
1235
+ ))
1236
+ .map((entry) => entry.name);
1237
+ return Object.freeze(
1238
+ readPrivateStateFilesBatched(directory, names, {
1239
+ maximumBytesPerFile: MAX_RUN_JOURNAL_BYTES,
1240
+ environment,
1241
+ expectedDirectoryIdentity: directorySnapshot.identity,
1242
+ }).map((file) => {
1243
+ const runId = file.name.slice(0, -5);
1244
+ if (file.status !== "present") {
1245
+ return { runId, invalid: true as const };
1246
+ }
1247
+ try {
1248
+ const value = parseRunJournalSnapshotText(file.content);
1249
+ return value.journal.runId === runId
1250
+ ? value
1251
+ : { runId, invalid: true as const };
1252
+ } catch {
1253
+ return { runId, invalid: true as const };
1254
+ }
1255
+ }),
1256
+ );
1257
+ }
1258
+
1259
+ export function runJournalNeedsRepair(
1260
+ journalValue: RunJournal,
1261
+ now = new Date(),
1262
+ inspectOwner: (owner: RunJournal["owner"]) => ProcessOwnerStatus
1263
+ = processOwnerStatus,
1264
+ ): boolean {
1265
+ const journal = parseRunJournal(journalValue);
1266
+ if (journal.phase === "terminal") return false;
1267
+ void now;
1268
+ // A wall-clock lease is diagnostic only. A suspended but exact-live owner
1269
+ // may resume with in-memory state, so another process must never mutate its
1270
+ // journal or release its claims solely because time elapsed.
1271
+ return inspectOwner(journal.owner) === "different-or-dead";
1272
+ }