@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,1560 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { join } from "node:path";
3
+
4
+ import { canonicalJson } from "./canonical-json";
5
+ import {
6
+ currentProcessStartIdentity,
7
+ processOwnerStatus,
8
+ type ProcessOwnerIdentity,
9
+ type ProcessOwnerStatus,
10
+ } from "./process-identity";
11
+ import {
12
+ isPortableProviderPluginVersion,
13
+ isProviderPluginId,
14
+ isProviderPluginSurfaceId,
15
+ } from "./provider-plugin-identifiers";
16
+ import {
17
+ createPrivateJsonIfAbsent,
18
+ ensurePrivateStateDirectory,
19
+ ghostgetStateHome,
20
+ readPrivateStateFileIfPresent,
21
+ readPrivateStateFilesBatched,
22
+ snapshotPrivateStateDirectory,
23
+ writePrivateJsonIfUnchanged,
24
+ } from "./storage";
25
+
26
+ type Environment = Readonly<Record<string, string | undefined>>;
27
+ type JsonRecord = Record<string, unknown>;
28
+ type OwnerInspector = (owner: ProcessOwnerIdentity) => ProcessOwnerStatus;
29
+
30
+ export const LINKED_DEVICE_LIFECYCLE_JOURNAL_SCHEMA_VERSION = 1 as const;
31
+ export const LINKED_DEVICE_LIFECYCLE_JOURNAL_STATE_DIRECTORY =
32
+ "run-journals/linked-device-lifecycle" as const;
33
+
34
+ const MAX_JOURNAL_BYTES = 32 * 1024;
35
+ const uuidPattern =
36
+ /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
37
+ const sha256Pattern = /^[a-f0-9]{64}$/u;
38
+ const authIdPattern = /^[a-z][a-z0-9-]{0,47}$/u;
39
+
40
+ export type LinkedDeviceLifecycleOwner = ProcessOwnerIdentity & {
41
+ readonly token: string;
42
+ readonly leaseUntil: string;
43
+ };
44
+
45
+ export type LinkedDevicePairResult = {
46
+ readonly kind: "pair";
47
+ readonly resultingAuthContentHash: string;
48
+ };
49
+
50
+ export type LinkedDeviceSyncResult = {
51
+ readonly kind: "sync";
52
+ readonly itemsStored: number;
53
+ readonly projection: "linked-device-local-store";
54
+ readonly emitsProtocolAcknowledgements: true;
55
+ };
56
+
57
+ export type LinkedDeviceLifecycleResult =
58
+ | LinkedDevicePairResult
59
+ | LinkedDeviceSyncResult;
60
+
61
+ export type LinkedDeviceLifecycleReasonCode =
62
+ | "prepared"
63
+ | "external-outcome-pending"
64
+ | "external-returned-commit-pending"
65
+ | "preflight-failed"
66
+ | "cancelled-before-begin"
67
+ | "owner-exited-before-begin"
68
+ | "runtime-error-after-begin"
69
+ | "cancelled-after-begin"
70
+ | "deadline-after-begin"
71
+ | "external-returned-before-completion-persisted"
72
+ | "owner-exited-after-begin"
73
+ | "reconciled-not-applied";
74
+
75
+ export type LinkedDeviceLifecycleJournal = {
76
+ readonly schemaVersion:
77
+ typeof LINKED_DEVICE_LIFECYCLE_JOURNAL_SCHEMA_VERSION;
78
+ readonly revision: number;
79
+ readonly journalId: string;
80
+ readonly kind: "pair" | "sync-once";
81
+ readonly pluginId: string;
82
+ readonly pluginVersion: string;
83
+ readonly pluginImplementationHash: string;
84
+ readonly lifecycleContractVersion: 1;
85
+ readonly surfaceId: string;
86
+ readonly authId: string;
87
+ readonly authRealmHash: string;
88
+ readonly authContentHash: string;
89
+ readonly initialSubjectState: "bound" | "unbound";
90
+ /** Pairing input is represented only by presence; raw phone data is forbidden. */
91
+ readonly phoneProvided: boolean;
92
+ readonly phase:
93
+ | "prepared"
94
+ | "external-begun"
95
+ | "external-completed"
96
+ | "terminal";
97
+ readonly status:
98
+ | "pending"
99
+ | "succeeded"
100
+ | "safe-retry"
101
+ | "indeterminate";
102
+ readonly reconciliation:
103
+ | "not-required"
104
+ | "required"
105
+ | "resolved-applied"
106
+ | "resolved-not-applied";
107
+ readonly reconciliationHash: string | null;
108
+ readonly owner: LinkedDeviceLifecycleOwner;
109
+ readonly startedAt: string;
110
+ readonly updatedAt: string;
111
+ readonly externalStartedAt: string | null;
112
+ readonly externalCompletedAt: string | null;
113
+ readonly finishedAt: string | null;
114
+ readonly reconciledAt: string | null;
115
+ readonly result: LinkedDeviceLifecycleResult | null;
116
+ readonly reasonCode: LinkedDeviceLifecycleReasonCode | null;
117
+ };
118
+
119
+ export type StartLinkedDeviceLifecycleJournal = {
120
+ readonly journalId: string;
121
+ readonly kind: LinkedDeviceLifecycleJournal["kind"];
122
+ readonly pluginId: string;
123
+ readonly pluginVersion: string;
124
+ readonly pluginImplementationHash: string;
125
+ readonly lifecycleContractVersion: 1;
126
+ readonly surfaceId: string;
127
+ readonly authId: string;
128
+ readonly authRealmHash: string;
129
+ readonly authContentHash: string;
130
+ readonly initialSubjectState: "bound" | "unbound";
131
+ readonly phoneProvided: boolean;
132
+ readonly owner: LinkedDeviceLifecycleOwner;
133
+ readonly startedAt: string;
134
+ };
135
+
136
+ export type LinkedDeviceLifecycleJournalEvent =
137
+ | {
138
+ readonly type: "external-begin";
139
+ readonly at: string;
140
+ }
141
+ | {
142
+ readonly type: "external-complete";
143
+ readonly result: LinkedDeviceLifecycleResult;
144
+ readonly at: string;
145
+ }
146
+ | {
147
+ readonly type: "committed";
148
+ readonly result: LinkedDeviceLifecycleResult;
149
+ readonly at: string;
150
+ }
151
+ | {
152
+ readonly type: "aborted-before-external";
153
+ readonly reasonCode: "preflight-failed" | "cancelled-before-begin";
154
+ readonly at: string;
155
+ }
156
+ | {
157
+ readonly type: "outcome-not-durable";
158
+ readonly reasonCode:
159
+ | "runtime-error-after-begin"
160
+ | "cancelled-after-begin"
161
+ | "deadline-after-begin"
162
+ | "external-returned-before-completion-persisted";
163
+ readonly at: string;
164
+ }
165
+ | {
166
+ readonly type: "reconciled";
167
+ readonly outcome: "applied";
168
+ readonly evidenceHash: string;
169
+ readonly result: LinkedDeviceLifecycleResult;
170
+ readonly at: string;
171
+ }
172
+ | {
173
+ readonly type: "reconciled";
174
+ readonly outcome: "not-applied";
175
+ readonly evidenceHash: string;
176
+ readonly at: string;
177
+ }
178
+ | {
179
+ readonly type: "lease-renewed";
180
+ readonly leaseUntil: string;
181
+ readonly at: string;
182
+ };
183
+
184
+ export type LinkedDeviceLifecycleJournalSnapshot = {
185
+ readonly journal: LinkedDeviceLifecycleJournal;
186
+ readonly contentSha256: string;
187
+ };
188
+
189
+ export type LinkedDeviceLifecycleRestartDisposition =
190
+ | {
191
+ readonly kind: "complete";
192
+ readonly reason: string;
193
+ }
194
+ | {
195
+ readonly kind: "live-owner";
196
+ readonly reason: string;
197
+ }
198
+ | {
199
+ readonly kind: "owner-unknown";
200
+ readonly reason: string;
201
+ }
202
+ | {
203
+ readonly kind: "safe-retry";
204
+ readonly reason: string;
205
+ }
206
+ | {
207
+ readonly kind: "reconciliation-required";
208
+ readonly reason: string;
209
+ };
210
+
211
+ export type LinkedDeviceLifecycleJournalListEntry =
212
+ | LinkedDeviceLifecycleJournalSnapshot
213
+ | {
214
+ readonly journalId: string;
215
+ readonly invalid: true;
216
+ };
217
+
218
+ export type LinkedDeviceLifecycleWriteAuthority = {
219
+ readonly owner: LinkedDeviceLifecycleOwner;
220
+ readonly environment?: Environment;
221
+ };
222
+
223
+ function dataRecord(value: unknown, label: string): JsonRecord {
224
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
225
+ throw new Error(`${label} must be an object`);
226
+ }
227
+ const prototype: unknown = Object.getPrototypeOf(value) as unknown;
228
+ if (prototype !== Object.prototype && prototype !== null) {
229
+ throw new Error(`${label} has an unsupported prototype`);
230
+ }
231
+ const result: JsonRecord = {};
232
+ for (const key of Reflect.ownKeys(value)) {
233
+ if (typeof key !== "string") {
234
+ throw new Error(`${label} has unsupported symbol fields`);
235
+ }
236
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
237
+ if (
238
+ descriptor === undefined
239
+ || !descriptor.enumerable
240
+ || !("value" in descriptor)
241
+ ) {
242
+ throw new Error(`${label} has unsupported accessor fields`);
243
+ }
244
+ Object.defineProperty(result, key, {
245
+ value: descriptor.value,
246
+ enumerable: true,
247
+ configurable: false,
248
+ writable: false,
249
+ });
250
+ }
251
+ return result;
252
+ }
253
+
254
+ function exactKeys(
255
+ value: Readonly<JsonRecord>,
256
+ expected: readonly string[],
257
+ label: string,
258
+ ): void {
259
+ const actual = Object.keys(value).sort();
260
+ const wanted = [...expected].sort();
261
+ if (
262
+ actual.length !== wanted.length
263
+ || actual.some((key, index) => key !== wanted[index])
264
+ ) {
265
+ throw new Error(`${label} has unsupported fields`);
266
+ }
267
+ }
268
+
269
+ function canonicalTimestamp(value: unknown, label: string): string {
270
+ if (
271
+ typeof value !== "string"
272
+ || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(value)
273
+ || !Number.isFinite(Date.parse(value))
274
+ || new Date(value).toISOString() !== value
275
+ ) {
276
+ throw new Error(`${label} must be one canonical UTC timestamp`);
277
+ }
278
+ return value;
279
+ }
280
+
281
+ function nullableTimestamp(value: unknown, label: string): string | null {
282
+ return value === null ? null : canonicalTimestamp(value, label);
283
+ }
284
+
285
+ function digest(value: unknown, label: string): string {
286
+ if (typeof value !== "string" || !sha256Pattern.test(value)) {
287
+ throw new Error(`${label} must be a lowercase SHA-256 digest`);
288
+ }
289
+ return value;
290
+ }
291
+
292
+ function nullableDigest(value: unknown, label: string): string | null {
293
+ return value === null ? null : digest(value, label);
294
+ }
295
+
296
+ function strictPluginVersion(value: unknown): string {
297
+ if (!isPortableProviderPluginVersion(value)) {
298
+ throw new Error("linked-device lifecycle plugin version is malformed");
299
+ }
300
+ return value;
301
+ }
302
+
303
+ function journalId(value: unknown): string {
304
+ if (typeof value !== "string" || !uuidPattern.test(value)) {
305
+ throw new Error("linked-device lifecycle journal ID is malformed");
306
+ }
307
+ return value;
308
+ }
309
+
310
+ function parseOwner(value: unknown): LinkedDeviceLifecycleOwner {
311
+ const record = dataRecord(value, "linked-device lifecycle journal owner");
312
+ exactKeys(
313
+ record,
314
+ ["pid", "token", "bootId", "processStartId", "leaseUntil"],
315
+ "linked-device lifecycle journal owner",
316
+ );
317
+ if (
318
+ typeof record.pid !== "number"
319
+ || !Number.isSafeInteger(record.pid)
320
+ || record.pid < 1
321
+ || record.pid > 2_147_483_647
322
+ || typeof record.token !== "string"
323
+ || !uuidPattern.test(record.token)
324
+ ) {
325
+ throw new Error("linked-device lifecycle journal owner is malformed");
326
+ }
327
+ return Object.freeze({
328
+ pid: record.pid,
329
+ token: record.token,
330
+ bootId: digest(
331
+ record.bootId,
332
+ "linked-device lifecycle journal owner boot identity",
333
+ ),
334
+ processStartId: digest(
335
+ record.processStartId,
336
+ "linked-device lifecycle journal owner process identity",
337
+ ),
338
+ leaseUntil: canonicalTimestamp(
339
+ record.leaseUntil,
340
+ "linked-device lifecycle journal owner lease",
341
+ ),
342
+ });
343
+ }
344
+
345
+ export function parseLinkedDeviceLifecycleOwner(
346
+ value: unknown,
347
+ ): LinkedDeviceLifecycleOwner {
348
+ return parseOwner(value);
349
+ }
350
+
351
+ function parseResult(value: unknown): LinkedDeviceLifecycleResult {
352
+ const record = dataRecord(value, "linked-device lifecycle result");
353
+ if (record.kind === "pair") {
354
+ exactKeys(
355
+ record,
356
+ ["kind", "resultingAuthContentHash"],
357
+ "linked-device pair result",
358
+ );
359
+ return Object.freeze({
360
+ kind: "pair",
361
+ resultingAuthContentHash: digest(
362
+ record.resultingAuthContentHash,
363
+ "linked-device pair resulting auth content hash",
364
+ ),
365
+ });
366
+ }
367
+ if (record.kind === "sync") {
368
+ exactKeys(
369
+ record,
370
+ ["kind", "itemsStored", "projection", "emitsProtocolAcknowledgements"],
371
+ "linked-device sync result",
372
+ );
373
+ if (
374
+ typeof record.itemsStored !== "number"
375
+ || !Number.isSafeInteger(record.itemsStored)
376
+ || record.itemsStored < 0
377
+ || record.itemsStored > 1_000_000_000
378
+ || record.projection !== "linked-device-local-store"
379
+ || record.emitsProtocolAcknowledgements !== true
380
+ ) {
381
+ throw new Error("linked-device sync result is malformed");
382
+ }
383
+ return Object.freeze({
384
+ kind: "sync",
385
+ itemsStored: record.itemsStored,
386
+ projection: record.projection,
387
+ emitsProtocolAcknowledgements: record.emitsProtocolAcknowledgements,
388
+ });
389
+ }
390
+ throw new Error("linked-device lifecycle result kind is unsupported");
391
+ }
392
+
393
+ function nullableResult(value: unknown): LinkedDeviceLifecycleResult | null {
394
+ return value === null ? null : parseResult(value);
395
+ }
396
+
397
+ function sameResult(
398
+ left: LinkedDeviceLifecycleResult,
399
+ right: LinkedDeviceLifecycleResult,
400
+ ): boolean {
401
+ return canonicalJson(left) === canonicalJson(right);
402
+ }
403
+
404
+ function resultMatchesJournalKind(
405
+ result: LinkedDeviceLifecycleResult,
406
+ kind: LinkedDeviceLifecycleJournal["kind"],
407
+ ): boolean {
408
+ return (result.kind === "pair" && kind === "pair")
409
+ || (result.kind === "sync" && kind === "sync-once");
410
+ }
411
+
412
+ function reasonCode(
413
+ value: unknown,
414
+ ): LinkedDeviceLifecycleReasonCode | null {
415
+ if (value === null) return null;
416
+ if (
417
+ value === "prepared"
418
+ || value === "external-outcome-pending"
419
+ || value === "external-returned-commit-pending"
420
+ || value === "preflight-failed"
421
+ || value === "cancelled-before-begin"
422
+ || value === "owner-exited-before-begin"
423
+ || value === "runtime-error-after-begin"
424
+ || value === "cancelled-after-begin"
425
+ || value === "deadline-after-begin"
426
+ || value === "external-returned-before-completion-persisted"
427
+ || value === "owner-exited-after-begin"
428
+ || value === "reconciled-not-applied"
429
+ ) {
430
+ return value;
431
+ }
432
+ throw new Error("linked-device lifecycle reason code is unsupported");
433
+ }
434
+
435
+ function preBeginReasonCode(
436
+ value: unknown,
437
+ ): "preflight-failed" | "cancelled-before-begin" {
438
+ if (value !== "preflight-failed" && value !== "cancelled-before-begin") {
439
+ throw new Error(
440
+ "linked-device lifecycle pre-begin reason code is unsupported",
441
+ );
442
+ }
443
+ return value;
444
+ }
445
+
446
+ function postBeginReasonCode(
447
+ value: unknown,
448
+ ):
449
+ | "runtime-error-after-begin"
450
+ | "cancelled-after-begin"
451
+ | "deadline-after-begin"
452
+ | "external-returned-before-completion-persisted" {
453
+ if (
454
+ value !== "runtime-error-after-begin"
455
+ && value !== "cancelled-after-begin"
456
+ && value !== "deadline-after-begin"
457
+ && value !== "external-returned-before-completion-persisted"
458
+ ) {
459
+ throw new Error(
460
+ "linked-device lifecycle post-begin reason code is unsupported",
461
+ );
462
+ }
463
+ return value;
464
+ }
465
+
466
+ function assertTimestampOrder(
467
+ earlier: string,
468
+ later: string,
469
+ label: string,
470
+ ): void {
471
+ if (Date.parse(later) < Date.parse(earlier)) {
472
+ throw new Error(`${label} moved backward`);
473
+ }
474
+ }
475
+
476
+ function assertJournalInvariants(journal: LinkedDeviceLifecycleJournal): void {
477
+ assertTimestampOrder(
478
+ journal.startedAt,
479
+ journal.updatedAt,
480
+ "linked-device lifecycle journal update",
481
+ );
482
+ assertTimestampOrder(
483
+ journal.startedAt,
484
+ journal.owner.leaseUntil,
485
+ "linked-device lifecycle journal owner lease",
486
+ );
487
+ if (
488
+ journal.kind === "sync-once"
489
+ && (
490
+ journal.phoneProvided
491
+ || journal.initialSubjectState !== "bound"
492
+ )
493
+ ) {
494
+ throw new Error(
495
+ "linked-device sync lifecycle requires bound auth and forbids phone input",
496
+ );
497
+ }
498
+ if (
499
+ journal.result !== null
500
+ && !resultMatchesJournalKind(journal.result, journal.kind)
501
+ ) {
502
+ throw new Error("linked-device lifecycle result kind contradicts its journal");
503
+ }
504
+ if (journal.externalStartedAt !== null) {
505
+ assertTimestampOrder(
506
+ journal.startedAt,
507
+ journal.externalStartedAt,
508
+ "linked-device lifecycle external start",
509
+ );
510
+ assertTimestampOrder(
511
+ journal.externalStartedAt,
512
+ journal.updatedAt,
513
+ "linked-device lifecycle post-external update",
514
+ );
515
+ }
516
+ if (journal.externalCompletedAt !== null) {
517
+ if (journal.externalStartedAt === null) {
518
+ throw new Error("linked-device lifecycle completion has no external start");
519
+ }
520
+ assertTimestampOrder(
521
+ journal.externalStartedAt,
522
+ journal.externalCompletedAt,
523
+ "linked-device lifecycle external completion",
524
+ );
525
+ assertTimestampOrder(
526
+ journal.externalCompletedAt,
527
+ journal.updatedAt,
528
+ "linked-device lifecycle post-completion update",
529
+ );
530
+ }
531
+ if (journal.finishedAt !== null) {
532
+ assertTimestampOrder(
533
+ journal.startedAt,
534
+ journal.finishedAt,
535
+ "linked-device lifecycle finish",
536
+ );
537
+ assertTimestampOrder(
538
+ journal.finishedAt,
539
+ journal.updatedAt,
540
+ "linked-device lifecycle post-finish update",
541
+ );
542
+ if (journal.externalStartedAt !== null) {
543
+ assertTimestampOrder(
544
+ journal.externalStartedAt,
545
+ journal.finishedAt,
546
+ "linked-device lifecycle external-to-finish order",
547
+ );
548
+ }
549
+ if (journal.externalCompletedAt !== null) {
550
+ assertTimestampOrder(
551
+ journal.externalCompletedAt,
552
+ journal.finishedAt,
553
+ "linked-device lifecycle completion-to-finish order",
554
+ );
555
+ }
556
+ }
557
+ if (journal.reconciledAt !== null) {
558
+ if (journal.finishedAt === null) {
559
+ throw new Error("linked-device lifecycle reconciliation has no finish");
560
+ }
561
+ assertTimestampOrder(
562
+ journal.finishedAt,
563
+ journal.reconciledAt,
564
+ "linked-device lifecycle reconciliation",
565
+ );
566
+ assertTimestampOrder(
567
+ journal.reconciledAt,
568
+ journal.updatedAt,
569
+ "linked-device lifecycle post-reconciliation update",
570
+ );
571
+ }
572
+
573
+ if (journal.phase === "prepared") {
574
+ if (
575
+ journal.status !== "pending"
576
+ || journal.reconciliation !== "not-required"
577
+ || journal.reconciliationHash !== null
578
+ || journal.externalStartedAt !== null
579
+ || journal.externalCompletedAt !== null
580
+ || journal.finishedAt !== null
581
+ || journal.reconciledAt !== null
582
+ || journal.result !== null
583
+ || journal.reasonCode !== "prepared"
584
+ ) throw new Error("prepared linked-device lifecycle journal is contradictory");
585
+ return;
586
+ }
587
+ if (journal.phase === "external-begun") {
588
+ if (
589
+ journal.revision < 1
590
+ || journal.status !== "pending"
591
+ || journal.reconciliation !== "not-required"
592
+ || journal.reconciliationHash !== null
593
+ || journal.externalStartedAt === null
594
+ || journal.externalCompletedAt !== null
595
+ || journal.finishedAt !== null
596
+ || journal.reconciledAt !== null
597
+ || journal.result !== null
598
+ || journal.reasonCode !== "external-outcome-pending"
599
+ ) throw new Error("begun linked-device lifecycle journal is contradictory");
600
+ return;
601
+ }
602
+ if (journal.phase === "external-completed") {
603
+ if (
604
+ journal.revision < 2
605
+ || journal.status !== "pending"
606
+ || journal.reconciliation !== "not-required"
607
+ || journal.reconciliationHash !== null
608
+ || journal.externalStartedAt === null
609
+ || journal.externalCompletedAt === null
610
+ || journal.finishedAt !== null
611
+ || journal.reconciledAt !== null
612
+ || journal.result === null
613
+ || journal.reasonCode !== "external-returned-commit-pending"
614
+ ) throw new Error("completed linked-device lifecycle journal is contradictory");
615
+ return;
616
+ }
617
+ if (journal.status === "pending" || journal.finishedAt === null) {
618
+ throw new Error("terminal linked-device lifecycle journal is contradictory");
619
+ }
620
+ if (journal.status === "indeterminate") {
621
+ const returnedWithoutDurableBegin =
622
+ journal.revision === 1
623
+ && journal.reasonCode
624
+ === "external-returned-before-completion-persisted"
625
+ && journal.externalCompletedAt === null
626
+ && journal.result === null;
627
+ if (
628
+ (journal.revision < 2 && !returnedWithoutDurableBegin)
629
+ || journal.externalStartedAt === null
630
+ || journal.reconciliation !== "required"
631
+ || journal.reconciliationHash !== null
632
+ || journal.reconciledAt !== null
633
+ || (
634
+ journal.reasonCode !== "runtime-error-after-begin"
635
+ && journal.reasonCode !== "cancelled-after-begin"
636
+ && journal.reasonCode !== "deadline-after-begin"
637
+ && journal.reasonCode
638
+ !== "external-returned-before-completion-persisted"
639
+ && journal.reasonCode !== "owner-exited-after-begin"
640
+ )
641
+ || (
642
+ (journal.externalCompletedAt === null) !== (journal.result === null)
643
+ )
644
+ ) {
645
+ throw new Error(
646
+ "indeterminate linked-device lifecycle journal is contradictory",
647
+ );
648
+ }
649
+ return;
650
+ }
651
+ if (journal.status === "succeeded") {
652
+ if (
653
+ journal.revision < 3
654
+ || journal.externalStartedAt === null
655
+ || journal.result === null
656
+ || journal.reasonCode !== null
657
+ || (
658
+ journal.reconciliation === "not-required"
659
+ ? journal.reconciliationHash !== null
660
+ || journal.reconciledAt !== null
661
+ || journal.externalCompletedAt === null
662
+ : journal.reconciliation !== "resolved-applied"
663
+ || journal.reconciliationHash === null
664
+ || journal.reconciledAt === null
665
+ )
666
+ ) {
667
+ throw new Error(
668
+ "successful linked-device lifecycle journal is contradictory",
669
+ );
670
+ }
671
+ return;
672
+ }
673
+ if (
674
+ journal.status !== "safe-retry"
675
+ || journal.result !== null
676
+ || (
677
+ journal.reconciliation === "not-required"
678
+ ? journal.revision < 1
679
+ || journal.reconciliationHash !== null
680
+ || journal.reconciledAt !== null
681
+ || journal.externalStartedAt !== null
682
+ || journal.externalCompletedAt !== null
683
+ || (
684
+ journal.reasonCode !== "preflight-failed"
685
+ && journal.reasonCode !== "cancelled-before-begin"
686
+ && journal.reasonCode !== "owner-exited-before-begin"
687
+ )
688
+ : journal.reconciliation !== "resolved-not-applied"
689
+ || journal.revision < 3
690
+ || journal.reconciliationHash === null
691
+ || journal.reconciledAt === null
692
+ || journal.externalStartedAt === null
693
+ || journal.externalCompletedAt !== null
694
+ || journal.reasonCode !== "reconciled-not-applied"
695
+ )
696
+ ) {
697
+ throw new Error("retryable linked-device lifecycle journal is contradictory");
698
+ }
699
+ }
700
+
701
+ export function parseLinkedDeviceLifecycleJournal(
702
+ value: unknown,
703
+ ): LinkedDeviceLifecycleJournal {
704
+ const record = dataRecord(value, "linked-device lifecycle journal");
705
+ exactKeys(record, [
706
+ "schemaVersion",
707
+ "revision",
708
+ "journalId",
709
+ "kind",
710
+ "pluginId",
711
+ "pluginVersion",
712
+ "pluginImplementationHash",
713
+ "lifecycleContractVersion",
714
+ "surfaceId",
715
+ "authId",
716
+ "authRealmHash",
717
+ "authContentHash",
718
+ "initialSubjectState",
719
+ "phoneProvided",
720
+ "phase",
721
+ "status",
722
+ "reconciliation",
723
+ "reconciliationHash",
724
+ "owner",
725
+ "startedAt",
726
+ "updatedAt",
727
+ "externalStartedAt",
728
+ "externalCompletedAt",
729
+ "finishedAt",
730
+ "reconciledAt",
731
+ "result",
732
+ "reasonCode",
733
+ ], "linked-device lifecycle journal");
734
+ if (
735
+ record.schemaVersion !== LINKED_DEVICE_LIFECYCLE_JOURNAL_SCHEMA_VERSION
736
+ || typeof record.revision !== "number"
737
+ || !Number.isSafeInteger(record.revision)
738
+ || record.revision < 0
739
+ || record.revision >= Number.MAX_SAFE_INTEGER
740
+ || (record.kind !== "pair" && record.kind !== "sync-once")
741
+ || !isProviderPluginId(record.pluginId)
742
+ || record.lifecycleContractVersion !== 1
743
+ || !isProviderPluginSurfaceId(record.surfaceId)
744
+ || typeof record.authId !== "string"
745
+ || !authIdPattern.test(record.authId)
746
+ || (
747
+ record.initialSubjectState !== "bound"
748
+ && record.initialSubjectState !== "unbound"
749
+ )
750
+ || typeof record.phoneProvided !== "boolean"
751
+ || (
752
+ record.phase !== "prepared"
753
+ && record.phase !== "external-begun"
754
+ && record.phase !== "external-completed"
755
+ && record.phase !== "terminal"
756
+ )
757
+ || (
758
+ record.status !== "pending"
759
+ && record.status !== "succeeded"
760
+ && record.status !== "safe-retry"
761
+ && record.status !== "indeterminate"
762
+ )
763
+ || (
764
+ record.reconciliation !== "not-required"
765
+ && record.reconciliation !== "required"
766
+ && record.reconciliation !== "resolved-applied"
767
+ && record.reconciliation !== "resolved-not-applied"
768
+ )
769
+ ) {
770
+ throw new Error("linked-device lifecycle journal is malformed");
771
+ }
772
+ const journal: LinkedDeviceLifecycleJournal = {
773
+ schemaVersion: LINKED_DEVICE_LIFECYCLE_JOURNAL_SCHEMA_VERSION,
774
+ revision: record.revision,
775
+ journalId: journalId(record.journalId),
776
+ kind: record.kind,
777
+ pluginId: record.pluginId,
778
+ pluginVersion: strictPluginVersion(record.pluginVersion),
779
+ pluginImplementationHash: digest(
780
+ record.pluginImplementationHash,
781
+ "linked-device lifecycle plugin implementation hash",
782
+ ),
783
+ lifecycleContractVersion: 1,
784
+ surfaceId: record.surfaceId,
785
+ authId: record.authId,
786
+ authRealmHash: digest(
787
+ record.authRealmHash,
788
+ "linked-device lifecycle auth realm hash",
789
+ ),
790
+ authContentHash: digest(
791
+ record.authContentHash,
792
+ "linked-device lifecycle auth content hash",
793
+ ),
794
+ initialSubjectState: record.initialSubjectState,
795
+ phoneProvided: record.phoneProvided,
796
+ phase: record.phase,
797
+ status: record.status,
798
+ reconciliation: record.reconciliation,
799
+ reconciliationHash: nullableDigest(
800
+ record.reconciliationHash,
801
+ "linked-device lifecycle reconciliation hash",
802
+ ),
803
+ owner: parseOwner(record.owner),
804
+ startedAt: canonicalTimestamp(
805
+ record.startedAt,
806
+ "linked-device lifecycle start",
807
+ ),
808
+ updatedAt: canonicalTimestamp(
809
+ record.updatedAt,
810
+ "linked-device lifecycle update",
811
+ ),
812
+ externalStartedAt: nullableTimestamp(
813
+ record.externalStartedAt,
814
+ "linked-device lifecycle external start",
815
+ ),
816
+ externalCompletedAt: nullableTimestamp(
817
+ record.externalCompletedAt,
818
+ "linked-device lifecycle external completion",
819
+ ),
820
+ finishedAt: nullableTimestamp(
821
+ record.finishedAt,
822
+ "linked-device lifecycle finish",
823
+ ),
824
+ reconciledAt: nullableTimestamp(
825
+ record.reconciledAt,
826
+ "linked-device lifecycle reconciliation",
827
+ ),
828
+ result: nullableResult(record.result),
829
+ reasonCode: reasonCode(record.reasonCode),
830
+ };
831
+ assertJournalInvariants(journal);
832
+ if (Buffer.byteLength(canonicalJson(journal), "utf8") > MAX_JOURNAL_BYTES) {
833
+ throw new Error("linked-device lifecycle journal exceeds its byte bound");
834
+ }
835
+ return Object.freeze({
836
+ ...journal,
837
+ owner: Object.freeze({ ...journal.owner }),
838
+ ...(journal.result === null
839
+ ? { result: null }
840
+ : { result: Object.freeze({ ...journal.result }) }),
841
+ });
842
+ }
843
+
844
+ export function createLinkedDeviceLifecycleOwner(
845
+ leaseUntilValue: string,
846
+ ): LinkedDeviceLifecycleOwner {
847
+ const identity = currentProcessStartIdentity();
848
+ return parseOwner({
849
+ pid: process.pid,
850
+ token: randomUUID(),
851
+ bootId: identity.bootId,
852
+ processStartId: identity.processStartId,
853
+ leaseUntil: leaseUntilValue,
854
+ });
855
+ }
856
+
857
+ export function initialLinkedDeviceLifecycleJournal(
858
+ value: StartLinkedDeviceLifecycleJournal,
859
+ ): LinkedDeviceLifecycleJournal {
860
+ const input = dataRecord(
861
+ value,
862
+ "linked-device lifecycle journal start",
863
+ );
864
+ exactKeys(input, [
865
+ "journalId",
866
+ "kind",
867
+ "pluginId",
868
+ "pluginVersion",
869
+ "pluginImplementationHash",
870
+ "lifecycleContractVersion",
871
+ "surfaceId",
872
+ "authId",
873
+ "authRealmHash",
874
+ "authContentHash",
875
+ "initialSubjectState",
876
+ "phoneProvided",
877
+ "owner",
878
+ "startedAt",
879
+ ], "linked-device lifecycle journal start");
880
+ return parseLinkedDeviceLifecycleJournal({
881
+ schemaVersion: LINKED_DEVICE_LIFECYCLE_JOURNAL_SCHEMA_VERSION,
882
+ revision: 0,
883
+ journalId: input.journalId,
884
+ kind: input.kind,
885
+ pluginId: input.pluginId,
886
+ pluginVersion: input.pluginVersion,
887
+ pluginImplementationHash: input.pluginImplementationHash,
888
+ lifecycleContractVersion: input.lifecycleContractVersion,
889
+ surfaceId: input.surfaceId,
890
+ authId: input.authId,
891
+ authRealmHash: input.authRealmHash,
892
+ authContentHash: input.authContentHash,
893
+ initialSubjectState: input.initialSubjectState,
894
+ phoneProvided: input.phoneProvided,
895
+ phase: "prepared",
896
+ status: "pending",
897
+ reconciliation: "not-required",
898
+ reconciliationHash: null,
899
+ owner: input.owner,
900
+ startedAt: input.startedAt,
901
+ updatedAt: input.startedAt,
902
+ externalStartedAt: null,
903
+ externalCompletedAt: null,
904
+ finishedAt: null,
905
+ reconciledAt: null,
906
+ result: null,
907
+ reasonCode: "prepared",
908
+ });
909
+ }
910
+
911
+ function parseEvent(value: unknown): LinkedDeviceLifecycleJournalEvent {
912
+ const record = dataRecord(value, "linked-device lifecycle journal event");
913
+ if (record.type === "external-begin") {
914
+ exactKeys(record, ["type", "at"], "linked-device lifecycle journal event");
915
+ return {
916
+ type: "external-begin",
917
+ at: canonicalTimestamp(record.at, "linked-device lifecycle event time"),
918
+ };
919
+ }
920
+ if (record.type === "committed") {
921
+ exactKeys(
922
+ record,
923
+ ["type", "result", "at"],
924
+ "linked-device lifecycle journal event",
925
+ );
926
+ return {
927
+ type: "committed",
928
+ result: parseResult(record.result),
929
+ at: canonicalTimestamp(record.at, "linked-device lifecycle event time"),
930
+ };
931
+ }
932
+ if (record.type === "external-complete") {
933
+ exactKeys(
934
+ record,
935
+ ["type", "result", "at"],
936
+ "linked-device lifecycle journal event",
937
+ );
938
+ return {
939
+ type: "external-complete",
940
+ result: parseResult(record.result),
941
+ at: canonicalTimestamp(record.at, "linked-device lifecycle event time"),
942
+ };
943
+ }
944
+ if (record.type === "aborted-before-external") {
945
+ exactKeys(
946
+ record,
947
+ ["type", "reasonCode", "at"],
948
+ "linked-device lifecycle journal event",
949
+ );
950
+ return {
951
+ type: "aborted-before-external",
952
+ reasonCode: preBeginReasonCode(record.reasonCode),
953
+ at: canonicalTimestamp(record.at, "linked-device lifecycle event time"),
954
+ };
955
+ }
956
+ if (record.type === "outcome-not-durable") {
957
+ exactKeys(
958
+ record,
959
+ ["type", "reasonCode", "at"],
960
+ "linked-device lifecycle journal event",
961
+ );
962
+ return {
963
+ type: "outcome-not-durable",
964
+ reasonCode: postBeginReasonCode(record.reasonCode),
965
+ at: canonicalTimestamp(record.at, "linked-device lifecycle event time"),
966
+ };
967
+ }
968
+ if (record.type === "reconciled" && record.outcome === "applied") {
969
+ exactKeys(
970
+ record,
971
+ ["type", "outcome", "evidenceHash", "result", "at"],
972
+ "linked-device lifecycle journal event",
973
+ );
974
+ return {
975
+ type: "reconciled",
976
+ outcome: "applied",
977
+ evidenceHash: digest(
978
+ record.evidenceHash,
979
+ "linked-device reconciliation evidence hash",
980
+ ),
981
+ result: parseResult(record.result),
982
+ at: canonicalTimestamp(record.at, "linked-device lifecycle event time"),
983
+ };
984
+ }
985
+ if (record.type === "reconciled" && record.outcome === "not-applied") {
986
+ exactKeys(
987
+ record,
988
+ ["type", "outcome", "evidenceHash", "at"],
989
+ "linked-device lifecycle journal event",
990
+ );
991
+ return {
992
+ type: "reconciled",
993
+ outcome: "not-applied",
994
+ evidenceHash: digest(
995
+ record.evidenceHash,
996
+ "linked-device reconciliation evidence hash",
997
+ ),
998
+ at: canonicalTimestamp(record.at, "linked-device lifecycle event time"),
999
+ };
1000
+ }
1001
+ if (record.type === "lease-renewed") {
1002
+ exactKeys(
1003
+ record,
1004
+ ["type", "leaseUntil", "at"],
1005
+ "linked-device lifecycle journal event",
1006
+ );
1007
+ return {
1008
+ type: "lease-renewed",
1009
+ leaseUntil: canonicalTimestamp(
1010
+ record.leaseUntil,
1011
+ "linked-device lifecycle renewed lease",
1012
+ ),
1013
+ at: canonicalTimestamp(record.at, "linked-device lifecycle event time"),
1014
+ };
1015
+ }
1016
+ throw new Error("linked-device lifecycle journal event is malformed");
1017
+ }
1018
+
1019
+ function nextRevision(current: LinkedDeviceLifecycleJournal): number {
1020
+ if (current.revision >= Number.MAX_SAFE_INTEGER - 1) {
1021
+ throw new Error("linked-device lifecycle journal revision is exhausted");
1022
+ }
1023
+ return current.revision + 1;
1024
+ }
1025
+
1026
+ export function transitionLinkedDeviceLifecycleJournal(
1027
+ currentValue: LinkedDeviceLifecycleJournal,
1028
+ eventValue: LinkedDeviceLifecycleJournalEvent,
1029
+ ): LinkedDeviceLifecycleJournal {
1030
+ const current = parseLinkedDeviceLifecycleJournal(currentValue);
1031
+ const event = parseEvent(eventValue);
1032
+ assertTimestampOrder(
1033
+ current.updatedAt,
1034
+ event.at,
1035
+ "linked-device lifecycle transition time",
1036
+ );
1037
+ const revision = nextRevision(current);
1038
+ if (event.type === "external-begin") {
1039
+ if (current.phase !== "prepared" || current.status !== "pending") {
1040
+ throw new Error("external begin is legal only from prepared lifecycle state");
1041
+ }
1042
+ return parseLinkedDeviceLifecycleJournal({
1043
+ ...current,
1044
+ revision,
1045
+ phase: "external-begun",
1046
+ externalStartedAt: event.at,
1047
+ updatedAt: event.at,
1048
+ reasonCode: "external-outcome-pending",
1049
+ });
1050
+ }
1051
+ if (event.type === "external-complete") {
1052
+ if (current.phase !== "external-begun" || current.status !== "pending") {
1053
+ throw new Error("external completion is legal only after external begin");
1054
+ }
1055
+ if (!resultMatchesJournalKind(event.result, current.kind)) {
1056
+ throw new Error("external result kind does not match the lifecycle journal");
1057
+ }
1058
+ return parseLinkedDeviceLifecycleJournal({
1059
+ ...current,
1060
+ revision,
1061
+ phase: "external-completed",
1062
+ externalCompletedAt: event.at,
1063
+ result: event.result,
1064
+ updatedAt: event.at,
1065
+ reasonCode: "external-returned-commit-pending",
1066
+ });
1067
+ }
1068
+ if (event.type === "committed") {
1069
+ if (
1070
+ current.phase !== "external-completed"
1071
+ || current.status !== "pending"
1072
+ || current.result === null
1073
+ ) {
1074
+ throw new Error("lifecycle commit is legal only after external completion");
1075
+ }
1076
+ if (!sameResult(current.result, event.result)) {
1077
+ throw new Error(
1078
+ "lifecycle commit result does not match the externally completed result",
1079
+ );
1080
+ }
1081
+ return parseLinkedDeviceLifecycleJournal({
1082
+ ...current,
1083
+ revision,
1084
+ phase: "terminal",
1085
+ status: "succeeded",
1086
+ finishedAt: event.at,
1087
+ updatedAt: event.at,
1088
+ reasonCode: null,
1089
+ });
1090
+ }
1091
+ if (event.type === "aborted-before-external") {
1092
+ if (current.phase !== "prepared" || current.status !== "pending") {
1093
+ throw new Error("safe abort is legal only before external begin");
1094
+ }
1095
+ return parseLinkedDeviceLifecycleJournal({
1096
+ ...current,
1097
+ revision,
1098
+ phase: "terminal",
1099
+ status: "safe-retry",
1100
+ finishedAt: event.at,
1101
+ updatedAt: event.at,
1102
+ reasonCode: event.reasonCode,
1103
+ });
1104
+ }
1105
+ if (event.type === "outcome-not-durable") {
1106
+ const returnedBeforeDurableBegin =
1107
+ current.phase === "prepared"
1108
+ && event.reasonCode
1109
+ === "external-returned-before-completion-persisted";
1110
+ if (
1111
+ (
1112
+ !returnedBeforeDurableBegin
1113
+ && current.phase !== "external-begun"
1114
+ && current.phase !== "external-completed"
1115
+ )
1116
+ || current.status !== "pending"
1117
+ ) {
1118
+ throw new Error(
1119
+ "indeterminate outcome is legal only after external begin or a returned external call",
1120
+ );
1121
+ }
1122
+ return parseLinkedDeviceLifecycleJournal({
1123
+ ...current,
1124
+ revision,
1125
+ phase: "terminal",
1126
+ status: "indeterminate",
1127
+ reconciliation: "required",
1128
+ externalStartedAt: returnedBeforeDurableBegin
1129
+ ? event.at
1130
+ : current.externalStartedAt,
1131
+ finishedAt: event.at,
1132
+ updatedAt: event.at,
1133
+ reasonCode: event.reasonCode,
1134
+ });
1135
+ }
1136
+ if (event.type === "reconciled") {
1137
+ if (
1138
+ current.phase !== "terminal"
1139
+ || current.status !== "indeterminate"
1140
+ || current.reconciliation !== "required"
1141
+ ) {
1142
+ throw new Error(
1143
+ "reconciliation is legal only for an indeterminate lifecycle",
1144
+ );
1145
+ }
1146
+ if (event.outcome === "applied") {
1147
+ if (!resultMatchesJournalKind(event.result, current.kind)) {
1148
+ throw new Error(
1149
+ "reconciled result kind does not match the lifecycle journal",
1150
+ );
1151
+ }
1152
+ if (
1153
+ current.result !== null
1154
+ && !sameResult(current.result, event.result)
1155
+ ) {
1156
+ throw new Error(
1157
+ "reconciled result contradicts the previously completed result",
1158
+ );
1159
+ }
1160
+ return parseLinkedDeviceLifecycleJournal({
1161
+ ...current,
1162
+ revision,
1163
+ status: "succeeded",
1164
+ reconciliation: "resolved-applied",
1165
+ reconciliationHash: event.evidenceHash,
1166
+ reconciledAt: event.at,
1167
+ result: event.result,
1168
+ updatedAt: event.at,
1169
+ reasonCode: null,
1170
+ });
1171
+ }
1172
+ if (
1173
+ current.externalCompletedAt !== null
1174
+ || current.result !== null
1175
+ || current.reasonCode
1176
+ === "external-returned-before-completion-persisted"
1177
+ ) {
1178
+ throw new Error(
1179
+ "not-applied reconciliation contradicts durable external completion",
1180
+ );
1181
+ }
1182
+ return parseLinkedDeviceLifecycleJournal({
1183
+ ...current,
1184
+ revision,
1185
+ status: "safe-retry",
1186
+ reconciliation: "resolved-not-applied",
1187
+ reconciliationHash: event.evidenceHash,
1188
+ reconciledAt: event.at,
1189
+ result: null,
1190
+ updatedAt: event.at,
1191
+ reasonCode: "reconciled-not-applied",
1192
+ });
1193
+ }
1194
+ if (
1195
+ current.phase === "terminal"
1196
+ && current.status !== "indeterminate"
1197
+ ) {
1198
+ throw new Error("resolved linked-device lifecycle journals cannot renew");
1199
+ }
1200
+ assertTimestampOrder(
1201
+ event.at,
1202
+ event.leaseUntil,
1203
+ "linked-device lifecycle renewed lease",
1204
+ );
1205
+ assertTimestampOrder(
1206
+ current.owner.leaseUntil,
1207
+ event.leaseUntil,
1208
+ "linked-device lifecycle renewed lease",
1209
+ );
1210
+ return parseLinkedDeviceLifecycleJournal({
1211
+ ...current,
1212
+ revision,
1213
+ owner: {
1214
+ ...current.owner,
1215
+ leaseUntil: event.leaseUntil,
1216
+ },
1217
+ updatedAt: event.at,
1218
+ });
1219
+ }
1220
+
1221
+ function journalDirectory(environment: Environment): string {
1222
+ return join(
1223
+ ghostgetStateHome(environment),
1224
+ ...LINKED_DEVICE_LIFECYCLE_JOURNAL_STATE_DIRECTORY.split("/"),
1225
+ );
1226
+ }
1227
+
1228
+ function journalPath(journalIdValue: string, environment: Environment): string {
1229
+ return join(journalDirectory(environment), `${journalId(journalIdValue)}.json`);
1230
+ }
1231
+
1232
+ function snapshot(
1233
+ journalValue: LinkedDeviceLifecycleJournal,
1234
+ ): LinkedDeviceLifecycleJournalSnapshot {
1235
+ const journal = parseLinkedDeviceLifecycleJournal(journalValue);
1236
+ const content = `${canonicalJson(journal)}\n`;
1237
+ return Object.freeze({
1238
+ journal,
1239
+ contentSha256: createHash("sha256").update(content).digest("hex"),
1240
+ });
1241
+ }
1242
+
1243
+ function assertSnapshot(
1244
+ value: LinkedDeviceLifecycleJournalSnapshot,
1245
+ ): LinkedDeviceLifecycleJournalSnapshot {
1246
+ const record = dataRecord(
1247
+ value,
1248
+ "linked-device lifecycle journal snapshot",
1249
+ );
1250
+ exactKeys(
1251
+ record,
1252
+ ["journal", "contentSha256"],
1253
+ "linked-device lifecycle journal snapshot",
1254
+ );
1255
+ if (
1256
+ typeof record.contentSha256 !== "string"
1257
+ || !sha256Pattern.test(record.contentSha256)
1258
+ ) {
1259
+ throw new Error("linked-device lifecycle journal snapshot is malformed");
1260
+ }
1261
+ const canonical = snapshot(
1262
+ parseLinkedDeviceLifecycleJournal(record.journal),
1263
+ );
1264
+ if (canonical.contentSha256 !== record.contentSha256) {
1265
+ throw new Error(
1266
+ "linked-device lifecycle journal snapshot is not content-bound",
1267
+ );
1268
+ }
1269
+ return canonical;
1270
+ }
1271
+
1272
+ function parseSnapshotText(text: string): LinkedDeviceLifecycleJournalSnapshot {
1273
+ let value: unknown;
1274
+ try {
1275
+ value = JSON.parse(text) as unknown;
1276
+ } catch {
1277
+ throw new Error("linked-device lifecycle journal is malformed");
1278
+ }
1279
+ const parsed = parseLinkedDeviceLifecycleJournal(value);
1280
+ const canonical = `${canonicalJson(parsed)}\n`;
1281
+ if (text !== canonical) {
1282
+ throw new Error("linked-device lifecycle journal is not canonical JSON");
1283
+ }
1284
+ return Object.freeze({
1285
+ journal: parsed,
1286
+ contentSha256: createHash("sha256").update(text).digest("hex"),
1287
+ });
1288
+ }
1289
+
1290
+ function sameOwnerAuthority(
1291
+ left: LinkedDeviceLifecycleOwner,
1292
+ right: LinkedDeviceLifecycleOwner,
1293
+ ): boolean {
1294
+ return left.pid === right.pid
1295
+ && left.token === right.token
1296
+ && left.bootId === right.bootId
1297
+ && left.processStartId === right.processStartId;
1298
+ }
1299
+
1300
+ function isCurrentProcessOwner(owner: ProcessOwnerIdentity): boolean {
1301
+ const identity = currentProcessStartIdentity();
1302
+ return owner.pid === process.pid
1303
+ && owner.bootId === identity.bootId
1304
+ && owner.processStartId === identity.processStartId;
1305
+ }
1306
+
1307
+ function requireExactLiveOwner(
1308
+ ownerValue: LinkedDeviceLifecycleOwner,
1309
+ ): LinkedDeviceLifecycleOwner {
1310
+ const owner = parseOwner(ownerValue);
1311
+ if (!isCurrentProcessOwner(owner)) {
1312
+ throw new Error(
1313
+ "linked-device lifecycle write owner is not the current process",
1314
+ );
1315
+ }
1316
+ if (processOwnerStatus(owner) !== "exact-live-owner") {
1317
+ throw new Error("linked-device lifecycle write owner is not exact and live");
1318
+ }
1319
+ return owner;
1320
+ }
1321
+
1322
+ function requireWriteAuthority(
1323
+ journal: LinkedDeviceLifecycleJournal,
1324
+ authority: LinkedDeviceLifecycleWriteAuthority,
1325
+ ): LinkedDeviceLifecycleOwner {
1326
+ const owner = requireExactLiveOwner(authority.owner);
1327
+ if (!sameOwnerAuthority(journal.owner, owner)) {
1328
+ throw new Error("linked-device lifecycle write authority does not own the journal");
1329
+ }
1330
+ return owner;
1331
+ }
1332
+
1333
+ export function createLinkedDeviceLifecycleJournal(
1334
+ journalValue: LinkedDeviceLifecycleJournal,
1335
+ environment: Environment = process.env,
1336
+ ): LinkedDeviceLifecycleJournalSnapshot {
1337
+ const journal = parseLinkedDeviceLifecycleJournal(journalValue);
1338
+ if (journal.revision !== 0 || journal.phase !== "prepared") {
1339
+ throw new Error(
1340
+ "new linked-device lifecycle journals must start prepared at revision zero",
1341
+ );
1342
+ }
1343
+ requireExactLiveOwner(journal.owner);
1344
+ ensurePrivateStateDirectory(journalDirectory(environment), environment);
1345
+ const created = createPrivateJsonIfAbsent(
1346
+ journalPath(journal.journalId, environment),
1347
+ journal,
1348
+ { environment, privateParent: true },
1349
+ );
1350
+ if (!created.created) {
1351
+ throw new Error("linked-device lifecycle journal ID already exists");
1352
+ }
1353
+ return snapshot(journal);
1354
+ }
1355
+
1356
+ export function readLinkedDeviceLifecycleJournal(
1357
+ journalIdValue: string,
1358
+ environment: Environment = process.env,
1359
+ ): LinkedDeviceLifecycleJournalSnapshot | null {
1360
+ ensurePrivateStateDirectory(journalDirectory(environment), environment);
1361
+ const text = readPrivateStateFileIfPresent(
1362
+ journalPath(journalIdValue, environment),
1363
+ MAX_JOURNAL_BYTES,
1364
+ "linked-device lifecycle journal",
1365
+ environment,
1366
+ );
1367
+ return text === null ? null : parseSnapshotText(text);
1368
+ }
1369
+
1370
+ export function updateLinkedDeviceLifecycleJournal(
1371
+ currentValue: LinkedDeviceLifecycleJournalSnapshot,
1372
+ event: LinkedDeviceLifecycleJournalEvent,
1373
+ authority: LinkedDeviceLifecycleWriteAuthority,
1374
+ ): LinkedDeviceLifecycleJournalSnapshot {
1375
+ const current = assertSnapshot(currentValue);
1376
+ requireWriteAuthority(current.journal, authority);
1377
+ const next = transitionLinkedDeviceLifecycleJournal(current.journal, event);
1378
+ const written = writePrivateJsonIfUnchanged(
1379
+ journalPath(
1380
+ current.journal.journalId,
1381
+ authority.environment ?? process.env,
1382
+ ),
1383
+ next,
1384
+ { expectedCurrentContentSha256: current.contentSha256 },
1385
+ );
1386
+ if (!written) {
1387
+ throw new Error(
1388
+ "linked-device lifecycle journal changed concurrently; reload before continuing",
1389
+ );
1390
+ }
1391
+ return snapshot(next);
1392
+ }
1393
+
1394
+ export function listLinkedDeviceLifecycleJournalSnapshots(
1395
+ environment: Environment = process.env,
1396
+ ): readonly LinkedDeviceLifecycleJournalListEntry[] {
1397
+ const directory = journalDirectory(environment);
1398
+ const identity = ensurePrivateStateDirectory(directory, environment);
1399
+ const directorySnapshot = snapshotPrivateStateDirectory(
1400
+ directory,
1401
+ environment,
1402
+ identity,
1403
+ );
1404
+ if (directorySnapshot.identity === null) return Object.freeze([]);
1405
+ const expectedNamePattern =
1406
+ /^([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\.json$/u;
1407
+ const fileNames = directorySnapshot.entries
1408
+ .filter((entry) =>
1409
+ entry.kind === "file"
1410
+ && expectedNamePattern.test(entry.name))
1411
+ .map((entry) => entry.name);
1412
+ const fileByName = new Map(
1413
+ readPrivateStateFilesBatched(directory, fileNames, {
1414
+ maximumBytesPerFile: MAX_JOURNAL_BYTES,
1415
+ environment,
1416
+ expectedDirectoryIdentity: directorySnapshot.identity,
1417
+ }).map((file) => [file.name, file] as const),
1418
+ );
1419
+ return Object.freeze(directorySnapshot.entries.map((entry) => {
1420
+ const match = expectedNamePattern.exec(entry.name);
1421
+ if (match === null || match[1] === undefined) {
1422
+ return {
1423
+ journalId: `invalid-${
1424
+ createHash("sha256")
1425
+ .update("linked-device-lifecycle-invalid-entry\0", "utf8")
1426
+ .update(entry.name, "utf8")
1427
+ .digest("hex")
1428
+ }`,
1429
+ invalid: true as const,
1430
+ };
1431
+ }
1432
+ const id = match[1];
1433
+ if (entry.kind !== "file") {
1434
+ return { journalId: id, invalid: true as const };
1435
+ }
1436
+ const file = fileByName.get(entry.name);
1437
+ if (file === undefined || file.status !== "present") {
1438
+ return { journalId: id, invalid: true as const };
1439
+ }
1440
+ try {
1441
+ const parsed = parseSnapshotText(file.content);
1442
+ return parsed.journal.journalId === id
1443
+ ? parsed
1444
+ : { journalId: id, invalid: true as const };
1445
+ } catch {
1446
+ return { journalId: id, invalid: true as const };
1447
+ }
1448
+ }));
1449
+ }
1450
+
1451
+ export function classifyLinkedDeviceLifecycleRestart(
1452
+ journalValue: LinkedDeviceLifecycleJournal,
1453
+ inspectOwner: OwnerInspector = processOwnerStatus,
1454
+ ): LinkedDeviceLifecycleRestartDisposition {
1455
+ const journal = parseLinkedDeviceLifecycleJournal(journalValue);
1456
+ if (journal.phase === "terminal" && journal.status !== "indeterminate") {
1457
+ return Object.freeze({
1458
+ kind: "complete",
1459
+ reason: "the linked-device lifecycle already has a resolved terminal outcome",
1460
+ });
1461
+ }
1462
+ const ownerStatus = inspectOwner(journal.owner);
1463
+ if (ownerStatus === "exact-live-owner") {
1464
+ return Object.freeze({
1465
+ kind: "live-owner",
1466
+ reason: "the exact process owner is still live; restart repair is forbidden",
1467
+ });
1468
+ }
1469
+ if (ownerStatus === "unknown") {
1470
+ return Object.freeze({
1471
+ kind: "owner-unknown",
1472
+ reason: "process ownership is unknown; restart repair must fail closed",
1473
+ });
1474
+ }
1475
+ if (journal.phase === "prepared") {
1476
+ return Object.freeze({
1477
+ kind: "safe-retry",
1478
+ reason: "the previous owner ended before the durable external begin boundary",
1479
+ });
1480
+ }
1481
+ return Object.freeze({
1482
+ kind: "reconciliation-required",
1483
+ reason: "the durable external begin boundary was crossed without a resolved outcome",
1484
+ });
1485
+ }
1486
+
1487
+ export function repairInterruptedLinkedDeviceLifecycleJournal(
1488
+ currentValue: LinkedDeviceLifecycleJournalSnapshot,
1489
+ options: {
1490
+ readonly owner: LinkedDeviceLifecycleOwner;
1491
+ readonly at: string;
1492
+ readonly environment?: Environment;
1493
+ },
1494
+ ): LinkedDeviceLifecycleJournalSnapshot {
1495
+ const current = assertSnapshot(currentValue);
1496
+ const at = canonicalTimestamp(
1497
+ options.at,
1498
+ "linked-device lifecycle repair time",
1499
+ );
1500
+ assertTimestampOrder(
1501
+ current.journal.updatedAt,
1502
+ at,
1503
+ "linked-device lifecycle repair time",
1504
+ );
1505
+ const repairOwner = requireExactLiveOwner(options.owner);
1506
+ assertTimestampOrder(
1507
+ at,
1508
+ repairOwner.leaseUntil,
1509
+ "linked-device lifecycle repair owner lease",
1510
+ );
1511
+ const disposition = classifyLinkedDeviceLifecycleRestart(
1512
+ current.journal,
1513
+ processOwnerStatus,
1514
+ );
1515
+ if (
1516
+ disposition.kind !== "safe-retry"
1517
+ && disposition.kind !== "reconciliation-required"
1518
+ ) {
1519
+ throw new Error(`linked-device lifecycle cannot be repaired: ${disposition.reason}`);
1520
+ }
1521
+ const revision = nextRevision(current.journal);
1522
+ const next = disposition.kind === "safe-retry"
1523
+ ? parseLinkedDeviceLifecycleJournal({
1524
+ ...current.journal,
1525
+ revision,
1526
+ phase: "terminal",
1527
+ status: "safe-retry",
1528
+ owner: repairOwner,
1529
+ finishedAt: at,
1530
+ updatedAt: at,
1531
+ reasonCode: "owner-exited-before-begin",
1532
+ })
1533
+ : parseLinkedDeviceLifecycleJournal({
1534
+ ...current.journal,
1535
+ revision,
1536
+ phase: "terminal",
1537
+ status: "indeterminate",
1538
+ reconciliation: "required",
1539
+ owner: repairOwner,
1540
+ finishedAt: current.journal.finishedAt ?? at,
1541
+ updatedAt: at,
1542
+ reasonCode: current.journal.status === "indeterminate"
1543
+ ? current.journal.reasonCode
1544
+ : "owner-exited-after-begin",
1545
+ });
1546
+ const written = writePrivateJsonIfUnchanged(
1547
+ journalPath(
1548
+ current.journal.journalId,
1549
+ options.environment ?? process.env,
1550
+ ),
1551
+ next,
1552
+ { expectedCurrentContentSha256: current.contentSha256 },
1553
+ );
1554
+ if (!written) {
1555
+ throw new Error(
1556
+ "linked-device lifecycle journal changed concurrently; reload before repair",
1557
+ );
1558
+ }
1559
+ return snapshot(next);
1560
+ }