@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,1602 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ closeSync,
4
+ constants,
5
+ fstatSync,
6
+ lstatSync,
7
+ openSync,
8
+ realpathSync,
9
+ } from "node:fs";
10
+
11
+ import {
12
+ linkedDeviceRealmKey,
13
+ loadAuthSnapshot,
14
+ normalizeAuthSubject,
15
+ replaceAuthIfUnchanged,
16
+ type AuthReplaceResult,
17
+ type AuthSnapshot,
18
+ type GhostgetAuth,
19
+ } from "./auth";
20
+ import {
21
+ linkedDeviceLifecycleAdmissionStore,
22
+ type LinkedDeviceLifecycleAdmission,
23
+ type LinkedDeviceLifecycleAdmissionStore,
24
+ } from "./linked-device-lifecycle-admission";
25
+ import {
26
+ classifyLinkedDeviceLifecycleRestart,
27
+ createLinkedDeviceLifecycleJournal,
28
+ createLinkedDeviceLifecycleOwner,
29
+ initialLinkedDeviceLifecycleJournal,
30
+ listLinkedDeviceLifecycleJournalSnapshots,
31
+ repairInterruptedLinkedDeviceLifecycleJournal,
32
+ updateLinkedDeviceLifecycleJournal,
33
+ type LinkedDeviceLifecycleJournal,
34
+ type LinkedDeviceLifecycleJournalEvent,
35
+ type LinkedDeviceLifecycleJournalListEntry,
36
+ type LinkedDeviceLifecycleJournalSnapshot,
37
+ type LinkedDeviceLifecycleOwner,
38
+ type LinkedDeviceLifecycleRestartDisposition,
39
+ type LinkedDeviceLifecycleResult,
40
+ } from "./linked-device-lifecycle-journal";
41
+ import { canonicalJson } from "./canonical-json";
42
+ import { OperationDeadlineError } from "./operation-deadline";
43
+ import { currentProcessStartIdentity } from "./process-identity";
44
+ import type {
45
+ LinkedDevicePluginBindingV1,
46
+ ProviderPluginLinkedDeviceAttemptBoundaryV1,
47
+ ProviderPluginLinkedDeviceSyncResultV1,
48
+ ProviderPluginV1,
49
+ } from "./provider-plugin";
50
+ import { requireProviderPluginAuth } from "./provider-plugin-auth";
51
+ import type { ProviderPluginRegistry } from "./provider-plugin-registry";
52
+
53
+ type Environment = Readonly<Record<string, string | undefined>>;
54
+ type LinkedDeviceAuth = Extract<
55
+ GhostgetAuth,
56
+ { readonly kind: "linked-device-store" }
57
+ >;
58
+
59
+ const sha256Pattern = /^[a-f0-9]{64}$/u;
60
+ const OWNER_LEASE_MILLISECONDS = 30 * 60_000;
61
+
62
+ export type LinkedDeviceLifecycleAuthSnapshot = AuthSnapshot;
63
+ export type LinkedDeviceLifecycleAuthReplaceResult = AuthReplaceResult;
64
+
65
+ /**
66
+ * The concrete implementation is supplied by auth.ts. Keeping this seam
67
+ * explicit makes the runtime unable to fall back to an unconditional write.
68
+ */
69
+ export type LinkedDeviceLifecycleAuthStore = {
70
+ readonly loadSnapshot: (
71
+ authId: string,
72
+ environment: Environment,
73
+ ) => LinkedDeviceLifecycleAuthSnapshot;
74
+ readonly replaceIfUnchanged: (
75
+ current: LinkedDeviceLifecycleAuthSnapshot,
76
+ replacement: LinkedDeviceAuth,
77
+ environment: Environment,
78
+ authority: {
79
+ readonly lifecycleAdmission: LinkedDeviceLifecycleAdmission;
80
+ },
81
+ ) => LinkedDeviceLifecycleAuthReplaceResult;
82
+ };
83
+
84
+ const defaultAuthStore: LinkedDeviceLifecycleAuthStore = Object.freeze({
85
+ loadSnapshot: loadAuthSnapshot,
86
+ replaceIfUnchanged: replaceAuthIfUnchanged,
87
+ });
88
+
89
+ export type LinkedDeviceLifecycleJournalStore = {
90
+ readonly createOwner: (leaseUntil: string) => LinkedDeviceLifecycleOwner;
91
+ readonly create: (
92
+ journal: LinkedDeviceLifecycleJournal,
93
+ environment: Environment,
94
+ ) => LinkedDeviceLifecycleJournalSnapshot;
95
+ readonly update: (
96
+ current: LinkedDeviceLifecycleJournalSnapshot,
97
+ event: LinkedDeviceLifecycleJournalEvent,
98
+ options: {
99
+ readonly owner: LinkedDeviceLifecycleOwner;
100
+ readonly environment: Environment;
101
+ },
102
+ ) => LinkedDeviceLifecycleJournalSnapshot;
103
+ readonly list: (
104
+ environment: Environment,
105
+ ) => readonly LinkedDeviceLifecycleJournalListEntry[];
106
+ readonly classifyRestart: (
107
+ journal: LinkedDeviceLifecycleJournal,
108
+ ) => LinkedDeviceLifecycleRestartDisposition;
109
+ readonly repairInterrupted: (
110
+ current: LinkedDeviceLifecycleJournalSnapshot,
111
+ options: {
112
+ readonly owner: LinkedDeviceLifecycleOwner;
113
+ readonly at: string;
114
+ readonly environment: Environment;
115
+ },
116
+ ) => LinkedDeviceLifecycleJournalSnapshot;
117
+ };
118
+
119
+ const defaultJournalStore: LinkedDeviceLifecycleJournalStore = Object.freeze({
120
+ createOwner: createLinkedDeviceLifecycleOwner,
121
+ create: createLinkedDeviceLifecycleJournal,
122
+ update: updateLinkedDeviceLifecycleJournal,
123
+ list: listLinkedDeviceLifecycleJournalSnapshots,
124
+ classifyRestart: classifyLinkedDeviceLifecycleRestart,
125
+ repairInterrupted: repairInterruptedLinkedDeviceLifecycleJournal,
126
+ });
127
+
128
+ export type LinkedDeviceLifecycleRecoveryIssue = {
129
+ readonly journalId: string;
130
+ readonly authId: string | null;
131
+ readonly kind:
132
+ | "invalid-journal"
133
+ | "owner-unknown"
134
+ | "reconciliation-required"
135
+ | "repair-conflict";
136
+ };
137
+
138
+ export type LinkedDeviceLifecycleRecoveryReport = {
139
+ readonly scanned: number;
140
+ readonly complete: number;
141
+ readonly live: number;
142
+ readonly repairedSafeRetry: number;
143
+ readonly repairedIndeterminate: number;
144
+ readonly invalid: number;
145
+ readonly blockedAuthIds: readonly string[];
146
+ readonly blockedRealmKeys: readonly string[];
147
+ readonly issues: readonly LinkedDeviceLifecycleRecoveryIssue[];
148
+ };
149
+
150
+ export type LinkedDevicePairLifecycleResult = {
151
+ readonly journalId: string;
152
+ readonly subject: string;
153
+ };
154
+
155
+ export type LinkedDeviceSyncLifecycleResult = {
156
+ readonly journalId: string;
157
+ readonly result: ProviderPluginLinkedDeviceSyncResultV1;
158
+ };
159
+
160
+ export type LinkedDeviceLifecycleReconciliationInput =
161
+ | {
162
+ readonly outcome: "not-applied";
163
+ readonly evidenceHash: string;
164
+ }
165
+ | {
166
+ readonly outcome: "applied";
167
+ readonly evidenceHash: string;
168
+ readonly result:
169
+ | {
170
+ readonly kind: "pair";
171
+ readonly subject: string;
172
+ }
173
+ | LinkedDeviceSyncResultInput;
174
+ };
175
+
176
+ type LinkedDeviceSyncResultInput = {
177
+ readonly kind: "sync";
178
+ readonly itemsStored: number;
179
+ readonly projection: "linked-device-local-store";
180
+ readonly emitsProtocolAcknowledgements: true;
181
+ };
182
+
183
+ export type ReconcileLinkedDeviceLifecycleResult = {
184
+ readonly ok: true;
185
+ readonly kind: "linked-device-lifecycle-reconciliation";
186
+ readonly journalId: string;
187
+ readonly authId: string;
188
+ readonly outcome: "applied" | "not-applied";
189
+ readonly status: "succeeded" | "safe-retry";
190
+ readonly reconciliation: "resolved-applied" | "resolved-not-applied";
191
+ readonly evidenceHash: string;
192
+ };
193
+
194
+ export type ReconcileLinkedDeviceLifecycleOptions = RuntimeBaseOptions;
195
+
196
+ export class LinkedDeviceLifecycleIndeterminateError extends Error {
197
+ readonly journalId: string;
198
+
199
+ constructor(journalId: string, cause: unknown) {
200
+ super(
201
+ `linked-device lifecycle attempt ${journalId} has an indeterminate external outcome; inspect it with ghostget doctor before retrying`,
202
+ { cause },
203
+ );
204
+ this.name = "LinkedDeviceLifecycleIndeterminateError";
205
+ this.journalId = journalId;
206
+ }
207
+ }
208
+
209
+ type RuntimeBaseOptions = {
210
+ readonly registry: ProviderPluginRegistry;
211
+ readonly authStore?: LinkedDeviceLifecycleAuthStore;
212
+ readonly admissionStore?: LinkedDeviceLifecycleAdmissionStore;
213
+ readonly environment?: Environment;
214
+ readonly journalStore?: LinkedDeviceLifecycleJournalStore;
215
+ readonly now?: () => Date;
216
+ readonly createJournalId?: () => string;
217
+ readonly signal?: AbortSignal;
218
+ };
219
+
220
+ export type RunLinkedDevicePairLifecycleOptions = RuntimeBaseOptions & {
221
+ readonly phone?: string;
222
+ readonly invokePair?: (
223
+ binding: LinkedDevicePluginBindingV1,
224
+ auth: LinkedDeviceAuth,
225
+ options: {
226
+ readonly phone?: string;
227
+ readonly environment: Environment;
228
+ readonly attempt: ProviderPluginLinkedDeviceAttemptBoundaryV1;
229
+ },
230
+ ) => Promise<string>;
231
+ };
232
+
233
+ export type RunLinkedDeviceSyncLifecycleOptions = RuntimeBaseOptions & {
234
+ readonly invokeSyncOnce?: (
235
+ binding: LinkedDevicePluginBindingV1,
236
+ auth: LinkedDeviceAuth,
237
+ options: {
238
+ readonly environment: Environment;
239
+ readonly attempt: ProviderPluginLinkedDeviceAttemptBoundaryV1;
240
+ },
241
+ ) => Promise<ProviderPluginLinkedDeviceSyncResultV1>;
242
+ };
243
+
244
+ function digestBytes(value: string): string {
245
+ return createHash("sha256").update(value, "utf8").digest("hex");
246
+ }
247
+
248
+ function canonicalAuthContentHash(auth: LinkedDeviceAuth): string {
249
+ return digestBytes(`${canonicalJson(auth)}\n`);
250
+ }
251
+
252
+ function linkedDeviceStoreIdentity(path: string): string | null {
253
+ let descriptor: number;
254
+ try {
255
+ descriptor = openSync(
256
+ path,
257
+ constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
258
+ );
259
+ } catch (error) {
260
+ if (
261
+ typeof error === "object"
262
+ && error !== null
263
+ && "code" in error
264
+ && error.code === "ENOENT"
265
+ ) {
266
+ return null;
267
+ }
268
+ throw error;
269
+ }
270
+ try {
271
+ const bound = fstatSync(descriptor, { bigint: true });
272
+ const current = lstatSync(path, { bigint: true });
273
+ if (
274
+ !bound.isDirectory()
275
+ || current.isSymbolicLink()
276
+ || !current.isDirectory()
277
+ || bound.dev !== current.dev
278
+ || bound.ino !== current.ino
279
+ ) {
280
+ throw new Error(
281
+ "linked-device store must remain one bound canonical physical directory",
282
+ );
283
+ }
284
+ if (realpathSync(path) !== path) {
285
+ throw new Error(
286
+ "linked-device store path changed from its canonical physical locator",
287
+ );
288
+ }
289
+ return `${bound.dev.toString(16)}:${bound.ino.toString(16)}`;
290
+ } finally {
291
+ closeSync(descriptor);
292
+ }
293
+ }
294
+
295
+ function assertLinkedDeviceStoreIdentityUnchanged(
296
+ expected: string | null,
297
+ path: string,
298
+ ): void {
299
+ const actual = linkedDeviceStoreIdentity(path);
300
+ if (expected === null || actual !== expected) {
301
+ throw new Error(
302
+ "linked-device store physical identity changed during its external lifecycle",
303
+ );
304
+ }
305
+ }
306
+
307
+ function bindLinkedDeviceStoreAtExternalBoundary(
308
+ auth: LinkedDeviceAuth,
309
+ admission: LinkedDeviceLifecycleAdmission,
310
+ expectedIdentity: string | null,
311
+ ): string {
312
+ if (linkedDeviceRealmKey(auth) !== admission.realmKey) {
313
+ throw new Error(
314
+ "linked-device auth physical realm changed before external dispatch",
315
+ );
316
+ }
317
+ const atBoundary = linkedDeviceStoreIdentity(auth.path);
318
+ if (atBoundary === null) {
319
+ throw new Error(
320
+ "linked-device store must exist before external dispatch begins",
321
+ );
322
+ }
323
+ if (expectedIdentity !== null && atBoundary !== expectedIdentity) {
324
+ throw new Error(
325
+ "linked-device store physical identity changed before external dispatch",
326
+ );
327
+ }
328
+ return atBoundary;
329
+ }
330
+
331
+ function assertAuthSnapshot(
332
+ value: LinkedDeviceLifecycleAuthSnapshot,
333
+ expectedId: string,
334
+ ): AuthSnapshot & { readonly auth: LinkedDeviceAuth } {
335
+ if (
336
+ value.auth.kind !== "linked-device-store"
337
+ || value.auth.id !== expectedId
338
+ || !sha256Pattern.test(value.contentSha256)
339
+ || canonicalAuthContentHash(value.auth) !== value.contentSha256
340
+ ) {
341
+ throw new Error(
342
+ "linked-device auth snapshot is malformed or not content-bound",
343
+ );
344
+ }
345
+ return Object.freeze({
346
+ auth: value.auth,
347
+ contentSha256: value.contentSha256,
348
+ });
349
+ }
350
+
351
+ function timestamp(now: () => Date): string {
352
+ const value = now();
353
+ if (
354
+ !(value instanceof Date)
355
+ || !Number.isFinite(value.valueOf())
356
+ ) {
357
+ throw new Error("linked-device lifecycle clock returned an invalid time");
358
+ }
359
+ return value.toISOString();
360
+ }
361
+
362
+ function ownerFor(
363
+ journalStore: LinkedDeviceLifecycleJournalStore,
364
+ at: string,
365
+ ): LinkedDeviceLifecycleOwner {
366
+ return journalStore.createOwner(
367
+ new Date(Date.parse(at) + OWNER_LEASE_MILLISECONDS).toISOString(),
368
+ );
369
+ }
370
+
371
+ function ownerPlugin(
372
+ registry: ProviderPluginRegistry,
373
+ binding: LinkedDevicePluginBindingV1,
374
+ ): ProviderPluginV1 {
375
+ const owners = registry.list().filter((plugin) =>
376
+ plugin.bindings.includes(binding));
377
+ if (owners.length !== 1 || owners[0] === undefined) {
378
+ throw new Error(
379
+ "linked-device lifecycle binding has no unique registered plugin owner",
380
+ );
381
+ }
382
+ return owners[0];
383
+ }
384
+
385
+ function lifecycleFor(binding: LinkedDevicePluginBindingV1) {
386
+ if (binding.linkedDeviceLifecycle === undefined) {
387
+ throw new Error(
388
+ `provider plugin surface ${binding.surfaceId} has no linked-device lifecycle`,
389
+ );
390
+ }
391
+ return binding.linkedDeviceLifecycle;
392
+ }
393
+
394
+ function validateBindingAuth(
395
+ binding: LinkedDevicePluginBindingV1,
396
+ auth: LinkedDeviceAuth,
397
+ ): void {
398
+ requireProviderPluginAuth(binding, auth);
399
+ if (auth.provider !== binding.surfaceId) {
400
+ throw new Error("linked-device auth provider does not match its plugin");
401
+ }
402
+ }
403
+
404
+ function createPreparedJournal(
405
+ kind: "pair" | "sync-once",
406
+ binding: LinkedDevicePluginBindingV1,
407
+ snapshot: AuthSnapshot & { readonly auth: LinkedDeviceAuth },
408
+ phoneProvided: boolean,
409
+ admission: LinkedDeviceLifecycleAdmission,
410
+ options: RuntimeBaseOptions,
411
+ ): {
412
+ readonly owner: LinkedDeviceLifecycleOwner;
413
+ readonly journal: LinkedDeviceLifecycleJournalSnapshot;
414
+ } {
415
+ const environment = options.environment ?? process.env;
416
+ const journalStore = options.journalStore ?? defaultJournalStore;
417
+ const startedAt = admission.acquiredAt;
418
+ const owner = admission.owner;
419
+ if (
420
+ admission.authId !== snapshot.auth.id
421
+ || admission.realmKey !== linkedDeviceRealmKey(snapshot.auth)
422
+ ) {
423
+ throw new Error(
424
+ "linked-device lifecycle admission does not match the admitted auth realm",
425
+ );
426
+ }
427
+ const plugin = ownerPlugin(options.registry, binding);
428
+ if (plugin.sourceKind === "portable") {
429
+ throw new Error(
430
+ "portable provider plugins do not support linked-device lifecycle execution",
431
+ );
432
+ }
433
+ const journal = initialLinkedDeviceLifecycleJournal({
434
+ journalId: (options.createJournalId ?? randomUUID)(),
435
+ kind,
436
+ pluginId: plugin.id,
437
+ pluginVersion: plugin.version,
438
+ pluginImplementationHash: options.registry
439
+ .implementationHash(binding)
440
+ .toString("hex"),
441
+ lifecycleContractVersion: 1,
442
+ surfaceId: binding.surfaceId,
443
+ authId: snapshot.auth.id,
444
+ authRealmHash: admission.realmKey,
445
+ authContentHash: snapshot.contentSha256,
446
+ initialSubjectState: snapshot.auth.subject === undefined
447
+ ? "unbound"
448
+ : "bound",
449
+ phoneProvided,
450
+ owner,
451
+ startedAt,
452
+ });
453
+ return Object.freeze({
454
+ owner,
455
+ journal: journalStore.create(journal, environment),
456
+ });
457
+ }
458
+
459
+ async function withLifecycleAdmission<T>(
460
+ authId: string,
461
+ options: RuntimeBaseOptions,
462
+ operation: (
463
+ admission: LinkedDeviceLifecycleAdmission,
464
+ authSnapshot: AuthSnapshot & { readonly auth: LinkedDeviceAuth },
465
+ ) => Promise<T> | T,
466
+ ): Promise<T> {
467
+ const environment = options.environment ?? process.env;
468
+ const now = options.now ?? (() => new Date());
469
+ const authStore = options.authStore ?? defaultAuthStore;
470
+ const admissionStore = options.admissionStore
471
+ ?? linkedDeviceLifecycleAdmissionStore;
472
+ const beforeAdmission = assertAuthSnapshot(
473
+ authStore.loadSnapshot(authId, environment),
474
+ authId,
475
+ );
476
+ const realmKey = linkedDeviceRealmKey(beforeAdmission.auth);
477
+ const recovery = admissionStore.recover(environment);
478
+ if (recovery.issues.length > 0) {
479
+ throw new Error(
480
+ "linked-device lifecycle admissions contain unresolved state; inspect ghostget doctor before continuing",
481
+ );
482
+ }
483
+ const admission = admissionStore.acquire(
484
+ realmKey,
485
+ authId,
486
+ timestamp(now),
487
+ environment,
488
+ );
489
+ let result: T;
490
+ try {
491
+ const admittedSnapshot = assertAuthSnapshot(
492
+ authStore.loadSnapshot(authId, environment),
493
+ authId,
494
+ );
495
+ if (
496
+ admittedSnapshot.contentSha256 !== beforeAdmission.contentSha256
497
+ || linkedDeviceRealmKey(admittedSnapshot.auth) !== realmKey
498
+ ) {
499
+ throw new Error(
500
+ `auth locator ${authId} changed while its linked-device realm admission was acquired`,
501
+ );
502
+ }
503
+ result = await operation(admission, admittedSnapshot);
504
+ } catch (error) {
505
+ try {
506
+ admission.release();
507
+ } catch (releaseError) {
508
+ throw new AggregateError(
509
+ [error, releaseError],
510
+ "linked-device lifecycle failed and its admission could not be released",
511
+ );
512
+ }
513
+ throw error;
514
+ }
515
+ admission.release();
516
+ return result;
517
+ }
518
+
519
+ function postBeginReason(
520
+ error: unknown,
521
+ signal: AbortSignal | undefined,
522
+ externalReturned: boolean,
523
+ ):
524
+ | "runtime-error-after-begin"
525
+ | "cancelled-after-begin"
526
+ | "deadline-after-begin"
527
+ | "external-returned-before-completion-persisted" {
528
+ if (externalReturned) {
529
+ return "external-returned-before-completion-persisted";
530
+ }
531
+ if (
532
+ error instanceof OperationDeadlineError
533
+ && error.failure === "timed-out"
534
+ ) return "deadline-after-begin";
535
+ if (
536
+ signal?.aborted === true
537
+ || (
538
+ error instanceof OperationDeadlineError
539
+ && error.failure === "cancelled"
540
+ )
541
+ ) return "cancelled-after-begin";
542
+ return "runtime-error-after-begin";
543
+ }
544
+
545
+ function failLifecycle(
546
+ current: LinkedDeviceLifecycleJournalSnapshot,
547
+ owner: LinkedDeviceLifecycleOwner,
548
+ error: unknown,
549
+ options: RuntimeBaseOptions,
550
+ externalReturned = false,
551
+ ): never {
552
+ const environment = options.environment ?? process.env;
553
+ const journalStore = options.journalStore ?? defaultJournalStore;
554
+ const now = options.now ?? (() => new Date());
555
+ const began = current.journal.phase !== "prepared";
556
+ const indeterminate = began || externalReturned;
557
+ try {
558
+ journalStore.update(
559
+ current,
560
+ indeterminate
561
+ ? {
562
+ type: "outcome-not-durable",
563
+ reasonCode: postBeginReason(
564
+ error,
565
+ options.signal,
566
+ externalReturned,
567
+ ),
568
+ at: timestamp(now),
569
+ }
570
+ : {
571
+ type: "aborted-before-external",
572
+ reasonCode: options.signal?.aborted === true
573
+ ? "cancelled-before-begin"
574
+ : "preflight-failed",
575
+ at: timestamp(now),
576
+ },
577
+ { owner, environment },
578
+ );
579
+ } catch {
580
+ // The original failure remains primary. Restart classification handles any
581
+ // journal that could not be terminalized here.
582
+ }
583
+ if (indeterminate) {
584
+ throw new LinkedDeviceLifecycleIndeterminateError(
585
+ current.journal.journalId,
586
+ error,
587
+ );
588
+ }
589
+ throw error;
590
+ }
591
+
592
+ function boundaryFor(
593
+ initial: LinkedDeviceLifecycleJournalSnapshot,
594
+ owner: LinkedDeviceLifecycleOwner,
595
+ options: RuntimeBaseOptions,
596
+ getCurrent: () => LinkedDeviceLifecycleJournalSnapshot,
597
+ setCurrent: (value: LinkedDeviceLifecycleJournalSnapshot) => void,
598
+ bindExternalStore: () => void,
599
+ ): {
600
+ readonly attempt: ProviderPluginLinkedDeviceAttemptBoundaryV1;
601
+ readonly wasCrossed: () => boolean;
602
+ readonly crossConservatively: () => Promise<void>;
603
+ } {
604
+ const environment = options.environment ?? process.env;
605
+ const journalStore = options.journalStore ?? defaultJournalStore;
606
+ const now = options.now ?? (() => new Date());
607
+ let beginPromise: Promise<void> | null = null;
608
+ const cross = (conservative: boolean): Promise<void> => {
609
+ if (beginPromise !== null) return beginPromise;
610
+ const pending = Promise.resolve().then(() => {
611
+ if (!conservative && options.signal?.aborted === true) {
612
+ throw new OperationDeadlineError(
613
+ "linked-device lifecycle",
614
+ "cancelled",
615
+ );
616
+ }
617
+ const current = getCurrent();
618
+ if (current.journal.phase !== "prepared") return;
619
+ if (!conservative) bindExternalStore();
620
+ setCurrent(journalStore.update(
621
+ current,
622
+ { type: "external-begin", at: timestamp(now) },
623
+ { owner, environment },
624
+ ));
625
+ if (conservative) bindExternalStore();
626
+ });
627
+ beginPromise = pending.catch((error: unknown) => {
628
+ beginPromise = null;
629
+ throw error;
630
+ });
631
+ return beginPromise;
632
+ };
633
+ return Object.freeze({
634
+ attempt: Object.freeze({
635
+ journalId: initial.journal.journalId,
636
+ beforeExternalBegin: () => cross(false),
637
+ }),
638
+ wasCrossed: () => getCurrent().journal.phase !== "prepared",
639
+ crossConservatively: () => cross(true),
640
+ });
641
+ }
642
+
643
+ function recoveryIssue(
644
+ journalId: string,
645
+ authId: string | null,
646
+ kind: LinkedDeviceLifecycleRecoveryIssue["kind"],
647
+ ): LinkedDeviceLifecycleRecoveryIssue {
648
+ return Object.freeze({ journalId, authId, kind });
649
+ }
650
+
651
+ /**
652
+ * Classify and repair only local journal state. This never invokes a plugin,
653
+ * probes a provider, retries an external action, or claims reconciliation.
654
+ */
655
+ export function recoverLinkedDeviceLifecycleJournals(
656
+ options: {
657
+ readonly environment?: Environment;
658
+ readonly journalStore?: LinkedDeviceLifecycleJournalStore;
659
+ readonly now?: () => Date;
660
+ } = {},
661
+ ): LinkedDeviceLifecycleRecoveryReport {
662
+ const environment = options.environment ?? process.env;
663
+ const journalStore = options.journalStore ?? defaultJournalStore;
664
+ const now = options.now ?? (() => new Date());
665
+ const entries = journalStore.list(environment);
666
+ const issues: LinkedDeviceLifecycleRecoveryIssue[] = [];
667
+ const blockedAuthIds = new Set<string>();
668
+ const blockedRealmKeys = new Set<string>();
669
+ let complete = 0;
670
+ let live = 0;
671
+ let repairedSafeRetry = 0;
672
+ let repairedIndeterminate = 0;
673
+ let invalid = 0;
674
+ let repairOwner: LinkedDeviceLifecycleOwner | null = null;
675
+
676
+ for (const entry of entries) {
677
+ if ("invalid" in entry) {
678
+ invalid += 1;
679
+ issues.push(recoveryIssue(
680
+ entry.journalId,
681
+ null,
682
+ "invalid-journal",
683
+ ));
684
+ continue;
685
+ }
686
+ const journal = entry.journal;
687
+ if (journal.phase === "terminal") {
688
+ if (journal.status === "indeterminate") {
689
+ blockedAuthIds.add(journal.authId);
690
+ blockedRealmKeys.add(journal.authRealmHash);
691
+ issues.push(recoveryIssue(
692
+ journal.journalId,
693
+ journal.authId,
694
+ "reconciliation-required",
695
+ ));
696
+ } else {
697
+ complete += 1;
698
+ }
699
+ continue;
700
+ }
701
+ const disposition = journalStore.classifyRestart(journal);
702
+ if (disposition.kind === "complete") {
703
+ complete += 1;
704
+ continue;
705
+ }
706
+ if (disposition.kind === "live-owner") {
707
+ live += 1;
708
+ blockedAuthIds.add(journal.authId);
709
+ blockedRealmKeys.add(journal.authRealmHash);
710
+ continue;
711
+ }
712
+ if (disposition.kind === "owner-unknown") {
713
+ blockedAuthIds.add(journal.authId);
714
+ blockedRealmKeys.add(journal.authRealmHash);
715
+ issues.push(recoveryIssue(
716
+ journal.journalId,
717
+ journal.authId,
718
+ "owner-unknown",
719
+ ));
720
+ continue;
721
+ }
722
+ const at = timestamp(now);
723
+ repairOwner ??= ownerFor(journalStore, at);
724
+ try {
725
+ const repaired = journalStore.repairInterrupted(entry, {
726
+ owner: repairOwner,
727
+ at,
728
+ environment,
729
+ });
730
+ if (repaired.journal.status === "safe-retry") {
731
+ repairedSafeRetry += 1;
732
+ } else {
733
+ repairedIndeterminate += 1;
734
+ blockedAuthIds.add(repaired.journal.authId);
735
+ blockedRealmKeys.add(repaired.journal.authRealmHash);
736
+ issues.push(recoveryIssue(
737
+ repaired.journal.journalId,
738
+ repaired.journal.authId,
739
+ "reconciliation-required",
740
+ ));
741
+ }
742
+ } catch {
743
+ blockedAuthIds.add(journal.authId);
744
+ blockedRealmKeys.add(journal.authRealmHash);
745
+ issues.push(recoveryIssue(
746
+ journal.journalId,
747
+ journal.authId,
748
+ "repair-conflict",
749
+ ));
750
+ }
751
+ }
752
+
753
+ return Object.freeze({
754
+ scanned: entries.length,
755
+ complete,
756
+ live,
757
+ repairedSafeRetry,
758
+ repairedIndeterminate,
759
+ invalid,
760
+ blockedAuthIds: Object.freeze([...blockedAuthIds].sort()),
761
+ blockedRealmKeys: Object.freeze([...blockedRealmKeys].sort()),
762
+ issues: Object.freeze(issues),
763
+ });
764
+ }
765
+
766
+ function assertLifecycleMayStart(
767
+ authId: string,
768
+ realmKey: string,
769
+ options: RuntimeBaseOptions,
770
+ ): void {
771
+ const recovery = recoverLinkedDeviceLifecycleJournals(options);
772
+ if (recovery.invalid > 0) {
773
+ throw new Error(
774
+ "linked-device lifecycle journals contain invalid state; inspect ghostget doctor before continuing",
775
+ );
776
+ }
777
+ if (
778
+ recovery.blockedAuthIds.includes(authId)
779
+ || recovery.blockedRealmKeys.includes(realmKey)
780
+ ) {
781
+ throw new Error(
782
+ `auth locator ${authId} has an unresolved linked-device lifecycle; reconcile it before retrying`,
783
+ );
784
+ }
785
+ }
786
+
787
+ function validatePairSubject(
788
+ binding: LinkedDevicePluginBindingV1,
789
+ auth: LinkedDeviceAuth,
790
+ subject: string,
791
+ ): string {
792
+ const normalized = normalizeAuthSubject(subject);
793
+ if (!binding.subject.matches(normalized)) {
794
+ throw new Error(
795
+ `provider plugin surface ${binding.surfaceId} returned a subject outside ${binding.subject.format}`,
796
+ );
797
+ }
798
+ if (auth.subject !== undefined && auth.subject !== normalized) {
799
+ throw new Error(
800
+ "paired linked-device account did not match the auth realm's existing subject",
801
+ );
802
+ }
803
+ return normalized;
804
+ }
805
+
806
+ function journalSyncResult(
807
+ value: ProviderPluginLinkedDeviceSyncResultV1,
808
+ ): LinkedDeviceLifecycleResult {
809
+ if (
810
+ !Number.isSafeInteger(value.itemsStored)
811
+ || value.itemsStored < 0
812
+ || value.itemsStored > 1_000_000_000
813
+ || typeof value.projection !== "string"
814
+ || value.projection.length < 1
815
+ || value.projection.length > 256
816
+ || value.emitsProtocolAcknowledgements !== true
817
+ ) {
818
+ throw new Error(
819
+ "linked-device plugin returned an invalid one-shot sync result",
820
+ );
821
+ }
822
+ return Object.freeze({
823
+ kind: "sync",
824
+ itemsStored: value.itemsStored,
825
+ projection: "linked-device-local-store",
826
+ emitsProtocolAcknowledgements: true,
827
+ });
828
+ }
829
+
830
+ function strictReconciliationRecord(
831
+ value: unknown,
832
+ expectedKeys: readonly string[],
833
+ label: string,
834
+ ): Readonly<Record<string, unknown>> {
835
+ if (
836
+ typeof value !== "object"
837
+ || value === null
838
+ || Array.isArray(value)
839
+ || (
840
+ Object.getPrototypeOf(value) !== Object.prototype
841
+ && Object.getPrototypeOf(value) !== null
842
+ )
843
+ ) {
844
+ throw new Error(`${label} must be a plain object`);
845
+ }
846
+ const keys = Reflect.ownKeys(value);
847
+ if (
848
+ keys.length !== expectedKeys.length
849
+ || keys.some((key) =>
850
+ typeof key !== "string" || !expectedKeys.includes(key))
851
+ ) {
852
+ throw new Error(`${label} has unsupported fields`);
853
+ }
854
+ const record: Record<string, unknown> = {};
855
+ for (const key of expectedKeys) {
856
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
857
+ if (
858
+ descriptor === undefined
859
+ || !descriptor.enumerable
860
+ || !("value" in descriptor)
861
+ ) {
862
+ throw new Error(`${label} has unsupported accessor fields`);
863
+ }
864
+ record[key] = descriptor.value;
865
+ }
866
+ return record;
867
+ }
868
+
869
+ export function parseLinkedDeviceLifecycleReconciliationInput(
870
+ value: unknown,
871
+ ): LinkedDeviceLifecycleReconciliationInput {
872
+ const base = strictReconciliationRecord(
873
+ value,
874
+ typeof value === "object"
875
+ && value !== null
876
+ && !Array.isArray(value)
877
+ && Object.prototype.hasOwnProperty.call(value, "result")
878
+ ? ["outcome", "evidenceHash", "result"]
879
+ : ["outcome", "evidenceHash"],
880
+ "linked-device lifecycle reconciliation input",
881
+ );
882
+ if (
883
+ typeof base.evidenceHash !== "string"
884
+ || !sha256Pattern.test(base.evidenceHash)
885
+ ) {
886
+ throw new Error(
887
+ "linked-device lifecycle reconciliation evidence hash is malformed",
888
+ );
889
+ }
890
+ if (base.outcome === "not-applied") {
891
+ if ("result" in base) {
892
+ throw new Error(
893
+ "not-applied linked-device reconciliation must not include a result",
894
+ );
895
+ }
896
+ return Object.freeze({
897
+ outcome: "not-applied",
898
+ evidenceHash: base.evidenceHash,
899
+ });
900
+ }
901
+ if (base.outcome !== "applied" || !("result" in base)) {
902
+ throw new Error(
903
+ "linked-device lifecycle reconciliation outcome is malformed",
904
+ );
905
+ }
906
+ const resultValue = base.result;
907
+ if (
908
+ typeof resultValue === "object"
909
+ && resultValue !== null
910
+ && !Array.isArray(resultValue)
911
+ && Object.prototype.hasOwnProperty.call(resultValue, "kind")
912
+ && Object.getOwnPropertyDescriptor(resultValue, "kind")?.value === "pair"
913
+ ) {
914
+ const result = strictReconciliationRecord(
915
+ resultValue,
916
+ ["kind", "subject"],
917
+ "linked-device pair reconciliation result",
918
+ );
919
+ if (result.kind !== "pair" || typeof result.subject !== "string") {
920
+ throw new Error("linked-device pair reconciliation result is malformed");
921
+ }
922
+ return Object.freeze({
923
+ outcome: "applied",
924
+ evidenceHash: base.evidenceHash,
925
+ result: Object.freeze({
926
+ kind: "pair",
927
+ subject: normalizeAuthSubject(result.subject),
928
+ }),
929
+ });
930
+ }
931
+ const result = strictReconciliationRecord(
932
+ resultValue,
933
+ ["kind", "itemsStored", "projection", "emitsProtocolAcknowledgements"],
934
+ "linked-device sync reconciliation result",
935
+ );
936
+ const parsed = journalSyncResult({
937
+ itemsStored: typeof result.itemsStored === "number"
938
+ ? result.itemsStored
939
+ : Number.NaN,
940
+ projection: typeof result.projection === "string"
941
+ ? result.projection
942
+ : "",
943
+ emitsProtocolAcknowledgements:
944
+ result.emitsProtocolAcknowledgements === true,
945
+ });
946
+ if (
947
+ result.kind !== "sync"
948
+ || result.projection !== "linked-device-local-store"
949
+ || parsed.kind !== "sync"
950
+ ) {
951
+ throw new Error("linked-device sync reconciliation result is malformed");
952
+ }
953
+ return Object.freeze({
954
+ outcome: "applied",
955
+ evidenceHash: base.evidenceHash,
956
+ result: Object.freeze({
957
+ kind: "sync",
958
+ itemsStored: parsed.itemsStored,
959
+ projection: parsed.projection,
960
+ emitsProtocolAcknowledgements: true,
961
+ }),
962
+ });
963
+ }
964
+
965
+ function currentProcessOwns(
966
+ owner: LinkedDeviceLifecycleOwner,
967
+ ): boolean {
968
+ const identity = currentProcessStartIdentity();
969
+ return owner.pid === process.pid
970
+ && owner.bootId === identity.bootId
971
+ && owner.processStartId === identity.processStartId;
972
+ }
973
+
974
+ function lifecycleSnapshotById(
975
+ journalId: string,
976
+ journalStore: LinkedDeviceLifecycleJournalStore,
977
+ environment: Environment,
978
+ ): LinkedDeviceLifecycleJournalSnapshot {
979
+ let found: LinkedDeviceLifecycleJournalSnapshot | null = null;
980
+ for (const entry of journalStore.list(environment)) {
981
+ if ("invalid" in entry) {
982
+ if (entry.journalId === journalId) {
983
+ throw new Error(
984
+ `linked-device lifecycle journal ${journalId} is invalid`,
985
+ );
986
+ }
987
+ continue;
988
+ }
989
+ if (entry.journal.journalId !== journalId) continue;
990
+ if (found !== null) {
991
+ throw new Error(
992
+ `linked-device lifecycle journal ${journalId} is duplicated`,
993
+ );
994
+ }
995
+ found = entry;
996
+ }
997
+ if (found === null) {
998
+ throw new Error(
999
+ `linked-device lifecycle journal ${journalId} was not found`,
1000
+ );
1001
+ }
1002
+ return found;
1003
+ }
1004
+
1005
+ function reconciliationResultView(
1006
+ journal: LinkedDeviceLifecycleJournal,
1007
+ input: LinkedDeviceLifecycleReconciliationInput,
1008
+ authSnapshot: AuthSnapshot & { readonly auth: LinkedDeviceAuth },
1009
+ binding: LinkedDevicePluginBindingV1,
1010
+ validateAppliedResult = true,
1011
+ ): ReconcileLinkedDeviceLifecycleResult {
1012
+ if (
1013
+ journal.phase !== "terminal"
1014
+ || (
1015
+ journal.reconciliation !== "resolved-applied"
1016
+ && journal.reconciliation !== "resolved-not-applied"
1017
+ )
1018
+ || journal.reconciliationHash !== input.evidenceHash
1019
+ || (
1020
+ input.outcome === "applied"
1021
+ ? journal.status !== "succeeded"
1022
+ || journal.reconciliation !== "resolved-applied"
1023
+ : journal.status !== "safe-retry"
1024
+ || journal.reconciliation !== "resolved-not-applied"
1025
+ )
1026
+ ) {
1027
+ throw new Error(
1028
+ "linked-device lifecycle is already resolved with a different reconciliation",
1029
+ );
1030
+ }
1031
+ if (input.outcome === "applied" && validateAppliedResult) {
1032
+ if (input.result.kind === "sync") {
1033
+ if (
1034
+ journal.kind !== "sync-once"
1035
+ || journal.result?.kind !== "sync"
1036
+ || journal.result.itemsStored !== input.result.itemsStored
1037
+ || journal.result.projection !== input.result.projection
1038
+ || journal.result.emitsProtocolAcknowledgements
1039
+ !== input.result.emitsProtocolAcknowledgements
1040
+ ) {
1041
+ throw new Error(
1042
+ "linked-device lifecycle is already resolved with a different reconciliation result",
1043
+ );
1044
+ }
1045
+ } else {
1046
+ const subject = validatePairSubject(
1047
+ binding,
1048
+ authSnapshot.auth,
1049
+ input.result.subject,
1050
+ );
1051
+ if (
1052
+ journal.kind !== "pair"
1053
+ || authSnapshot.auth.subject !== subject
1054
+ || journal.result?.kind !== "pair"
1055
+ || journal.result.resultingAuthContentHash
1056
+ !== authSnapshot.contentSha256
1057
+ ) {
1058
+ throw new Error(
1059
+ "linked-device lifecycle is already resolved with a different reconciliation result",
1060
+ );
1061
+ }
1062
+ }
1063
+ }
1064
+ return Object.freeze({
1065
+ ok: true,
1066
+ kind: "linked-device-lifecycle-reconciliation",
1067
+ journalId: journal.journalId,
1068
+ authId: journal.authId,
1069
+ outcome: input.outcome,
1070
+ status: input.outcome === "applied" ? "succeeded" : "safe-retry",
1071
+ reconciliation: input.outcome === "applied"
1072
+ ? "resolved-applied"
1073
+ : "resolved-not-applied",
1074
+ evidenceHash: input.evidenceHash,
1075
+ });
1076
+ }
1077
+
1078
+ /**
1079
+ * Discharge an indeterminate post-boundary lifecycle from explicit,
1080
+ * independently obtained evidence. This path never invokes the provider
1081
+ * lifecycle and never retries the effect-capable operation.
1082
+ */
1083
+ export async function reconcileLinkedDeviceLifecycleJournal(
1084
+ journalId: string,
1085
+ inputValue: unknown,
1086
+ options: ReconcileLinkedDeviceLifecycleOptions,
1087
+ ): Promise<ReconcileLinkedDeviceLifecycleResult> {
1088
+ const input = parseLinkedDeviceLifecycleReconciliationInput(inputValue);
1089
+ const environment = options.environment ?? process.env;
1090
+ const journalStore = options.journalStore ?? defaultJournalStore;
1091
+ const initial = lifecycleSnapshotById(
1092
+ journalId,
1093
+ journalStore,
1094
+ environment,
1095
+ );
1096
+ return withLifecycleAdmission(
1097
+ initial.journal.authId,
1098
+ options,
1099
+ (admission, authSnapshot) => {
1100
+ let current = lifecycleSnapshotById(
1101
+ journalId,
1102
+ journalStore,
1103
+ environment,
1104
+ );
1105
+ if (
1106
+ current.journal.authId !== authSnapshot.auth.id
1107
+ || current.journal.authRealmHash !== admission.realmKey
1108
+ ) {
1109
+ throw new Error(
1110
+ "linked-device lifecycle journal no longer matches its auth realm",
1111
+ );
1112
+ }
1113
+ const binding = (() => {
1114
+ const selected = options.registry.requireSessionRoute(
1115
+ current.journal.surfaceId,
1116
+ );
1117
+ if (selected.transport !== "linked-device") {
1118
+ throw new Error(
1119
+ "linked-device lifecycle journal surface is no longer linked-device",
1120
+ );
1121
+ }
1122
+ const plugin = ownerPlugin(options.registry, selected);
1123
+ if (
1124
+ plugin.id !== current.journal.pluginId
1125
+ || plugin.version !== current.journal.pluginVersion
1126
+ || options.registry.implementationHash(selected).toString("hex")
1127
+ !== current.journal.pluginImplementationHash
1128
+ ) {
1129
+ throw new Error(
1130
+ "linked-device lifecycle plugin implementation no longer matches its journal",
1131
+ );
1132
+ }
1133
+ return selected;
1134
+ })();
1135
+ validateBindingAuth(binding, authSnapshot.auth);
1136
+ if (current.journal.phase !== "terminal") {
1137
+ const disposition = journalStore.classifyRestart(current.journal);
1138
+ if (disposition.kind !== "reconciliation-required") {
1139
+ throw new Error(
1140
+ `linked-device lifecycle cannot be reconciled: ${disposition.reason}`,
1141
+ );
1142
+ }
1143
+ const at = timestamp(options.now ?? (() => new Date()));
1144
+ const repairOwner = ownerFor(journalStore, at);
1145
+ current = journalStore.repairInterrupted(current, {
1146
+ owner: repairOwner,
1147
+ at,
1148
+ environment,
1149
+ });
1150
+ }
1151
+ if (
1152
+ current.journal.phase === "terminal"
1153
+ && current.journal.status !== "indeterminate"
1154
+ ) {
1155
+ return reconciliationResultView(
1156
+ current.journal,
1157
+ input,
1158
+ authSnapshot,
1159
+ binding,
1160
+ );
1161
+ }
1162
+ if (
1163
+ current.journal.phase !== "terminal"
1164
+ || current.journal.status !== "indeterminate"
1165
+ || current.journal.reconciliation !== "required"
1166
+ ) {
1167
+ throw new Error(
1168
+ "linked-device lifecycle is not ready for explicit reconciliation",
1169
+ );
1170
+ }
1171
+
1172
+ let owner: LinkedDeviceLifecycleOwner;
1173
+ if (currentProcessOwns(current.journal.owner)) {
1174
+ owner = current.journal.owner;
1175
+ } else {
1176
+ const disposition = journalStore.classifyRestart(current.journal);
1177
+ if (disposition.kind !== "reconciliation-required") {
1178
+ throw new Error(
1179
+ `linked-device lifecycle cannot be reconciled: ${disposition.reason}`,
1180
+ );
1181
+ }
1182
+ const at = timestamp(options.now ?? (() => new Date()));
1183
+ owner = ownerFor(journalStore, at);
1184
+ current = journalStore.repairInterrupted(current, {
1185
+ owner,
1186
+ at,
1187
+ environment,
1188
+ });
1189
+ }
1190
+
1191
+ let journalResult: LinkedDeviceLifecycleResult | undefined;
1192
+ if (input.outcome === "not-applied") {
1193
+ if (
1194
+ current.journal.externalCompletedAt !== null
1195
+ || current.journal.result !== null
1196
+ || current.journal.reasonCode
1197
+ === "external-returned-before-completion-persisted"
1198
+ ) {
1199
+ throw new Error(
1200
+ "not-applied reconciliation contradicts durable linked-device external completion",
1201
+ );
1202
+ }
1203
+ if (authSnapshot.contentSha256 !== current.journal.authContentHash) {
1204
+ throw new Error(
1205
+ "linked-device auth changed since the indeterminate lifecycle began",
1206
+ );
1207
+ }
1208
+ } else if (input.result.kind === "sync") {
1209
+ if (
1210
+ current.journal.kind !== "sync-once"
1211
+ || authSnapshot.contentSha256 !== current.journal.authContentHash
1212
+ ) {
1213
+ throw new Error(
1214
+ "linked-device sync reconciliation contradicts its journal or auth snapshot",
1215
+ );
1216
+ }
1217
+ journalResult = input.result;
1218
+ } else {
1219
+ if (current.journal.kind !== "pair") {
1220
+ throw new Error(
1221
+ "linked-device pair reconciliation contradicts its journal",
1222
+ );
1223
+ }
1224
+ const subject = validatePairSubject(
1225
+ binding,
1226
+ authSnapshot.auth,
1227
+ input.result.subject,
1228
+ );
1229
+ const replacement = Object.freeze({
1230
+ ...authSnapshot.auth,
1231
+ subject,
1232
+ });
1233
+ const desiredHash = canonicalAuthContentHash(replacement);
1234
+ journalResult = Object.freeze({
1235
+ kind: "pair",
1236
+ resultingAuthContentHash: desiredHash,
1237
+ });
1238
+ if (
1239
+ current.journal.result !== null
1240
+ && (
1241
+ current.journal.result.kind !== "pair"
1242
+ || current.journal.result.resultingAuthContentHash !== desiredHash
1243
+ )
1244
+ ) {
1245
+ throw new Error(
1246
+ "linked-device pair reconciliation contradicts the external completion journal",
1247
+ );
1248
+ }
1249
+ if (authSnapshot.contentSha256 === current.journal.authContentHash) {
1250
+ const authStore = options.authStore ?? defaultAuthStore;
1251
+ const replacementResult = authStore.replaceIfUnchanged(
1252
+ authSnapshot,
1253
+ replacement,
1254
+ environment,
1255
+ { lifecycleAdmission: admission },
1256
+ );
1257
+ if (
1258
+ !replacementResult.replaced
1259
+ || replacementResult.snapshot.contentSha256 !== desiredHash
1260
+ ) {
1261
+ throw new Error(
1262
+ "linked-device pair reconciliation lost its auth commit CAS",
1263
+ );
1264
+ }
1265
+ } else if (authSnapshot.contentSha256 !== desiredHash) {
1266
+ throw new Error(
1267
+ "linked-device auth changed since the indeterminate pairing began",
1268
+ );
1269
+ }
1270
+ }
1271
+ const at = timestamp(options.now ?? (() => new Date()));
1272
+ current = journalStore.update(
1273
+ current,
1274
+ input.outcome === "applied"
1275
+ ? {
1276
+ type: "reconciled",
1277
+ outcome: "applied",
1278
+ evidenceHash: input.evidenceHash,
1279
+ result: journalResult
1280
+ ?? (() => {
1281
+ throw new Error(
1282
+ "applied linked-device reconciliation has no result",
1283
+ );
1284
+ })(),
1285
+ at,
1286
+ }
1287
+ : {
1288
+ type: "reconciled",
1289
+ outcome: "not-applied",
1290
+ evidenceHash: input.evidenceHash,
1291
+ at,
1292
+ },
1293
+ { owner, environment },
1294
+ );
1295
+ return reconciliationResultView(
1296
+ current.journal,
1297
+ input,
1298
+ authSnapshot,
1299
+ binding,
1300
+ false,
1301
+ );
1302
+ },
1303
+ );
1304
+ }
1305
+
1306
+ async function runLinkedDevicePairLifecycleAdmitted(
1307
+ binding: LinkedDevicePluginBindingV1,
1308
+ authId: string,
1309
+ options: RunLinkedDevicePairLifecycleOptions,
1310
+ admission: LinkedDeviceLifecycleAdmission,
1311
+ admittedAuthSnapshot: AuthSnapshot & { readonly auth: LinkedDeviceAuth },
1312
+ ): Promise<LinkedDevicePairLifecycleResult> {
1313
+ const environment = options.environment ?? process.env;
1314
+ const journalStore = options.journalStore ?? defaultJournalStore;
1315
+ const authStore = options.authStore ?? defaultAuthStore;
1316
+ const now = options.now ?? (() => new Date());
1317
+ assertLifecycleMayStart(authId, admission.realmKey, options);
1318
+ const authSnapshot = admittedAuthSnapshot;
1319
+ let storeIdentity = linkedDeviceStoreIdentity(authSnapshot.auth.path);
1320
+ validateBindingAuth(binding, authSnapshot.auth);
1321
+ lifecycleFor(binding);
1322
+ const prepared = createPreparedJournal(
1323
+ "pair",
1324
+ binding,
1325
+ authSnapshot,
1326
+ options.phone !== undefined,
1327
+ admission,
1328
+ options,
1329
+ );
1330
+ let current = prepared.journal;
1331
+ const boundary = boundaryFor(
1332
+ prepared.journal,
1333
+ prepared.owner,
1334
+ options,
1335
+ () => current,
1336
+ (value) => {
1337
+ current = value;
1338
+ },
1339
+ () => {
1340
+ storeIdentity = bindLinkedDeviceStoreAtExternalBoundary(
1341
+ authSnapshot.auth,
1342
+ admission,
1343
+ storeIdentity,
1344
+ );
1345
+ },
1346
+ );
1347
+ let externalReturned = false;
1348
+ try {
1349
+ if (options.signal?.aborted === true) {
1350
+ throw new OperationDeadlineError("linked-device pairing", "cancelled");
1351
+ }
1352
+ const invoke = options.invokePair
1353
+ ?? ((
1354
+ selectedBinding: LinkedDevicePluginBindingV1,
1355
+ auth: LinkedDeviceAuth,
1356
+ invocationOptions: {
1357
+ readonly phone?: string;
1358
+ readonly environment: Environment;
1359
+ readonly attempt: ProviderPluginLinkedDeviceAttemptBoundaryV1;
1360
+ },
1361
+ ) => lifecycleFor(selectedBinding).pair(auth, invocationOptions));
1362
+ const subject = await invoke(binding, authSnapshot.auth, {
1363
+ ...(options.phone === undefined ? {} : { phone: options.phone }),
1364
+ environment,
1365
+ attempt: boundary.attempt,
1366
+ });
1367
+ externalReturned = true;
1368
+ if (!boundary.wasCrossed()) {
1369
+ await boundary.crossConservatively();
1370
+ throw new Error(
1371
+ "linked-device plugin returned from pairing without crossing its durable external boundary",
1372
+ );
1373
+ }
1374
+ const normalizedSubject = validatePairSubject(
1375
+ binding,
1376
+ authSnapshot.auth,
1377
+ subject,
1378
+ );
1379
+ const replacement = Object.freeze({
1380
+ ...authSnapshot.auth,
1381
+ subject: normalizedSubject,
1382
+ });
1383
+ const result = Object.freeze({
1384
+ kind: "pair" as const,
1385
+ resultingAuthContentHash: canonicalAuthContentHash(replacement),
1386
+ });
1387
+ current = journalStore.update(
1388
+ current,
1389
+ { type: "external-complete", result, at: timestamp(now) },
1390
+ { owner: prepared.owner, environment },
1391
+ );
1392
+ assertLinkedDeviceStoreIdentityUnchanged(
1393
+ storeIdentity,
1394
+ authSnapshot.auth.path,
1395
+ );
1396
+ const replacementResult = authStore.replaceIfUnchanged(
1397
+ authSnapshot,
1398
+ replacement,
1399
+ environment,
1400
+ { lifecycleAdmission: admission },
1401
+ );
1402
+ if (!replacementResult.replaced) {
1403
+ throw new Error(
1404
+ "linked-device auth changed concurrently after external pairing",
1405
+ );
1406
+ }
1407
+ const written = assertAuthSnapshot(replacementResult.snapshot, authId);
1408
+ if (written.contentSha256 !== result.resultingAuthContentHash) {
1409
+ throw new Error(
1410
+ "linked-device auth replacement returned an unexpected content hash",
1411
+ );
1412
+ }
1413
+ current = journalStore.update(
1414
+ current,
1415
+ { type: "committed", result, at: timestamp(now) },
1416
+ { owner: prepared.owner, environment },
1417
+ );
1418
+ return Object.freeze({
1419
+ journalId: current.journal.journalId,
1420
+ subject: normalizedSubject,
1421
+ });
1422
+ } catch (error) {
1423
+ return failLifecycle(
1424
+ current,
1425
+ prepared.owner,
1426
+ error,
1427
+ options,
1428
+ externalReturned,
1429
+ );
1430
+ }
1431
+ }
1432
+
1433
+ async function runLinkedDeviceSyncOnceLifecycleAdmitted(
1434
+ binding: LinkedDevicePluginBindingV1,
1435
+ authId: string,
1436
+ options: RunLinkedDeviceSyncLifecycleOptions,
1437
+ admission: LinkedDeviceLifecycleAdmission,
1438
+ admittedAuthSnapshot: AuthSnapshot & { readonly auth: LinkedDeviceAuth },
1439
+ ): Promise<LinkedDeviceSyncLifecycleResult> {
1440
+ const environment = options.environment ?? process.env;
1441
+ const journalStore = options.journalStore ?? defaultJournalStore;
1442
+ const authStore = options.authStore ?? defaultAuthStore;
1443
+ const now = options.now ?? (() => new Date());
1444
+ assertLifecycleMayStart(authId, admission.realmKey, options);
1445
+ const authSnapshot = admittedAuthSnapshot;
1446
+ let storeIdentity = linkedDeviceStoreIdentity(authSnapshot.auth.path);
1447
+ validateBindingAuth(binding, authSnapshot.auth);
1448
+ if (authSnapshot.auth.subject === undefined) {
1449
+ throw new Error(
1450
+ "linked-device sync requires an account-bound auth locator",
1451
+ );
1452
+ }
1453
+ if (!binding.subject.matches(authSnapshot.auth.subject)) {
1454
+ throw new Error(
1455
+ `auth subject does not match ${binding.subject.format}`,
1456
+ );
1457
+ }
1458
+ lifecycleFor(binding);
1459
+ const prepared = createPreparedJournal(
1460
+ "sync-once",
1461
+ binding,
1462
+ authSnapshot,
1463
+ false,
1464
+ admission,
1465
+ options,
1466
+ );
1467
+ let current = prepared.journal;
1468
+ const boundary = boundaryFor(
1469
+ prepared.journal,
1470
+ prepared.owner,
1471
+ options,
1472
+ () => current,
1473
+ (value) => {
1474
+ current = value;
1475
+ },
1476
+ () => {
1477
+ storeIdentity = bindLinkedDeviceStoreAtExternalBoundary(
1478
+ authSnapshot.auth,
1479
+ admission,
1480
+ storeIdentity,
1481
+ );
1482
+ },
1483
+ );
1484
+ let externalReturned = false;
1485
+ try {
1486
+ if (options.signal?.aborted === true) {
1487
+ throw new OperationDeadlineError("linked-device sync", "cancelled");
1488
+ }
1489
+ const invoke = options.invokeSyncOnce
1490
+ ?? ((
1491
+ selectedBinding: LinkedDevicePluginBindingV1,
1492
+ auth: LinkedDeviceAuth,
1493
+ invocationOptions: {
1494
+ readonly environment: Environment;
1495
+ readonly attempt: ProviderPluginLinkedDeviceAttemptBoundaryV1;
1496
+ },
1497
+ ) => lifecycleFor(selectedBinding).syncOnce(auth, invocationOptions));
1498
+ const pluginResult = await invoke(binding, authSnapshot.auth, {
1499
+ environment,
1500
+ attempt: boundary.attempt,
1501
+ });
1502
+ externalReturned = true;
1503
+ if (!boundary.wasCrossed()) {
1504
+ await boundary.crossConservatively();
1505
+ throw new Error(
1506
+ "linked-device plugin returned from sync without crossing its durable external boundary",
1507
+ );
1508
+ }
1509
+ const result = journalSyncResult(pluginResult);
1510
+ current = journalStore.update(
1511
+ current,
1512
+ { type: "external-complete", result, at: timestamp(now) },
1513
+ { owner: prepared.owner, environment },
1514
+ );
1515
+ assertLinkedDeviceStoreIdentityUnchanged(
1516
+ storeIdentity,
1517
+ authSnapshot.auth.path,
1518
+ );
1519
+ const commitAuthSnapshot = assertAuthSnapshot(
1520
+ authStore.loadSnapshot(authId, environment),
1521
+ authId,
1522
+ );
1523
+ if (
1524
+ commitAuthSnapshot.contentSha256 !== authSnapshot.contentSha256
1525
+ || linkedDeviceRealmKey(commitAuthSnapshot.auth)
1526
+ !== admission.realmKey
1527
+ ) {
1528
+ throw new Error(
1529
+ "linked-device auth changed concurrently after external sync",
1530
+ );
1531
+ }
1532
+ current = journalStore.update(
1533
+ current,
1534
+ { type: "committed", result, at: timestamp(now) },
1535
+ { owner: prepared.owner, environment },
1536
+ );
1537
+ return Object.freeze({
1538
+ journalId: current.journal.journalId,
1539
+ result: Object.freeze({ ...pluginResult }),
1540
+ });
1541
+ } catch (error) {
1542
+ return failLifecycle(
1543
+ current,
1544
+ prepared.owner,
1545
+ error,
1546
+ options,
1547
+ externalReturned,
1548
+ );
1549
+ }
1550
+ }
1551
+
1552
+ export async function runLinkedDevicePairLifecycle(
1553
+ binding: LinkedDevicePluginBindingV1,
1554
+ authId: string,
1555
+ options: RunLinkedDevicePairLifecycleOptions,
1556
+ ): Promise<LinkedDevicePairLifecycleResult> {
1557
+ lifecycleFor(binding);
1558
+ const plugin = ownerPlugin(options.registry, binding);
1559
+ if (plugin.sourceKind === "portable") {
1560
+ throw new Error(
1561
+ "portable provider plugins do not support linked-device lifecycle execution",
1562
+ );
1563
+ }
1564
+ return withLifecycleAdmission(
1565
+ authId,
1566
+ options,
1567
+ (admission, authSnapshot) =>
1568
+ runLinkedDevicePairLifecycleAdmitted(
1569
+ binding,
1570
+ authId,
1571
+ options,
1572
+ admission,
1573
+ authSnapshot,
1574
+ ),
1575
+ );
1576
+ }
1577
+
1578
+ export async function runLinkedDeviceSyncOnceLifecycle(
1579
+ binding: LinkedDevicePluginBindingV1,
1580
+ authId: string,
1581
+ options: RunLinkedDeviceSyncLifecycleOptions,
1582
+ ): Promise<LinkedDeviceSyncLifecycleResult> {
1583
+ lifecycleFor(binding);
1584
+ const plugin = ownerPlugin(options.registry, binding);
1585
+ if (plugin.sourceKind === "portable") {
1586
+ throw new Error(
1587
+ "portable provider plugins do not support linked-device lifecycle execution",
1588
+ );
1589
+ }
1590
+ return withLifecycleAdmission(
1591
+ authId,
1592
+ options,
1593
+ (admission, authSnapshot) =>
1594
+ runLinkedDeviceSyncOnceLifecycleAdmitted(
1595
+ binding,
1596
+ authId,
1597
+ options,
1598
+ admission,
1599
+ authSnapshot,
1600
+ ),
1601
+ );
1602
+ }