@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
package/src/client.ts ADDED
@@ -0,0 +1,2726 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { types as nodeTypes } from "node:util";
5
+
6
+ import { canonicalJson, sha256 } from "./canonical-json";
7
+ import type {
8
+ CapabilityReadRequest,
9
+ InvokeCapabilityOptions,
10
+ InvokeCapabilitySyncOptions,
11
+ ReadCapabilityOptions,
12
+ ReadProjectionCacheResult,
13
+ ReadProjectionCacheOutcome,
14
+ ReadProjectionPublication,
15
+ RevalidateCapabilityOptions,
16
+ RevalidatedCapability,
17
+ RevalidatedCapabilityCurrent,
18
+ GhostgetClientInvocationResult,
19
+ GhostgetClientLocalCliContractIdentity,
20
+ GhostgetClientLocalCliToolIdentity,
21
+ GhostgetClientPortableOperationIdentity,
22
+ GhostgetClientReadFailure,
23
+ GhostgetClientRunReceipt,
24
+ } from "./client-types";
25
+ import {
26
+ isProviderPluginOperationName,
27
+ isProviderPluginSurfaceId,
28
+ PROVIDER_PLUGIN_ID_MAX_LENGTH,
29
+ PROVIDER_PLUGIN_OPERATION_NAME_MAX_LENGTH,
30
+ } from "./provider-plugin-identifiers";
31
+ import { parsePortableOperationIdentityV1 } from "./provider-plugin-portable-identity";
32
+
33
+ type JsonRecord = Record<string, unknown>;
34
+ type ReadProjectionCacheHit = Extract<
35
+ ReadProjectionCacheResult,
36
+ { readonly status: "hit" }
37
+ >;
38
+
39
+ const MAX_INPUT_BYTES = 1024 * 1024;
40
+ const MAX_OUTPUT_BYTES = 20 * 1024 * 1024;
41
+ const MAX_ERROR_BYTES = 8 * 1024;
42
+ const MAX_RUN_RECEIPT_ERROR_BYTES = 12 * 1024;
43
+ const MAX_FRESH_FOR_MS = 365 * 24 * 60 * 60 * 1_000;
44
+ const MAX_INPUT_DEPTH = 64;
45
+ const MAX_INPUT_NODES = 100_000;
46
+ const abortSignalAbortedGetter = (() => {
47
+ const descriptor = Object.getOwnPropertyDescriptor(
48
+ AbortSignal.prototype,
49
+ "aborted",
50
+ );
51
+ const getter = descriptor === undefined
52
+ ? undefined
53
+ : Reflect.get(descriptor, "get") as unknown;
54
+ return typeof getter !== "function"
55
+ ? undefined
56
+ : (value: unknown): unknown => Reflect.apply(getter, value, []);
57
+ })();
58
+
59
+ type JsonSnapshot =
60
+ | null
61
+ | boolean
62
+ | number
63
+ | string
64
+ | readonly JsonSnapshot[]
65
+ | { readonly [key: string]: JsonSnapshot };
66
+
67
+ type PreparedRequest = {
68
+ readonly adapterId: string;
69
+ readonly operationId: string;
70
+ readonly authId?: string;
71
+ readonly input: string;
72
+ };
73
+
74
+ const PUBLIC_WEB_SESSION_AUTHORITY_KIND = "public-web-session";
75
+
76
+ type PublicWebSessionAuthority = {
77
+ readonly schemaVersion: 1;
78
+ readonly id: string;
79
+ readonly kind: typeof PUBLIC_WEB_SESSION_AUTHORITY_KIND;
80
+ readonly subject: string;
81
+ };
82
+
83
+ function publicWebSessionAuthority(
84
+ request: Pick<PreparedRequest, "adapterId" | "operationId">,
85
+ ): PublicWebSessionAuthority {
86
+ const coordinate = Object.freeze({
87
+ adapter: request.adapterId,
88
+ operation: request.operationId,
89
+ });
90
+ return Object.freeze({
91
+ schemaVersion: 1,
92
+ id: `public-${sha256(canonicalJson(coordinate)).slice(0, 32)}`,
93
+ kind: PUBLIC_WEB_SESSION_AUTHORITY_KIND,
94
+ subject: `public:${request.adapterId}:${request.operationId}`,
95
+ });
96
+ }
97
+
98
+ function expectedRequestAuthId(
99
+ request: PreparedRequest,
100
+ authKind: string,
101
+ ): string {
102
+ if (authKind === PUBLIC_WEB_SESSION_AUTHORITY_KIND) {
103
+ if (request.authId !== undefined) {
104
+ throw new Error("Ghostget public invocation unexpectedly accepted an auth locator");
105
+ }
106
+ return publicWebSessionAuthority(request).id;
107
+ }
108
+ return request.authId ?? request.adapterId;
109
+ }
110
+
111
+ type PreparedCommand = {
112
+ readonly arguments: readonly string[];
113
+ readonly input: string;
114
+ };
115
+
116
+ type PreparedChildEnvironment = Readonly<Record<string, string>>;
117
+
118
+ type PreparedClientOptions = {
119
+ readonly cwd: string;
120
+ readonly environment: PreparedChildEnvironment;
121
+ readonly freshForMs?: number;
122
+ readonly now?: Date;
123
+ readonly headed?: boolean;
124
+ readonly signal?: AbortSignal;
125
+ };
126
+
127
+ type ProjectionIdentity =
128
+ | {
129
+ readonly status: "ready";
130
+ readonly key: string;
131
+ readonly authIdentity: string;
132
+ readonly authHash: string;
133
+ readonly inputHash: string;
134
+ }
135
+ | {
136
+ readonly status: "unbound";
137
+ readonly authIdentity: string;
138
+ readonly authHash: string;
139
+ readonly inputHash: string;
140
+ };
141
+
142
+ type ExecutionIdentity = {
143
+ readonly adapter: {
144
+ readonly id: string;
145
+ readonly version: string;
146
+ readonly hash: string;
147
+ };
148
+ readonly operation: string;
149
+ } & (
150
+ | {
151
+ readonly schemaVersion: 2;
152
+ readonly transport: "browser";
153
+ }
154
+ | {
155
+ readonly schemaVersion: 3;
156
+ readonly transport: "provider-api";
157
+ readonly providerContractHash: string;
158
+ }
159
+ | {
160
+ readonly schemaVersion: 4;
161
+ readonly transport: "web-session-api";
162
+ readonly webSessionContractHash: string;
163
+ }
164
+ | {
165
+ readonly schemaVersion: 5;
166
+ readonly transport: "reviewed-template-api";
167
+ readonly reviewedTemplateContractHash: string;
168
+ }
169
+ | {
170
+ readonly schemaVersion: 6;
171
+ readonly transport: "portable-provider-plugin";
172
+ readonly portablePluginContract: GhostgetClientPortableOperationIdentity;
173
+ }
174
+ | {
175
+ readonly schemaVersion: 7;
176
+ readonly transport: "local-cli";
177
+ readonly localCliContract: GhostgetClientLocalCliContractIdentity;
178
+ }
179
+ );
180
+
181
+ type PreviewExecutionIdentity = {
182
+ readonly adapter: ExecutionIdentity["adapter"];
183
+ readonly operation: string;
184
+ } & (
185
+ | { readonly transport: "browser" }
186
+ | { readonly transport: "provider-api" }
187
+ | { readonly transport: "web-session-api" }
188
+ | { readonly transport: "reviewed-template-api" }
189
+ | { readonly transport: "local-cli" }
190
+ | {
191
+ readonly transport: "portable-provider-plugin";
192
+ readonly portablePluginContract: GhostgetClientPortableOperationIdentity;
193
+ }
194
+ );
195
+
196
+ function isRecord(value: unknown): value is JsonRecord {
197
+ return typeof value === "object" && value !== null && !Array.isArray(value);
198
+ }
199
+
200
+ function isUnknownArray(value: unknown): value is readonly unknown[] {
201
+ return Array.isArray(value);
202
+ }
203
+
204
+ function record(value: unknown, label: string): JsonRecord {
205
+ if (
206
+ !isRecord(value)
207
+ || nodeTypes.isProxy(value)
208
+ || (
209
+ Object.getPrototypeOf(value) !== Object.prototype
210
+ && Object.getPrototypeOf(value) !== null
211
+ )
212
+ ) throw new Error(`${label} must be a plain data object`);
213
+ const descriptors = Object.getOwnPropertyDescriptors(value);
214
+ if (Reflect.ownKeys(descriptors).some((key) => typeof key !== "string")) {
215
+ throw new Error(`${label} must be a plain data object`);
216
+ }
217
+ const result: JsonRecord = {};
218
+ for (const [key, descriptor] of Object.entries(descriptors)) {
219
+ if (!descriptor.enumerable || !("value" in descriptor)) {
220
+ throw new Error(`${label} must be a plain data object`);
221
+ }
222
+ Object.defineProperty(result, key, {
223
+ configurable: true,
224
+ enumerable: true,
225
+ value: descriptor.value,
226
+ writable: true,
227
+ });
228
+ }
229
+ return result;
230
+ }
231
+
232
+ function assertExactKeys(
233
+ value: JsonRecord,
234
+ required: readonly string[],
235
+ optional: readonly string[],
236
+ label: string,
237
+ ): void {
238
+ const allowed = new Set([...required, ...optional]);
239
+ const keys = Object.keys(value);
240
+ if (
241
+ required.some((key) => !Object.hasOwn(value, key))
242
+ || keys.some((key) => !allowed.has(key))
243
+ ) throw new Error(`${label} is malformed`);
244
+ }
245
+
246
+ const clientReadFailureRetryDisposition = Object.freeze({
247
+ "target-unavailable": "do-not-retry",
248
+ "auth-repair-required": "repair-auth",
249
+ "account-mismatch": "do-not-retry",
250
+ "provider-throttled": "retry-once-after-60s",
251
+ "provider-temporary": "retry-once-after-60s",
252
+ "operation-timeout": "retry-once-after-60s",
253
+ "contract-drift": "do-not-retry",
254
+ "cleanup-required": "do-not-retry",
255
+ } as const satisfies Readonly<Record<
256
+ GhostgetClientReadFailure["category"],
257
+ GhostgetClientReadFailure["retryDisposition"]
258
+ >>);
259
+
260
+ function parseClientReadFailure(value: unknown): GhostgetClientReadFailure {
261
+ const failure = record(value, "Ghostget read failure");
262
+ assertExactKeys(
263
+ failure,
264
+ ["category", "retryDisposition"],
265
+ [],
266
+ "Ghostget read failure",
267
+ );
268
+ const category = failure.category;
269
+ if (
270
+ typeof category !== "string"
271
+ || !Object.hasOwn(clientReadFailureRetryDisposition, category)
272
+ ) throw new Error("Ghostget read failure category is malformed");
273
+ const retryDisposition = clientReadFailureRetryDisposition[
274
+ category as GhostgetClientReadFailure["category"]
275
+ ];
276
+ if (failure.retryDisposition !== retryDisposition) {
277
+ throw new Error("Ghostget read failure retry disposition is inconsistent");
278
+ }
279
+ return Object.freeze({ category, retryDisposition }) as GhostgetClientReadFailure;
280
+ }
281
+
282
+ /**
283
+ * Clone caller-owned input from property descriptors. Accessors are rejected
284
+ * without being invoked, and a shared object is inspected at most once.
285
+ */
286
+ function snapshotJson(value: unknown, label: string): JsonSnapshot {
287
+ let nodes = 0;
288
+ const ancestors = new WeakSet<object>();
289
+ const completed = new WeakMap<object, JsonSnapshot>();
290
+ const visit = (candidate: unknown, depth: number): JsonSnapshot => {
291
+ nodes += 1;
292
+ if (nodes > MAX_INPUT_NODES || depth > MAX_INPUT_DEPTH) {
293
+ throw new Error(`${label} exceeds its structural bound`);
294
+ }
295
+ if (
296
+ candidate === null
297
+ || typeof candidate === "boolean"
298
+ || typeof candidate === "string"
299
+ ) return candidate;
300
+ if (typeof candidate === "number") {
301
+ if (!Number.isFinite(candidate)) {
302
+ throw new Error(`${label} must contain only JSON data`);
303
+ }
304
+ return candidate;
305
+ }
306
+ if (typeof candidate !== "object") {
307
+ throw new Error(`${label} must contain only JSON data`);
308
+ }
309
+ if (nodeTypes.isProxy(candidate)) {
310
+ throw new Error(`${label} must not contain proxies`);
311
+ }
312
+ const prior = completed.get(candidate);
313
+ if (prior !== undefined) return prior;
314
+ if (ancestors.has(candidate)) throw new Error(`${label} must not be circular`);
315
+ ancestors.add(candidate);
316
+ try {
317
+ if (Array.isArray(candidate)) {
318
+ if (Object.getPrototypeOf(candidate) !== Array.prototype) {
319
+ throw new Error(`${label} arrays must use the standard prototype`);
320
+ }
321
+ const descriptors = Object.getOwnPropertyDescriptors(candidate) as unknown as
322
+ Readonly<Record<string, PropertyDescriptor>>;
323
+ const descriptorKeys = Reflect.ownKeys(descriptors);
324
+ if (descriptorKeys.some((key) => typeof key !== "string")) {
325
+ throw new Error(`${label} arrays have unsupported symbol fields`);
326
+ }
327
+ const lengthDescriptor = descriptors.length;
328
+ if (
329
+ lengthDescriptor === undefined
330
+ || !("value" in lengthDescriptor)
331
+ || typeof lengthDescriptor.value !== "number"
332
+ || !Number.isSafeInteger(lengthDescriptor.value)
333
+ || lengthDescriptor.value < 0
334
+ ) throw new Error(`${label} arrays are malformed`);
335
+ const length = lengthDescriptor.value;
336
+ const keys = descriptorKeys.filter(
337
+ (key): key is string => typeof key === "string" && key !== "length",
338
+ );
339
+ if (
340
+ keys.length !== length
341
+ || keys.some((key, index) => key !== String(index))
342
+ ) throw new Error(`${label} arrays must be dense data arrays`);
343
+ const cloned: JsonSnapshot[] = [];
344
+ for (let index = 0; index < length; index += 1) {
345
+ const descriptor = descriptors[String(index)];
346
+ if (
347
+ descriptor === undefined
348
+ || !descriptor.enumerable
349
+ || !("value" in descriptor)
350
+ ) throw new Error(`${label} arrays must contain only data elements`);
351
+ cloned.push(visit(descriptor.value, depth + 1));
352
+ }
353
+ const snapshot = Object.freeze(cloned);
354
+ completed.set(candidate, snapshot);
355
+ return snapshot;
356
+ }
357
+ const prototype = Object.getPrototypeOf(candidate) as unknown;
358
+ if (prototype !== Object.prototype && prototype !== null) {
359
+ throw new Error(`${label} objects must use a plain prototype`);
360
+ }
361
+ const descriptors = Object.getOwnPropertyDescriptors(candidate);
362
+ const descriptorKeys = Reflect.ownKeys(descriptors);
363
+ if (descriptorKeys.some((key) => typeof key !== "string")) {
364
+ throw new Error(`${label} objects have unsupported symbol fields`);
365
+ }
366
+ const cloned: JsonRecord = {};
367
+ for (const key of (descriptorKeys as string[]).sort((left, right) =>
368
+ left.localeCompare(right))) {
369
+ const descriptor = descriptors[key];
370
+ if (
371
+ descriptor === undefined
372
+ || !descriptor.enumerable
373
+ || !("value" in descriptor)
374
+ ) throw new Error(`${label} objects must contain only data properties`);
375
+ Object.defineProperty(cloned, key, {
376
+ value: visit(descriptor.value, depth + 1),
377
+ enumerable: true,
378
+ configurable: false,
379
+ writable: false,
380
+ });
381
+ }
382
+ const snapshot = Object.freeze(cloned) as JsonSnapshot;
383
+ completed.set(candidate, snapshot);
384
+ return snapshot;
385
+ } finally {
386
+ ancestors.delete(candidate);
387
+ }
388
+ };
389
+ return visit(value, 0);
390
+ }
391
+
392
+ function safeString(value: unknown, label: string, maximum = 512): string {
393
+ if (
394
+ typeof value !== "string"
395
+ || value.length < 1
396
+ || value.length > maximum
397
+ || value.trim() !== value
398
+ || [...value].some((character) => {
399
+ const code = character.codePointAt(0) ?? -1;
400
+ return code <= 0x1f || code === 0x7f;
401
+ })
402
+ ) throw new Error(`${label} is malformed`);
403
+ return value;
404
+ }
405
+
406
+ function providerOperationName(value: unknown, label: string): string {
407
+ const operation = safeString(
408
+ value,
409
+ label,
410
+ PROVIDER_PLUGIN_OPERATION_NAME_MAX_LENGTH,
411
+ );
412
+ if (!isProviderPluginOperationName(operation)) {
413
+ throw new Error(`${label} is malformed`);
414
+ }
415
+ return operation;
416
+ }
417
+
418
+ function providerSurfaceId(value: unknown, label: string): string {
419
+ const surface = safeString(value, label, PROVIDER_PLUGIN_ID_MAX_LENGTH);
420
+ if (!isProviderPluginSurfaceId(surface)) {
421
+ throw new Error(`${label} is malformed`);
422
+ }
423
+ return surface;
424
+ }
425
+
426
+ function boundedUtf8String(
427
+ value: unknown,
428
+ label: string,
429
+ maximumBytes: number,
430
+ ): string {
431
+ if (
432
+ typeof value !== "string"
433
+ || Buffer.byteLength(value, "utf8") > maximumBytes
434
+ ) throw new Error(`${label} is malformed`);
435
+ return value;
436
+ }
437
+
438
+ function digest(value: unknown, label: string): string {
439
+ const text = safeString(value, label, 64);
440
+ if (!/^[a-f0-9]{64}$/u.test(text)) throw new Error(`${label} is malformed`);
441
+ return text;
442
+ }
443
+
444
+ function safeInteger(
445
+ value: unknown,
446
+ label: string,
447
+ minimum: number,
448
+ maximum: number,
449
+ ): number {
450
+ if (!Number.isSafeInteger(value) || typeof value !== "number" || value < minimum || value > maximum) {
451
+ throw new Error(`${label} is malformed`);
452
+ }
453
+ return value;
454
+ }
455
+
456
+ function timestamp(value: unknown, label: string): string {
457
+ const text = safeString(value, label, 64);
458
+ const date = new Date(text);
459
+ if (!Number.isFinite(date.getTime()) || date.toISOString() !== text) {
460
+ throw new Error(`${label} is malformed`);
461
+ }
462
+ return text;
463
+ }
464
+
465
+ function boundedMessage(value: string): string {
466
+ const bytes = Buffer.from(value, "utf8");
467
+ if (bytes.byteLength <= MAX_ERROR_BYTES) return value.trim();
468
+ return `${bytes.subarray(0, MAX_ERROR_BYTES).toString("utf8").trim()}…`;
469
+ }
470
+
471
+ function cliSourcePath(): string {
472
+ const besideSource = fileURLToPath(new URL("./cli.ts", import.meta.url));
473
+ if (existsSync(besideSource)) return besideSource;
474
+ const packagedSource = fileURLToPath(new URL("../src/cli.ts", import.meta.url));
475
+ if (existsSync(packagedSource)) return packagedSource;
476
+ throw new Error("the installed Ghostget CLI source is unavailable");
477
+ }
478
+
479
+ function requireBunRuntime(): void {
480
+ if (typeof process.versions.bun !== "string") {
481
+ throw new Error("@hraness/ghostget/client requires Bun to run the installed Ghostget CLI");
482
+ }
483
+ }
484
+
485
+ function environmentName(value: string): string {
486
+ if (value.length < 1 || value.includes("=") || value.includes("\0")) {
487
+ throw new Error("Ghostget client environment name is malformed");
488
+ }
489
+ return value;
490
+ }
491
+
492
+ function defineEnvironmentValue(
493
+ environment: Record<string, string>,
494
+ key: string,
495
+ value: string,
496
+ ): void {
497
+ Object.defineProperty(environment, key, {
498
+ value,
499
+ enumerable: true,
500
+ configurable: true,
501
+ writable: true,
502
+ });
503
+ }
504
+
505
+ function snapshotChildEnvironment(
506
+ overrides: unknown,
507
+ ): PreparedChildEnvironment {
508
+ const environment = Object.create(null) as Record<string, string>;
509
+ for (const [key, value] of Object.entries(process.env)) {
510
+ if (typeof value === "string") defineEnvironmentValue(environment, key, value);
511
+ }
512
+ if (overrides === undefined) return Object.freeze(environment);
513
+ if (
514
+ !isRecord(overrides)
515
+ || nodeTypes.isProxy(overrides)
516
+ || (
517
+ Object.getPrototypeOf(overrides) !== Object.prototype
518
+ && Object.getPrototypeOf(overrides) !== null
519
+ )
520
+ ) {
521
+ throw new Error(
522
+ "Ghostget client environment must use a plain, non-proxy object",
523
+ );
524
+ }
525
+ const descriptors = Object.getOwnPropertyDescriptors(overrides);
526
+ const keys = Reflect.ownKeys(descriptors);
527
+ if (keys.some((key) => typeof key !== "string")) {
528
+ throw new Error("Ghostget client environment has unsupported symbol fields");
529
+ }
530
+ for (const key of (keys as string[]).sort((left, right) =>
531
+ left.localeCompare(right))) {
532
+ const descriptor = descriptors[key];
533
+ if (descriptor === undefined || !("value" in descriptor)) {
534
+ throw new Error(
535
+ "Ghostget client environment must contain only data properties",
536
+ );
537
+ }
538
+ if (!descriptor.enumerable) {
539
+ throw new Error(
540
+ "Ghostget client environment properties must be enumerable",
541
+ );
542
+ }
543
+ const name = environmentName(key);
544
+ if (descriptor.value === undefined) {
545
+ delete environment[name];
546
+ } else if (
547
+ typeof descriptor.value !== "string"
548
+ || descriptor.value.includes("\0")
549
+ ) {
550
+ throw new Error("Ghostget client environment value is malformed");
551
+ } else {
552
+ defineEnvironmentValue(environment, name, descriptor.value);
553
+ }
554
+ }
555
+ return Object.freeze(environment);
556
+ }
557
+
558
+ function isBrandedAbortSignal(value: unknown): value is AbortSignal {
559
+ if (
560
+ nodeTypes.isProxy(value)
561
+ || typeof value !== "object"
562
+ || value === null
563
+ || abortSignalAbortedGetter === undefined
564
+ ) return false;
565
+ if (Object.getPrototypeOf(value) !== AbortSignal.prototype) return false;
566
+ try {
567
+ return typeof abortSignalAbortedGetter(value) === "boolean";
568
+ } catch {
569
+ return false;
570
+ }
571
+ }
572
+
573
+ type ClientOptionsMode = "read" | "revalidation" | "invoke-async" | "invoke-sync";
574
+
575
+ function snapshotClientOptions(
576
+ optionsValue: unknown,
577
+ mode: ClientOptionsMode,
578
+ ): PreparedClientOptions {
579
+ if (
580
+ !isRecord(optionsValue)
581
+ || nodeTypes.isProxy(optionsValue)
582
+ || (
583
+ Object.getPrototypeOf(optionsValue) !== Object.prototype
584
+ && Object.getPrototypeOf(optionsValue) !== null
585
+ )
586
+ ) throw new Error("Ghostget client options must use a plain, non-proxy object");
587
+ const descriptors = Object.getOwnPropertyDescriptors(optionsValue);
588
+ const keys = Reflect.ownKeys(descriptors);
589
+ if (keys.some((key) => typeof key !== "string")) {
590
+ throw new Error("Ghostget client options have unsupported symbol fields");
591
+ }
592
+ const allowed = new Set(mode === "read"
593
+ ? ["environment", "freshForMs", "now"]
594
+ : mode === "revalidation"
595
+ ? ["environment", "freshForMs", "now", "headed", "signal"]
596
+ : mode === "invoke-async"
597
+ ? ["environment", "headed", "signal"]
598
+ : ["environment", "headed"]);
599
+ for (const key of keys as string[]) {
600
+ const descriptor = descriptors[key];
601
+ if (descriptor === undefined || !allowed.has(key)) {
602
+ throw new Error("Ghostget client options contain an unsupported field");
603
+ }
604
+ if (!("value" in descriptor)) {
605
+ throw new Error("Ghostget client options must contain only data properties");
606
+ }
607
+ }
608
+ const option = (key: string): unknown => descriptors[key]?.value;
609
+ const freshForMsValue = option("freshForMs");
610
+ const nowValue = option("now");
611
+ const headedValue = option("headed");
612
+ const signalValue = option("signal");
613
+ const freshForMs = freshForMsValue === undefined
614
+ ? undefined
615
+ : safeInteger(
616
+ freshForMsValue,
617
+ "Ghostget client freshness window",
618
+ 0,
619
+ MAX_FRESH_FOR_MS,
620
+ );
621
+ let now: Date | undefined;
622
+ if (nowValue !== undefined) {
623
+ if (
624
+ nodeTypes.isProxy(nowValue)
625
+ || !(nowValue instanceof Date)
626
+ || Object.getPrototypeOf(nowValue) !== Date.prototype
627
+ ) throw new Error("Ghostget client observation time is invalid");
628
+ let time: number;
629
+ try {
630
+ time = Date.prototype.getTime.call(nowValue);
631
+ } catch {
632
+ throw new Error("Ghostget client observation time is invalid");
633
+ }
634
+ if (!Number.isFinite(time)) {
635
+ throw new Error("Ghostget client observation time is invalid");
636
+ }
637
+ now = new Date(time);
638
+ }
639
+ if (headedValue !== undefined && typeof headedValue !== "boolean") {
640
+ throw new Error("Ghostget client headed option is malformed");
641
+ }
642
+ if (
643
+ signalValue !== undefined
644
+ && !isBrandedAbortSignal(signalValue)
645
+ ) throw new Error("Ghostget client abort signal is malformed");
646
+ return Object.freeze({
647
+ cwd: process.cwd(),
648
+ environment: snapshotChildEnvironment(option("environment")),
649
+ ...(freshForMs === undefined ? {} : { freshForMs }),
650
+ ...(now === undefined ? {} : { now }),
651
+ ...(headedValue === undefined ? {} : { headed: headedValue }),
652
+ ...(signalValue === undefined ? {} : { signal: signalValue }),
653
+ });
654
+ }
655
+
656
+ function prepareRequest(requestValue: CapabilityReadRequest): PreparedRequest {
657
+ const snapshot = snapshotJson(
658
+ requestValue,
659
+ "Ghostget client request",
660
+ );
661
+ const request = record(snapshot, "Ghostget client request");
662
+ assertExactKeys(
663
+ request,
664
+ ["adapterId", "operationId"],
665
+ ["authId", "input"],
666
+ "Ghostget client request",
667
+ );
668
+ const adapterId = safeString(
669
+ request.adapterId,
670
+ "Ghostget client adapter ID",
671
+ 64,
672
+ );
673
+ const operationId = providerOperationName(
674
+ request.operationId,
675
+ "Ghostget client operation ID",
676
+ );
677
+ const authId = Object.hasOwn(request, "authId")
678
+ ? safeString(request.authId, "Ghostget client auth ID", 64)
679
+ : undefined;
680
+ const rawInput = Object.hasOwn(request, "input") ? request.input : {};
681
+ if (!isRecord(rawInput)) throw new Error("input must be a JSON object");
682
+ const input = canonicalJson(rawInput);
683
+ if (Buffer.byteLength(input, "utf8") > MAX_INPUT_BYTES) {
684
+ throw new Error("Ghostget client input exceeds its byte bound");
685
+ }
686
+ return Object.freeze({
687
+ adapterId,
688
+ operationId,
689
+ ...(authId === undefined ? {} : { authId }),
690
+ input,
691
+ });
692
+ }
693
+
694
+ function preparedCommand(
695
+ request: PreparedRequest,
696
+ options: {
697
+ readonly cacheOnly: boolean;
698
+ readonly projectionIdentityOnly: boolean;
699
+ readonly preview?: boolean;
700
+ readonly headed: boolean;
701
+ },
702
+ ): PreparedCommand {
703
+ return Object.freeze({
704
+ arguments: Object.freeze([
705
+ cliSourcePath(),
706
+ "invoke",
707
+ request.adapterId,
708
+ request.operationId,
709
+ "--input",
710
+ "-",
711
+ ...(request.authId === undefined ? [] : ["--auth", request.authId]),
712
+ ...(options.cacheOnly ? ["--cache-only"] : []),
713
+ ...(options.projectionIdentityOnly
714
+ ? ["--projection-identity-only"]
715
+ : []),
716
+ ...(options.preview === true ? ["--preview"] : []),
717
+ ...(options.headed ? ["--headed"] : []),
718
+ "--json",
719
+ ]),
720
+ input: request.input,
721
+ });
722
+ }
723
+
724
+ function preparedCapabilitiesCommand(request: PreparedRequest): PreparedCommand {
725
+ return Object.freeze({
726
+ arguments: Object.freeze([
727
+ cliSourcePath(),
728
+ "capabilities",
729
+ request.adapterId,
730
+ "--json",
731
+ ]),
732
+ input: "",
733
+ });
734
+ }
735
+
736
+ function parseOutput(text: string, label: string): JsonRecord {
737
+ if (Buffer.byteLength(text, "utf8") > MAX_OUTPUT_BYTES) {
738
+ throw new Error(`${label} exceeds its byte bound`);
739
+ }
740
+ try {
741
+ return record(JSON.parse(text) as unknown, label);
742
+ } catch (error) {
743
+ if (error instanceof Error && error.message === `${label} must be an object`) throw error;
744
+ throw new Error(`${label} is malformed`);
745
+ }
746
+ }
747
+
748
+ function runCacheCommand(
749
+ command: PreparedCommand,
750
+ options: PreparedClientOptions,
751
+ ): { readonly code: number; readonly output: JsonRecord } {
752
+ requireBunRuntime();
753
+ const result = spawnSync(process.execPath, command.arguments, {
754
+ cwd: options.cwd,
755
+ env: options.environment,
756
+ input: command.input,
757
+ encoding: "utf8",
758
+ maxBuffer: MAX_OUTPUT_BYTES,
759
+ });
760
+ if (result.error !== undefined) throw result.error;
761
+ const code = result.status ?? 3;
762
+ const stdout = String(result.stdout ?? "");
763
+ const stderr = String(result.stderr ?? "");
764
+ if (stdout.trim().length === 0) {
765
+ throw new Error(
766
+ boundedMessage(stderr) || `Ghostget cache lookup exited ${code}`,
767
+ );
768
+ }
769
+ if (code !== 0 && code !== 3) {
770
+ throw new Error(
771
+ boundedMessage(stderr) || `Ghostget cache lookup exited ${code}`,
772
+ );
773
+ }
774
+ return Object.freeze({
775
+ code,
776
+ output: parseOutput(stdout, "Ghostget cache response"),
777
+ });
778
+ }
779
+
780
+ function runIdentityCommand(
781
+ command: PreparedCommand,
782
+ options: PreparedClientOptions,
783
+ label: string,
784
+ responseLabel = `${label} response`,
785
+ ): JsonRecord {
786
+ requireBunRuntime();
787
+ const result = spawnSync(process.execPath, command.arguments, {
788
+ cwd: options.cwd,
789
+ env: options.environment,
790
+ input: command.input,
791
+ encoding: "utf8",
792
+ maxBuffer: MAX_OUTPUT_BYTES,
793
+ });
794
+ if (result.error !== undefined) throw result.error;
795
+ const code = result.status ?? 3;
796
+ const stdout = String(result.stdout ?? "");
797
+ const stderr = String(result.stderr ?? "");
798
+ if (stdout.trim().length === 0 || code !== 0) {
799
+ throw new Error(
800
+ boundedMessage(stderr)
801
+ || `${label} exited ${code}`,
802
+ );
803
+ }
804
+ return parseOutput(stdout, responseLabel);
805
+ }
806
+
807
+ function runProjectionIdentityCommand(
808
+ command: PreparedCommand,
809
+ options: PreparedClientOptions,
810
+ ): JsonRecord {
811
+ return runIdentityCommand(
812
+ command,
813
+ options,
814
+ "Ghostget projection identity preflight",
815
+ "Ghostget projection identity response",
816
+ );
817
+ }
818
+
819
+ function observeProjectionIdentity(
820
+ request: PreparedRequest,
821
+ options: PreparedClientOptions,
822
+ ): ProjectionIdentity {
823
+ const value = runProjectionIdentityCommand(
824
+ preparedCommand(request, {
825
+ cacheOnly: false,
826
+ projectionIdentityOnly: true,
827
+ headed: false,
828
+ }),
829
+ options,
830
+ );
831
+ if (
832
+ value.ok !== true
833
+ || value.source !== "projection-identity"
834
+ ) throw new Error("Ghostget projection identity response is malformed");
835
+ if (value.status === "ready") {
836
+ assertExactKeys(
837
+ value,
838
+ [
839
+ "ok",
840
+ "source",
841
+ "status",
842
+ "authIdentity",
843
+ "authHash",
844
+ "inputHash",
845
+ "projection",
846
+ ],
847
+ [],
848
+ "Ghostget projection identity response",
849
+ );
850
+ const projection = record(
851
+ value.projection,
852
+ "Ghostget projection identity",
853
+ );
854
+ assertExactKeys(
855
+ projection,
856
+ ["key"],
857
+ [],
858
+ "Ghostget projection identity",
859
+ );
860
+ return Object.freeze({
861
+ status: "ready",
862
+ key: digest(projection.key, "Ghostget projection identity key"),
863
+ authIdentity: digest(
864
+ value.authIdentity,
865
+ "Ghostget projection auth identity",
866
+ ),
867
+ authHash: digest(value.authHash, "Ghostget projection auth hash"),
868
+ inputHash: digest(
869
+ value.inputHash,
870
+ "Ghostget projection validated input hash",
871
+ ),
872
+ });
873
+ }
874
+ if (value.status === "unbound") {
875
+ assertExactKeys(
876
+ value,
877
+ [
878
+ "ok",
879
+ "source",
880
+ "status",
881
+ "authIdentity",
882
+ "authHash",
883
+ "inputHash",
884
+ ],
885
+ [],
886
+ "Ghostget projection identity response",
887
+ );
888
+ return Object.freeze({
889
+ status: "unbound",
890
+ authIdentity: digest(
891
+ value.authIdentity,
892
+ "Ghostget projection auth identity",
893
+ ),
894
+ authHash: digest(value.authHash, "Ghostget projection auth hash"),
895
+ inputHash: digest(
896
+ value.inputHash,
897
+ "Ghostget projection validated input hash",
898
+ ),
899
+ });
900
+ }
901
+ throw new Error("Ghostget projection identity response is malformed");
902
+ }
903
+
904
+ function projectionIdentitiesMatch(
905
+ before: ProjectionIdentity,
906
+ after: ProjectionIdentity,
907
+ ): boolean {
908
+ return before.status === "ready"
909
+ ? after.status === "ready"
910
+ && after.key === before.key
911
+ && after.authIdentity === before.authIdentity
912
+ && after.authHash === before.authHash
913
+ && after.inputHash === before.inputHash
914
+ : after.status === "unbound"
915
+ && after.authIdentity === before.authIdentity
916
+ && after.authHash === before.authHash
917
+ && after.inputHash === before.inputHash;
918
+ }
919
+
920
+ function parseExecutionPreview(
921
+ value: JsonRecord,
922
+ request: PreparedRequest,
923
+ ): PreviewExecutionIdentity {
924
+ const portable = value.transport === "portable-provider-plugin";
925
+ assertExactKeys(
926
+ value,
927
+ [
928
+ "ok",
929
+ "status",
930
+ "requiresConfirmation",
931
+ "adapter",
932
+ "operation",
933
+ "risk",
934
+ "sideEffect",
935
+ "input",
936
+ "auth",
937
+ "identityBinding",
938
+ "transport",
939
+ ...(portable ? ["portablePluginContract"] : []),
940
+ ],
941
+ [],
942
+ "Ghostget execution identity preview",
943
+ );
944
+ if (
945
+ value.ok !== true
946
+ || value.status !== "preview"
947
+ || value.requiresConfirmation !== false
948
+ || value.risk !== "R1"
949
+ ) throw new Error("Ghostget execution identity preview is malformed");
950
+ const adapterValue = record(
951
+ value.adapter,
952
+ "Ghostget execution identity preview adapter",
953
+ );
954
+ const auth = record(value.auth, "Ghostget execution identity preview auth");
955
+ const binding = record(
956
+ value.identityBinding,
957
+ "Ghostget execution identity preview binding",
958
+ );
959
+ assertExactKeys(
960
+ adapterValue,
961
+ ["id", "version", "hash"],
962
+ [],
963
+ "Ghostget execution identity preview adapter",
964
+ );
965
+ assertExactKeys(
966
+ auth,
967
+ ["id", "kind", "realmFingerprint"],
968
+ [],
969
+ "Ghostget execution identity preview auth",
970
+ );
971
+ assertExactKeys(
972
+ binding,
973
+ ["status", "subject", "accountActor", "requestedActor"],
974
+ [],
975
+ "Ghostget execution identity preview binding",
976
+ );
977
+ const adapter = Object.freeze({
978
+ id: safeString(
979
+ adapterValue.id,
980
+ "Ghostget execution identity preview adapter ID",
981
+ 64,
982
+ ),
983
+ version: safeString(
984
+ adapterValue.version,
985
+ "Ghostget execution identity preview adapter version",
986
+ 64,
987
+ ),
988
+ hash: digest(
989
+ adapterValue.hash,
990
+ "Ghostget execution identity preview adapter hash",
991
+ ),
992
+ });
993
+ const operation = providerOperationName(
994
+ value.operation,
995
+ "Ghostget execution identity preview operation",
996
+ );
997
+ if (adapter.id !== request.adapterId || operation !== request.operationId) {
998
+ throw new Error("Ghostget execution identity preview route is malformed");
999
+ }
1000
+ const authKind = safeString(
1001
+ auth.kind,
1002
+ "Ghostget execution identity preview auth kind",
1003
+ 64,
1004
+ );
1005
+ if (
1006
+ safeString(auth.id, "Ghostget execution identity preview auth ID", 64)
1007
+ !== expectedRequestAuthId(request, authKind)
1008
+ ) throw new Error("Ghostget execution identity preview auth is malformed");
1009
+ const realmFingerprint = safeString(
1010
+ auth.realmFingerprint,
1011
+ "Ghostget execution identity preview auth fingerprint",
1012
+ 16,
1013
+ );
1014
+ if (!/^[a-f0-9]{16}$/u.test(realmFingerprint)) {
1015
+ throw new Error("Ghostget execution identity preview auth fingerprint is malformed");
1016
+ }
1017
+ if (authKind === PUBLIC_WEB_SESSION_AUTHORITY_KIND) {
1018
+ const authority = publicWebSessionAuthority(request);
1019
+ if (
1020
+ value.transport !== "web-session-api"
1021
+ || realmFingerprint
1022
+ !== sha256(canonicalJson(authority)).slice(0, 16)
1023
+ || binding.status !== "public"
1024
+ || binding.subject !== authority.subject
1025
+ || binding.accountActor !== null
1026
+ || binding.requestedActor !== null
1027
+ ) {
1028
+ throw new Error(
1029
+ "Ghostget execution identity preview public authority is malformed",
1030
+ );
1031
+ }
1032
+ }
1033
+ safeString(
1034
+ value.sideEffect,
1035
+ "Ghostget execution identity preview side effect",
1036
+ 64,
1037
+ );
1038
+ const transport = value.transport;
1039
+ if (transport === "portable-provider-plugin") {
1040
+ const portablePluginContract = parsePortableOperationIdentity(
1041
+ value.portablePluginContract,
1042
+ );
1043
+ if (
1044
+ portablePluginContract.adapterId !== adapter.id
1045
+ || portablePluginContract.operation !== operation
1046
+ ) throw new Error("Ghostget execution identity preview route is malformed");
1047
+ return Object.freeze({
1048
+ adapter,
1049
+ operation,
1050
+ transport,
1051
+ portablePluginContract,
1052
+ });
1053
+ }
1054
+ if (
1055
+ transport !== "browser"
1056
+ && transport !== "provider-api"
1057
+ && transport !== "web-session-api"
1058
+ && transport !== "reviewed-template-api"
1059
+ && transport !== "local-cli"
1060
+ ) throw new Error("Ghostget execution identity preview transport is malformed");
1061
+ return Object.freeze({ adapter, operation, transport });
1062
+ }
1063
+
1064
+ function parseCatalogExecutionIdentity(
1065
+ value: JsonRecord,
1066
+ request: PreparedRequest,
1067
+ preview: Exclude<
1068
+ PreviewExecutionIdentity,
1069
+ { readonly transport: "browser" | "portable-provider-plugin" }
1070
+ >,
1071
+ ): ExecutionIdentity {
1072
+ assertExactKeys(
1073
+ value,
1074
+ ["ok", "adapters"],
1075
+ [],
1076
+ "Ghostget execution identity catalog",
1077
+ );
1078
+ if (value.ok !== true || !isUnknownArray(value.adapters)) {
1079
+ throw new Error("Ghostget execution identity catalog is malformed");
1080
+ }
1081
+ if (value.adapters.length !== 1) {
1082
+ throw new Error("Ghostget execution identity catalog is ambiguous");
1083
+ }
1084
+ const adapterValue = record(
1085
+ value.adapters[0],
1086
+ "Ghostget execution identity catalog adapter",
1087
+ );
1088
+ assertExactKeys(
1089
+ adapterValue,
1090
+ [
1091
+ "id",
1092
+ "version",
1093
+ "displayName",
1094
+ "surfaceId",
1095
+ "origins",
1096
+ "manifestHash",
1097
+ "operations",
1098
+ ],
1099
+ [],
1100
+ "Ghostget execution identity catalog adapter",
1101
+ );
1102
+ const adapter = Object.freeze({
1103
+ id: safeString(
1104
+ adapterValue.id,
1105
+ "Ghostget execution identity catalog adapter ID",
1106
+ 64,
1107
+ ),
1108
+ version: safeString(
1109
+ adapterValue.version,
1110
+ "Ghostget execution identity catalog adapter version",
1111
+ 64,
1112
+ ),
1113
+ hash: digest(
1114
+ adapterValue.manifestHash,
1115
+ "Ghostget execution identity catalog adapter hash",
1116
+ ),
1117
+ });
1118
+ if (
1119
+ adapter.id !== request.adapterId
1120
+ || adapter.id !== preview.adapter.id
1121
+ || adapter.version !== preview.adapter.version
1122
+ || adapter.hash !== preview.adapter.hash
1123
+ || !isUnknownArray(adapterValue.operations)
1124
+ ) throw new Error("Ghostget execution identity preflights disagreed");
1125
+ const operations = adapterValue.operations.filter(
1126
+ (candidate): candidate is JsonRecord => (
1127
+ isRecord(candidate) && candidate.id === request.operationId
1128
+ ),
1129
+ );
1130
+ if (operations.length !== 1) {
1131
+ throw new Error("Ghostget execution identity catalog operation is ambiguous");
1132
+ }
1133
+ const operation = operations[0]!;
1134
+ const commonKeys = [
1135
+ "id",
1136
+ "description",
1137
+ "risk",
1138
+ "sideEffect",
1139
+ "idempotency",
1140
+ "dedupeWindowMs",
1141
+ "transport",
1142
+ "input",
1143
+ ] as const;
1144
+ if (
1145
+ operation.risk !== "R1"
1146
+ || operation.transport !== preview.transport
1147
+ ) throw new Error("Ghostget execution identity preflights disagreed");
1148
+ const common = Object.freeze({
1149
+ adapter,
1150
+ operation: preview.operation,
1151
+ });
1152
+ if (preview.transport === "provider-api") {
1153
+ assertExactKeys(
1154
+ operation,
1155
+ [
1156
+ ...commonKeys,
1157
+ "provider",
1158
+ "providerAction",
1159
+ "providerContractVersion",
1160
+ "providerContractHash",
1161
+ "requiredScopeSets",
1162
+ "coverage",
1163
+ "implementation",
1164
+ ],
1165
+ [],
1166
+ "Ghostget execution identity provider capability",
1167
+ );
1168
+ const catalogSurface = providerSurfaceId(
1169
+ adapterValue.surfaceId,
1170
+ "Ghostget execution identity catalog surface",
1171
+ );
1172
+ const provider = providerSurfaceId(
1173
+ operation.provider,
1174
+ "Ghostget execution identity provider surface",
1175
+ );
1176
+ const providerAction = providerOperationName(
1177
+ operation.providerAction,
1178
+ "Ghostget execution identity provider action",
1179
+ );
1180
+ safeInteger(
1181
+ operation.providerContractVersion,
1182
+ "Ghostget execution identity provider contract version",
1183
+ 1,
1184
+ 1_000_000,
1185
+ );
1186
+ if (
1187
+ provider !== catalogSurface
1188
+ || providerAction !== preview.operation
1189
+ ) throw new Error("Ghostget execution identity provider route is malformed");
1190
+ return Object.freeze({
1191
+ ...common,
1192
+ schemaVersion: 3 as const,
1193
+ transport: "provider-api" as const,
1194
+ providerContractHash: digest(
1195
+ operation.providerContractHash,
1196
+ "Ghostget execution identity provider contract hash",
1197
+ ),
1198
+ });
1199
+ }
1200
+ if (preview.transport === "web-session-api") {
1201
+ assertExactKeys(
1202
+ operation,
1203
+ [
1204
+ ...commonKeys,
1205
+ "site",
1206
+ "webSessionAction",
1207
+ "webSessionContractVersion",
1208
+ "webSessionContractHash",
1209
+ "state",
1210
+ "implementation",
1211
+ ],
1212
+ [],
1213
+ "Ghostget execution identity web-session capability",
1214
+ );
1215
+ const catalogSurface = providerSurfaceId(
1216
+ adapterValue.surfaceId,
1217
+ "Ghostget execution identity catalog surface",
1218
+ );
1219
+ const site = providerSurfaceId(
1220
+ operation.site,
1221
+ "Ghostget execution identity web-session surface",
1222
+ );
1223
+ const webSessionAction = providerOperationName(
1224
+ operation.webSessionAction,
1225
+ "Ghostget execution identity web-session action",
1226
+ );
1227
+ safeInteger(
1228
+ operation.webSessionContractVersion,
1229
+ "Ghostget execution identity web-session contract version",
1230
+ 1,
1231
+ 1_000_000,
1232
+ );
1233
+ if (
1234
+ site !== catalogSurface
1235
+ || webSessionAction !== preview.operation
1236
+ ) throw new Error("Ghostget execution identity web-session route is malformed");
1237
+ return Object.freeze({
1238
+ ...common,
1239
+ schemaVersion: 4 as const,
1240
+ transport: "web-session-api" as const,
1241
+ webSessionContractHash: digest(
1242
+ operation.webSessionContractHash,
1243
+ "Ghostget execution identity web-session contract hash",
1244
+ ),
1245
+ });
1246
+ }
1247
+ if (preview.transport === "local-cli") {
1248
+ assertExactKeys(
1249
+ operation,
1250
+ [
1251
+ ...commonKeys,
1252
+ "surface",
1253
+ "localCliAction",
1254
+ "localCliContractVersion",
1255
+ "localCliContractHash",
1256
+ "localCliTool",
1257
+ "state",
1258
+ "implementation",
1259
+ ],
1260
+ [],
1261
+ "Ghostget execution identity local CLI capability",
1262
+ );
1263
+ const catalogSurface = providerSurfaceId(
1264
+ adapterValue.surfaceId,
1265
+ "Ghostget execution identity catalog surface",
1266
+ );
1267
+ const localCliContract = parseLocalCliContractIdentity({
1268
+ surface: operation.surface,
1269
+ action: operation.localCliAction,
1270
+ version: operation.localCliContractVersion,
1271
+ hash: operation.localCliContractHash,
1272
+ tool: operation.localCliTool,
1273
+ });
1274
+ if (
1275
+ localCliContract.surface !== catalogSurface
1276
+ || localCliContract.action !== preview.operation
1277
+ ) {
1278
+ throw new Error("Ghostget execution identity local CLI route is malformed");
1279
+ }
1280
+ return Object.freeze({
1281
+ ...common,
1282
+ schemaVersion: 7 as const,
1283
+ transport: "local-cli" as const,
1284
+ localCliContract,
1285
+ });
1286
+ }
1287
+ assertExactKeys(
1288
+ operation,
1289
+ [
1290
+ ...commonKeys,
1291
+ "state",
1292
+ "reviewedTemplateContractVersion",
1293
+ "reviewedTemplateContractHash",
1294
+ ],
1295
+ ["instructions", "reviewedAt", "evidenceSha256", "origin"],
1296
+ "Ghostget execution identity reviewed-template capability",
1297
+ );
1298
+ safeInteger(
1299
+ operation.reviewedTemplateContractVersion,
1300
+ "Ghostget execution identity reviewed-template contract version",
1301
+ 1,
1302
+ Number.MAX_SAFE_INTEGER,
1303
+ );
1304
+ return Object.freeze({
1305
+ ...common,
1306
+ schemaVersion: 5 as const,
1307
+ transport: "reviewed-template-api" as const,
1308
+ reviewedTemplateContractHash: digest(
1309
+ operation.reviewedTemplateContractHash,
1310
+ "Ghostget execution identity reviewed-template contract hash",
1311
+ ),
1312
+ });
1313
+ }
1314
+
1315
+ function observeExecutionIdentity(
1316
+ request: PreparedRequest,
1317
+ options: PreparedClientOptions,
1318
+ ): ExecutionIdentity {
1319
+ // Preview owns the portable wrapper identity. The targeted catalog view
1320
+ // supplies built-in contract hashes that preview intentionally omits. Keep
1321
+ // this receipt fence separate from the opaque projection-key fence because
1322
+ // a cache publication error carries no projection key to compare.
1323
+ const preview = parseExecutionPreview(
1324
+ runIdentityCommand(
1325
+ preparedCommand(request, {
1326
+ cacheOnly: false,
1327
+ projectionIdentityOnly: false,
1328
+ preview: true,
1329
+ headed: false,
1330
+ }),
1331
+ options,
1332
+ "Ghostget execution identity preview",
1333
+ ),
1334
+ request,
1335
+ );
1336
+ if (preview.transport === "portable-provider-plugin") {
1337
+ return Object.freeze({
1338
+ adapter: preview.adapter,
1339
+ operation: preview.operation,
1340
+ schemaVersion: 6 as const,
1341
+ transport: preview.transport,
1342
+ portablePluginContract: preview.portablePluginContract,
1343
+ });
1344
+ }
1345
+ if (preview.transport === "browser") {
1346
+ return Object.freeze({
1347
+ adapter: preview.adapter,
1348
+ operation: preview.operation,
1349
+ schemaVersion: 2 as const,
1350
+ transport: preview.transport,
1351
+ });
1352
+ }
1353
+ return parseCatalogExecutionIdentity(
1354
+ runIdentityCommand(
1355
+ preparedCapabilitiesCommand(request),
1356
+ options,
1357
+ "Ghostget execution identity catalog preflight",
1358
+ ),
1359
+ request,
1360
+ preview,
1361
+ );
1362
+ }
1363
+
1364
+ function receiptExecutionIdentity(
1365
+ receipt: GhostgetClientRunReceipt,
1366
+ ): ExecutionIdentity {
1367
+ const common = Object.freeze({
1368
+ adapter: receipt.adapter,
1369
+ operation: receipt.operation,
1370
+ });
1371
+ if (receipt.schemaVersion === 2) {
1372
+ return Object.freeze({
1373
+ ...common,
1374
+ schemaVersion: receipt.schemaVersion,
1375
+ transport: receipt.transport,
1376
+ });
1377
+ }
1378
+ if (receipt.schemaVersion === 3) {
1379
+ return Object.freeze({
1380
+ ...common,
1381
+ schemaVersion: receipt.schemaVersion,
1382
+ transport: receipt.transport,
1383
+ providerContractHash: receipt.providerContractHash,
1384
+ });
1385
+ }
1386
+ if (receipt.schemaVersion === 4) {
1387
+ return Object.freeze({
1388
+ ...common,
1389
+ schemaVersion: receipt.schemaVersion,
1390
+ transport: receipt.transport,
1391
+ webSessionContractHash: receipt.webSessionContractHash,
1392
+ });
1393
+ }
1394
+ if (receipt.schemaVersion === 5) {
1395
+ return Object.freeze({
1396
+ ...common,
1397
+ schemaVersion: receipt.schemaVersion,
1398
+ transport: receipt.transport,
1399
+ reviewedTemplateContractHash: receipt.reviewedTemplateContractHash,
1400
+ });
1401
+ }
1402
+ if (receipt.schemaVersion === 6) return Object.freeze({
1403
+ ...common,
1404
+ schemaVersion: receipt.schemaVersion,
1405
+ transport: receipt.transport,
1406
+ portablePluginContract: receipt.portablePluginContract,
1407
+ });
1408
+ return Object.freeze({
1409
+ ...common,
1410
+ schemaVersion: receipt.schemaVersion,
1411
+ transport: receipt.transport,
1412
+ localCliContract: receipt.localCliContract,
1413
+ });
1414
+ }
1415
+
1416
+ function executionIdentitiesMatch(
1417
+ left: ExecutionIdentity,
1418
+ right: ExecutionIdentity,
1419
+ ): boolean {
1420
+ return canonicalJson(left) === canonicalJson(right);
1421
+ }
1422
+
1423
+ function runLiveCommand(
1424
+ command: PreparedCommand,
1425
+ options: PreparedClientOptions,
1426
+ ): Promise<JsonRecord> {
1427
+ requireBunRuntime();
1428
+ return new Promise((resolve, reject) => {
1429
+ const child = spawn(process.execPath, command.arguments, {
1430
+ cwd: options.cwd,
1431
+ env: options.environment,
1432
+ stdio: ["pipe", "pipe", "pipe"],
1433
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
1434
+ });
1435
+ const stdout: Buffer[] = [];
1436
+ const stderr: Buffer[] = [];
1437
+ let stdoutBytes = 0;
1438
+ let stderrBytes = 0;
1439
+ let settled = false;
1440
+ const fail = (error: unknown): void => {
1441
+ if (settled) return;
1442
+ settled = true;
1443
+ reject(error instanceof Error ? error : new Error(String(error)));
1444
+ };
1445
+ child.stdout.on("data", (chunkValue: Buffer | string) => {
1446
+ const chunk = Buffer.isBuffer(chunkValue) ? chunkValue : Buffer.from(chunkValue);
1447
+ stdoutBytes += chunk.byteLength;
1448
+ if (stdoutBytes > MAX_OUTPUT_BYTES) {
1449
+ child.kill();
1450
+ fail(new Error("Ghostget live response exceeds its byte bound"));
1451
+ return;
1452
+ }
1453
+ stdout.push(chunk);
1454
+ });
1455
+ child.stderr.on("data", (chunkValue: Buffer | string) => {
1456
+ const chunk = Buffer.isBuffer(chunkValue) ? chunkValue : Buffer.from(chunkValue);
1457
+ stderrBytes += chunk.byteLength;
1458
+ if (stderrBytes <= MAX_ERROR_BYTES) stderr.push(chunk);
1459
+ });
1460
+ child.on("error", fail);
1461
+ child.on("close", (code, signal) => {
1462
+ if (settled) return;
1463
+ const output = Buffer.concat(stdout).toString("utf8");
1464
+ const error = boundedMessage(Buffer.concat(stderr).toString("utf8"));
1465
+ if (output.trim().length === 0) {
1466
+ fail(new Error(error || `Ghostget live invocation exited ${code ?? signal ?? "unknown"}`));
1467
+ return;
1468
+ }
1469
+ if (code !== 0 && code !== 3 && code !== 5) {
1470
+ fail(new Error(error || `Ghostget live invocation exited ${code ?? signal ?? "unknown"}`));
1471
+ return;
1472
+ }
1473
+ let parsed: JsonRecord;
1474
+ try {
1475
+ parsed = parseOutput(output, "Ghostget live response");
1476
+ } catch (parseError) {
1477
+ fail(parseError);
1478
+ return;
1479
+ }
1480
+ settled = true;
1481
+ resolve(parsed);
1482
+ });
1483
+ child.stdin.on("error", fail);
1484
+ child.stdin.end(command.input);
1485
+ });
1486
+ }
1487
+
1488
+ function runLiveCommandSync(
1489
+ command: PreparedCommand,
1490
+ options: PreparedClientOptions,
1491
+ ): JsonRecord {
1492
+ requireBunRuntime();
1493
+ const result = spawnSync(process.execPath, command.arguments, {
1494
+ cwd: options.cwd,
1495
+ env: options.environment,
1496
+ input: command.input,
1497
+ encoding: "utf8",
1498
+ maxBuffer: MAX_OUTPUT_BYTES,
1499
+ });
1500
+ if (result.error !== undefined) throw result.error;
1501
+ const code = result.status ?? 3;
1502
+ const stdout = String(result.stdout ?? "");
1503
+ const stderr = String(result.stderr ?? "");
1504
+ if (stdout.trim().length === 0) {
1505
+ throw new Error(
1506
+ boundedMessage(stderr)
1507
+ || `Ghostget live invocation exited ${String(code)}`,
1508
+ );
1509
+ }
1510
+ if (code !== 0 && code !== 3 && code !== 5) {
1511
+ throw new Error(
1512
+ boundedMessage(stderr)
1513
+ || `Ghostget live invocation exited ${String(code)}`,
1514
+ );
1515
+ }
1516
+ return parseOutput(stdout, "Ghostget live response");
1517
+ }
1518
+
1519
+ function parseFreshness(
1520
+ value: unknown,
1521
+ options: PreparedClientOptions,
1522
+ ageMs: number,
1523
+ ): ReadProjectionCacheHit["freshness"] {
1524
+ const freshnessValue = record(value, "Ghostget cache freshness");
1525
+ assertExactKeys(
1526
+ freshnessValue,
1527
+ ["state", "freshForMs"],
1528
+ [],
1529
+ "Ghostget cache freshness",
1530
+ );
1531
+ const childState = freshnessValue.state;
1532
+ if (childState !== "fresh" && childState !== "stale" && childState !== "unclassified") {
1533
+ throw new Error("Ghostget cache freshness state is malformed");
1534
+ }
1535
+ if (options.freshForMs === undefined) {
1536
+ if (freshnessValue.freshForMs !== null) {
1537
+ throw new Error("Ghostget cache freshness window is malformed");
1538
+ }
1539
+ return Object.freeze({ state: childState, freshForMs: null });
1540
+ }
1541
+ const freshForMs = safeInteger(
1542
+ options.freshForMs,
1543
+ "Ghostget client freshness window",
1544
+ 0,
1545
+ MAX_FRESH_FOR_MS,
1546
+ );
1547
+ return Object.freeze({
1548
+ state: ageMs <= freshForMs ? "fresh" : "stale",
1549
+ freshForMs,
1550
+ });
1551
+ }
1552
+
1553
+ function validateReadOptions(options: PreparedClientOptions): Date {
1554
+ const now = options.now ?? new Date();
1555
+ if (!(now instanceof Date) || !Number.isFinite(now.getTime())) {
1556
+ throw new Error("Ghostget client observation time is invalid");
1557
+ }
1558
+ if (options.freshForMs !== undefined) {
1559
+ safeInteger(
1560
+ options.freshForMs,
1561
+ "Ghostget client freshness window",
1562
+ 0,
1563
+ MAX_FRESH_FOR_MS,
1564
+ );
1565
+ }
1566
+ return now;
1567
+ }
1568
+
1569
+ function parseCacheResult(
1570
+ value: JsonRecord,
1571
+ options: PreparedClientOptions,
1572
+ now: Date,
1573
+ ): ReadProjectionCacheResult {
1574
+ if (value.source !== "cache") throw new Error("Ghostget cache response has the wrong source");
1575
+ const projection = record(value.projection, "Ghostget cache projection");
1576
+ if (value.status === "cache-miss") {
1577
+ assertExactKeys(
1578
+ value,
1579
+ ["ok", "source", "status", "projection"],
1580
+ [],
1581
+ "Ghostget cache miss",
1582
+ );
1583
+ assertExactKeys(
1584
+ projection,
1585
+ ["key"],
1586
+ [],
1587
+ "Ghostget cache miss projection",
1588
+ );
1589
+ if (value.ok !== false) throw new Error("Ghostget cache miss has the wrong success state");
1590
+ const key = digest(projection.key, "Ghostget cache key");
1591
+ return Object.freeze({ status: "miss", key });
1592
+ }
1593
+ if (value.status !== "cached" || value.ok !== true || !("output" in value)) {
1594
+ throw new Error("Ghostget cache response has an unsupported outcome");
1595
+ }
1596
+ assertExactKeys(
1597
+ value,
1598
+ ["ok", "source", "status", "projection", "output"],
1599
+ [],
1600
+ "Ghostget cache response",
1601
+ );
1602
+ assertExactKeys(
1603
+ projection,
1604
+ [
1605
+ "key",
1606
+ "dataRevision",
1607
+ "createdAt",
1608
+ "dataChangedAt",
1609
+ "validatedAt",
1610
+ "runId",
1611
+ "ageMs",
1612
+ "freshness",
1613
+ ],
1614
+ [],
1615
+ "Ghostget cache projection",
1616
+ );
1617
+ const key = digest(projection.key, "Ghostget cache key");
1618
+ const validatedAt = timestamp(projection.validatedAt, "Ghostget cache validation time");
1619
+ const childAgeMs = safeInteger(projection.ageMs, "Ghostget cache age", 0, Number.MAX_SAFE_INTEGER);
1620
+ const ageMs = options.now === undefined
1621
+ ? childAgeMs
1622
+ : Math.max(0, now.getTime() - new Date(validatedAt).getTime());
1623
+ return Object.freeze({
1624
+ status: "hit" as const,
1625
+ source: "cache" as const,
1626
+ key,
1627
+ output: value.output,
1628
+ dataRevision: digest(projection.dataRevision, "Ghostget cache data revision"),
1629
+ createdAt: timestamp(projection.createdAt, "Ghostget cache creation time"),
1630
+ dataChangedAt: timestamp(projection.dataChangedAt, "Ghostget cache data-change time"),
1631
+ validatedAt,
1632
+ runId: safeString(projection.runId, "Ghostget cache run ID", 64),
1633
+ ageMs,
1634
+ freshness: parseFreshness(projection.freshness, options, ageMs),
1635
+ });
1636
+ }
1637
+
1638
+ function parsePublication(value: unknown): ReadProjectionPublication {
1639
+ const publication = record(value, "Ghostget cache publication");
1640
+ assertExactKeys(
1641
+ publication,
1642
+ ["key", "dataRevision", "validatedAt", "dataChangedAt", "disposition"],
1643
+ ["currentDataRevision"],
1644
+ "Ghostget cache publication",
1645
+ );
1646
+ const disposition = publication.disposition;
1647
+ if (
1648
+ disposition !== "created"
1649
+ && disposition !== "changed"
1650
+ && disposition !== "unchanged"
1651
+ && disposition !== "superseded"
1652
+ ) throw new Error("Ghostget cache publication disposition is malformed");
1653
+ if (
1654
+ (disposition === "superseded")
1655
+ !== Object.hasOwn(publication, "currentDataRevision")
1656
+ ) throw new Error("Ghostget cache publication current revision is malformed");
1657
+ return Object.freeze({
1658
+ key: digest(publication.key, "Ghostget cache publication key"),
1659
+ dataRevision: digest(publication.dataRevision, "Ghostget cache publication data revision"),
1660
+ validatedAt: timestamp(publication.validatedAt, "Ghostget cache publication validation time"),
1661
+ dataChangedAt: timestamp(publication.dataChangedAt, "Ghostget cache publication data-change time"),
1662
+ disposition,
1663
+ ...(publication.currentDataRevision === undefined
1664
+ ? {}
1665
+ : {
1666
+ currentDataRevision: digest(
1667
+ publication.currentDataRevision,
1668
+ "Ghostget current cache data revision",
1669
+ ),
1670
+ }),
1671
+ });
1672
+ }
1673
+
1674
+ function parseCacheOutcome(value: unknown): ReadProjectionCacheOutcome {
1675
+ const outcome = record(value, "Ghostget cache outcome");
1676
+ if (outcome.status === "stored") {
1677
+ assertExactKeys(
1678
+ outcome,
1679
+ ["status", "publication"],
1680
+ [],
1681
+ "Ghostget cache outcome",
1682
+ );
1683
+ return Object.freeze({ status: "stored", publication: parsePublication(outcome.publication) });
1684
+ }
1685
+ if (outcome.status === "retained" && outcome.reason === "live-read-failed") {
1686
+ assertExactKeys(outcome, ["status", "reason"], [], "Ghostget cache outcome");
1687
+ return Object.freeze({ status: "retained", reason: "live-read-failed" });
1688
+ }
1689
+ if (outcome.status === "miss" && outcome.reason === "no-cached-snapshot") {
1690
+ assertExactKeys(outcome, ["status", "reason"], [], "Ghostget cache outcome");
1691
+ return Object.freeze({ status: "miss", reason: "no-cached-snapshot" });
1692
+ }
1693
+ if (
1694
+ outcome.status === "skipped"
1695
+ && outcome.reason === "auth-subject-unbound"
1696
+ ) {
1697
+ assertExactKeys(outcome, ["status", "reason"], [], "Ghostget cache outcome");
1698
+ return Object.freeze({ status: "skipped", reason: outcome.reason });
1699
+ }
1700
+ if (outcome.status === "error") {
1701
+ assertExactKeys(outcome, ["status", "message"], [], "Ghostget cache outcome");
1702
+ return Object.freeze({ status: "error", message: safeString(outcome.message, "Ghostget cache error", MAX_ERROR_BYTES) });
1703
+ }
1704
+ throw new Error("Ghostget cache outcome is malformed");
1705
+ }
1706
+
1707
+ function parseReceiptStatus(
1708
+ value: unknown,
1709
+ ): GhostgetClientRunReceipt["status"] {
1710
+ if (value !== "succeeded" && value !== "failed") {
1711
+ throw new Error("Ghostget live receipt status is malformed");
1712
+ }
1713
+ return value;
1714
+ }
1715
+
1716
+ function parsePortableOperationIdentity(
1717
+ value: unknown,
1718
+ ): GhostgetClientPortableOperationIdentity {
1719
+ return parsePortableOperationIdentityV1(value);
1720
+ }
1721
+
1722
+ function parseLocalCliToolIdentity(
1723
+ value: unknown,
1724
+ ): GhostgetClientLocalCliToolIdentity {
1725
+ const strictRecord = (candidate: unknown, label: string): JsonRecord => {
1726
+ if (
1727
+ typeof candidate !== "object"
1728
+ || candidate === null
1729
+ || Array.isArray(candidate)
1730
+ || nodeTypes.isProxy(candidate)
1731
+ || (
1732
+ Object.getPrototypeOf(candidate) !== Object.prototype
1733
+ && Object.getPrototypeOf(candidate) !== null
1734
+ )
1735
+ ) throw new Error(`${label} is malformed`);
1736
+ const descriptors = Object.getOwnPropertyDescriptors(candidate);
1737
+ if (Reflect.ownKeys(descriptors).some((key) => typeof key !== "string")) {
1738
+ throw new Error(`${label} is malformed`);
1739
+ }
1740
+ const result: JsonRecord = {};
1741
+ for (const [key, descriptor] of Object.entries(descriptors)) {
1742
+ if (!descriptor.enumerable || !("value" in descriptor)) {
1743
+ throw new Error(`${label} is malformed`);
1744
+ }
1745
+ result[key] = descriptor.value;
1746
+ }
1747
+ return result;
1748
+ };
1749
+ const strictArray = (candidate: unknown, label: string): readonly unknown[] => {
1750
+ if (
1751
+ !Array.isArray(candidate)
1752
+ || nodeTypes.isProxy(candidate)
1753
+ || Object.getPrototypeOf(candidate) !== Array.prototype
1754
+ || candidate.length > 16
1755
+ ) throw new Error(`${label} is malformed`);
1756
+ const descriptors = Object.getOwnPropertyDescriptors(candidate);
1757
+ if (
1758
+ Reflect.ownKeys(descriptors).some((key) => typeof key !== "string")
1759
+ || Object.keys(descriptors).length !== candidate.length + 1
1760
+ ) throw new Error(`${label} is malformed`);
1761
+ return Object.freeze(Array.from({ length: candidate.length }, (_unused, index) => {
1762
+ const descriptor = descriptors[String(index)];
1763
+ if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor)) {
1764
+ throw new Error(`${label} is malformed`);
1765
+ }
1766
+ return descriptor.value;
1767
+ }));
1768
+ };
1769
+ const exactHttpsUrl = (candidate: unknown, label: string): string => {
1770
+ const source = safeString(candidate, label, 2_000);
1771
+ let parsed: URL;
1772
+ try {
1773
+ parsed = new URL(source);
1774
+ } catch {
1775
+ throw new Error(`${label} is malformed`);
1776
+ }
1777
+ if (
1778
+ parsed.protocol !== "https:"
1779
+ || parsed.username !== ""
1780
+ || parsed.password !== ""
1781
+ || parsed.hash !== ""
1782
+ || parsed.search !== ""
1783
+ || parsed.href !== source
1784
+ ) throw new Error(`${label} is malformed`);
1785
+ return source;
1786
+ };
1787
+ const wellFormedVisible = (candidate: string): boolean => {
1788
+ for (let index = 0; index < candidate.length; index += 1) {
1789
+ const code = candidate.charCodeAt(index);
1790
+ if (code <= 0x1f || (code >= 0x7f && code <= 0x9f)) return false;
1791
+ if (code >= 0xd800 && code <= 0xdbff) {
1792
+ if (index + 1 >= candidate.length) return false;
1793
+ const next = candidate.charCodeAt(index + 1);
1794
+ if (next < 0xdc00 || next > 0xdfff) return false;
1795
+ index += 1;
1796
+ } else if (code >= 0xdc00 && code <= 0xdfff) {
1797
+ return false;
1798
+ }
1799
+ }
1800
+ return true;
1801
+ };
1802
+ const token = (candidate: unknown, label: string): string => {
1803
+ const parsed = safeString(candidate, label, 64);
1804
+ if (!/^[a-z0-9](?:[a-z0-9._+-]{0,62}[a-z0-9])?$/u.test(parsed)) {
1805
+ throw new Error(`${label} is malformed`);
1806
+ }
1807
+ return parsed;
1808
+ };
1809
+ const tool = strictRecord(value, "Ghostget local CLI tool identity");
1810
+ assertExactKeys(
1811
+ tool,
1812
+ [
1813
+ "schemaVersion",
1814
+ "id",
1815
+ "implementation",
1816
+ "versionScheme",
1817
+ "version",
1818
+ "artifacts",
1819
+ ],
1820
+ [
1821
+ "sourceUrl",
1822
+ "releaseCommit",
1823
+ "releaseManifestSha256",
1824
+ "releaseManifestUrl",
1825
+ ],
1826
+ "Ghostget local CLI tool identity",
1827
+ );
1828
+ const artifactValues = strictArray(
1829
+ tool.artifacts,
1830
+ "Ghostget local CLI tool artifact table",
1831
+ );
1832
+ if (tool.schemaVersion !== 1) {
1833
+ throw new Error("Ghostget local CLI tool identity is malformed");
1834
+ }
1835
+ if (artifactValues.length < 1) {
1836
+ throw new Error("Ghostget local CLI tool artifact table is malformed");
1837
+ }
1838
+ const artifacts = artifactValues.map((artifactValue, index) => {
1839
+ const artifact = strictRecord(
1840
+ artifactValue,
1841
+ `Ghostget local CLI tool artifact ${index}`,
1842
+ );
1843
+ assertExactKeys(
1844
+ artifact,
1845
+ ["platform", "arch", "executableSha256"],
1846
+ ["archiveSha256", "downloadUrl"],
1847
+ `Ghostget local CLI tool artifact ${index}`,
1848
+ );
1849
+ const archiveSha256 = artifact.archiveSha256 === undefined
1850
+ ? undefined
1851
+ : digest(
1852
+ artifact.archiveSha256,
1853
+ `Ghostget local CLI tool artifact ${index} archive hash`,
1854
+ );
1855
+ const downloadUrl = artifact.downloadUrl === undefined
1856
+ ? undefined
1857
+ : exactHttpsUrl(
1858
+ artifact.downloadUrl,
1859
+ `Ghostget local CLI tool artifact ${index} download URL`,
1860
+ );
1861
+ if ((archiveSha256 === undefined) !== (downloadUrl === undefined)) {
1862
+ throw new Error("Ghostget local CLI tool artifact archive provenance is malformed");
1863
+ }
1864
+ return Object.freeze({
1865
+ platform: token(
1866
+ artifact.platform,
1867
+ `Ghostget local CLI tool artifact ${index} platform`,
1868
+ ),
1869
+ arch: token(
1870
+ artifact.arch,
1871
+ `Ghostget local CLI tool artifact ${index} architecture`,
1872
+ ),
1873
+ executableSha256: digest(
1874
+ artifact.executableSha256,
1875
+ `Ghostget local CLI tool artifact ${index} executable hash`,
1876
+ ),
1877
+ ...(archiveSha256 === undefined ? {} : { archiveSha256 }),
1878
+ ...(downloadUrl === undefined ? {} : { downloadUrl }),
1879
+ });
1880
+ });
1881
+ const coordinates = artifacts.map((artifact) =>
1882
+ `${artifact.platform}\0${artifact.arch}`);
1883
+ if (
1884
+ new Set(coordinates).size !== coordinates.length
1885
+ || coordinates.some((coordinate, index) =>
1886
+ index > 0 && coordinates[index - 1]! >= coordinate)
1887
+ ) throw new Error("Ghostget local CLI tool artifact table is not canonical");
1888
+ const sourceUrl = tool.sourceUrl === undefined
1889
+ ? undefined
1890
+ : exactHttpsUrl(tool.sourceUrl, "Ghostget local CLI tool source URL");
1891
+ const releaseManifestSha256 = tool.releaseManifestSha256 === undefined
1892
+ ? undefined
1893
+ : digest(
1894
+ tool.releaseManifestSha256,
1895
+ "Ghostget local CLI tool release manifest hash",
1896
+ );
1897
+ const releaseManifestUrl = tool.releaseManifestUrl === undefined
1898
+ ? undefined
1899
+ : exactHttpsUrl(
1900
+ tool.releaseManifestUrl,
1901
+ "Ghostget local CLI tool release manifest URL",
1902
+ );
1903
+ if (
1904
+ (releaseManifestSha256 === undefined)
1905
+ !== (releaseManifestUrl === undefined)
1906
+ ) throw new Error("Ghostget local CLI tool release manifest provenance is malformed");
1907
+ if (tool.versionScheme !== "semver" && tool.versionScheme !== "opaque") {
1908
+ throw new Error("Ghostget local CLI tool version scheme is malformed");
1909
+ }
1910
+ if (
1911
+ typeof tool.version !== "string"
1912
+ || tool.version.length < 1
1913
+ || tool.version.length > 128
1914
+ ) throw new Error("Ghostget local CLI tool version is malformed");
1915
+ const version = tool.version;
1916
+ if (
1917
+ !wellFormedVisible(version)
1918
+ || (
1919
+ tool.versionScheme === "semver"
1920
+ && !/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-(?:(?:0|[1-9]\d*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u.test(version)
1921
+ )
1922
+ ) {
1923
+ throw new Error("Ghostget local CLI tool version is malformed");
1924
+ }
1925
+ const hasReleaseCommit = tool.releaseCommit !== undefined;
1926
+ const releaseCommit = hasReleaseCommit
1927
+ ? safeString(
1928
+ tool.releaseCommit,
1929
+ "Ghostget local CLI tool release commit",
1930
+ 40,
1931
+ )
1932
+ : undefined;
1933
+ if (
1934
+ releaseCommit !== undefined
1935
+ && !/^[a-f0-9]{40}$/u.test(releaseCommit)
1936
+ ) throw new Error("Ghostget local CLI tool release commit is malformed");
1937
+ const implementation = safeString(
1938
+ tool.implementation,
1939
+ "Ghostget local CLI tool implementation",
1940
+ 256,
1941
+ );
1942
+ if (!/^[A-Za-z0-9](?:[A-Za-z0-9._/+:-]{0,254}[A-Za-z0-9])?$/u.test(implementation)) {
1943
+ throw new Error("Ghostget local CLI tool implementation is malformed");
1944
+ }
1945
+ return Object.freeze({
1946
+ schemaVersion: 1,
1947
+ id: token(tool.id, "Ghostget local CLI tool ID"),
1948
+ implementation,
1949
+ versionScheme: tool.versionScheme,
1950
+ version,
1951
+ ...(releaseCommit === undefined ? {} : { releaseCommit }),
1952
+ ...(releaseManifestSha256 === undefined
1953
+ ? {}
1954
+ : { releaseManifestSha256, releaseManifestUrl: releaseManifestUrl! }),
1955
+ ...(sourceUrl === undefined ? {} : { sourceUrl }),
1956
+ artifacts: Object.freeze(artifacts),
1957
+ });
1958
+ }
1959
+
1960
+ function parseLocalCliContractIdentity(
1961
+ value: unknown,
1962
+ ): GhostgetClientLocalCliContractIdentity {
1963
+ const identity = record(value, "Ghostget local CLI contract identity");
1964
+ assertExactKeys(
1965
+ identity,
1966
+ ["surface", "action", "version", "hash", "tool"],
1967
+ [],
1968
+ "Ghostget local CLI contract identity",
1969
+ );
1970
+ const surface = providerSurfaceId(
1971
+ identity.surface,
1972
+ "Ghostget local CLI surface",
1973
+ );
1974
+ const action = providerOperationName(
1975
+ identity.action,
1976
+ "Ghostget local CLI action",
1977
+ );
1978
+ return Object.freeze({
1979
+ surface,
1980
+ action,
1981
+ version: safeInteger(
1982
+ identity.version,
1983
+ "Ghostget local CLI contract version",
1984
+ 1,
1985
+ 1_000_000,
1986
+ ),
1987
+ hash: digest(identity.hash, "Ghostget local CLI contract hash"),
1988
+ tool: parseLocalCliToolIdentity(identity.tool),
1989
+ });
1990
+ }
1991
+
1992
+ function parseLiveReceipt(
1993
+ value: unknown,
1994
+ request: PreparedRequest,
1995
+ expectedInputHash: string,
1996
+ ): GhostgetClientRunReceipt {
1997
+ const receipt = record(value, "Ghostget live receipt");
1998
+ const commonKeys = [
1999
+ "schemaVersion",
2000
+ "runId",
2001
+ "planDigest",
2002
+ "adapter",
2003
+ "operation",
2004
+ "risk",
2005
+ "inputHash",
2006
+ "auth",
2007
+ "transport",
2008
+ "status",
2009
+ "dispatchStarted",
2010
+ "dispatch",
2011
+ "startedAt",
2012
+ "finishedAt",
2013
+ "finalOrigin",
2014
+ "error",
2015
+ ] as const;
2016
+ if (receipt.schemaVersion === 2 && receipt.transport === "browser") {
2017
+ assertExactKeys(receipt, commonKeys, [], "Ghostget live receipt");
2018
+ } else if (
2019
+ receipt.schemaVersion === 3
2020
+ && receipt.transport === "provider-api"
2021
+ ) {
2022
+ assertExactKeys(
2023
+ receipt,
2024
+ [...commonKeys, "providerContractHash"],
2025
+ [],
2026
+ "Ghostget live receipt",
2027
+ );
2028
+ } else if (
2029
+ receipt.schemaVersion === 4
2030
+ && receipt.transport === "web-session-api"
2031
+ ) {
2032
+ assertExactKeys(
2033
+ receipt,
2034
+ [...commonKeys, "webSessionContractHash"],
2035
+ [],
2036
+ "Ghostget live receipt",
2037
+ );
2038
+ } else if (
2039
+ receipt.schemaVersion === 5
2040
+ && receipt.transport === "reviewed-template-api"
2041
+ ) {
2042
+ assertExactKeys(
2043
+ receipt,
2044
+ [...commonKeys, "reviewedTemplateContractHash"],
2045
+ [],
2046
+ "Ghostget live receipt",
2047
+ );
2048
+ } else if (
2049
+ receipt.schemaVersion === 6
2050
+ && receipt.transport === "portable-provider-plugin"
2051
+ ) {
2052
+ assertExactKeys(
2053
+ receipt,
2054
+ [...commonKeys, "portablePluginContract"],
2055
+ [],
2056
+ "Ghostget live receipt",
2057
+ );
2058
+ } else if (
2059
+ receipt.schemaVersion === 7
2060
+ && receipt.transport === "local-cli"
2061
+ ) {
2062
+ assertExactKeys(
2063
+ receipt,
2064
+ [...commonKeys, "localCliContract"],
2065
+ [],
2066
+ "Ghostget live receipt",
2067
+ );
2068
+ } else {
2069
+ throw new Error("Ghostget live receipt schema and transport are malformed");
2070
+ }
2071
+ const adapter = record(receipt.adapter, "Ghostget live receipt adapter");
2072
+ const auth = record(receipt.auth, "Ghostget live receipt auth");
2073
+ const dispatch = record(receipt.dispatch, "Ghostget live receipt dispatch");
2074
+ assertExactKeys(
2075
+ adapter,
2076
+ ["id", "version", "hash"],
2077
+ [],
2078
+ "Ghostget live receipt adapter",
2079
+ );
2080
+ assertExactKeys(
2081
+ auth,
2082
+ ["id", "hash", "kind"],
2083
+ [],
2084
+ "Ghostget live receipt auth",
2085
+ );
2086
+ assertExactKeys(
2087
+ dispatch,
2088
+ ["planned", "started", "verified"],
2089
+ [],
2090
+ "Ghostget live receipt dispatch",
2091
+ );
2092
+ if (receipt.risk !== "R1") {
2093
+ throw new Error("Ghostget live receipt risk is malformed");
2094
+ }
2095
+ const authKind = auth.kind;
2096
+ if (
2097
+ authKind !== "browser-profile"
2098
+ && authKind !== "cookie-source"
2099
+ && authKind !== "cookies-file"
2100
+ && authKind !== "linked-device-store"
2101
+ && authKind !== "oauth-token-file"
2102
+ && authKind !== PUBLIC_WEB_SESSION_AUTHORITY_KIND
2103
+ ) throw new Error("Ghostget live receipt auth kind is malformed");
2104
+ if (receipt.dispatchStarted !== false) {
2105
+ throw new Error("Ghostget live receipt dispatch state is malformed");
2106
+ }
2107
+ const planned = safeInteger(
2108
+ dispatch.planned,
2109
+ "Ghostget live receipt planned dispatch count",
2110
+ 0,
2111
+ Number.MAX_SAFE_INTEGER,
2112
+ );
2113
+ const started = safeInteger(
2114
+ dispatch.started,
2115
+ "Ghostget live receipt started dispatch count",
2116
+ 0,
2117
+ Number.MAX_SAFE_INTEGER,
2118
+ );
2119
+ const verified = safeInteger(
2120
+ dispatch.verified,
2121
+ "Ghostget live receipt verified dispatch count",
2122
+ 0,
2123
+ Number.MAX_SAFE_INTEGER,
2124
+ );
2125
+ if (planned !== 0 || started !== 0 || verified !== 0) {
2126
+ throw new Error("Ghostget live receipt dispatch counts are inconsistent");
2127
+ }
2128
+ const status = parseReceiptStatus(receipt.status);
2129
+ const finalOrigin = receipt.finalOrigin === null
2130
+ ? null
2131
+ : safeString(receipt.finalOrigin, "Ghostget live receipt final origin", 4_096);
2132
+ const error = receipt.error === null
2133
+ ? null
2134
+ : boundedUtf8String(
2135
+ receipt.error,
2136
+ "Ghostget live receipt error",
2137
+ MAX_RUN_RECEIPT_ERROR_BYTES,
2138
+ );
2139
+ if (receipt.planDigest !== null) {
2140
+ throw new Error("Ghostget live receipt plan digest is malformed");
2141
+ }
2142
+ const startedAt = timestamp(receipt.startedAt, "Ghostget live receipt start time");
2143
+ const finishedAt = timestamp(receipt.finishedAt, "Ghostget live receipt finish time");
2144
+ if (startedAt > finishedAt) {
2145
+ throw new Error("Ghostget live receipt finished before it started");
2146
+ }
2147
+ const common = Object.freeze({
2148
+ runId: safeString(receipt.runId, "Ghostget live receipt run ID", 64),
2149
+ planDigest: null,
2150
+ adapter: Object.freeze({
2151
+ id: safeString(adapter.id, "Ghostget live receipt adapter ID", 64),
2152
+ version: safeString(adapter.version, "Ghostget live receipt adapter version", 64),
2153
+ hash: digest(adapter.hash, "Ghostget live receipt adapter hash"),
2154
+ }),
2155
+ operation: providerOperationName(
2156
+ receipt.operation,
2157
+ "Ghostget live receipt operation",
2158
+ ),
2159
+ risk: "R1" as const,
2160
+ inputHash: digest(receipt.inputHash, "Ghostget live receipt input hash"),
2161
+ auth: Object.freeze({
2162
+ id: safeString(auth.id, "Ghostget live receipt auth ID", 64),
2163
+ hash: digest(auth.hash, "Ghostget live receipt auth hash"),
2164
+ kind: authKind,
2165
+ }),
2166
+ status,
2167
+ dispatchStarted: false as const,
2168
+ dispatch: Object.freeze({
2169
+ planned: 0 as const,
2170
+ started: 0 as const,
2171
+ verified: 0 as const,
2172
+ }),
2173
+ startedAt,
2174
+ finishedAt,
2175
+ finalOrigin,
2176
+ error,
2177
+ });
2178
+ if (
2179
+ common.adapter.id !== request.adapterId
2180
+ || common.operation !== request.operationId
2181
+ ) throw new Error("Ghostget live receipt route does not match its request");
2182
+ if (
2183
+ common.auth.id !== expectedRequestAuthId(request, common.auth.kind)
2184
+ ) {
2185
+ throw new Error("Ghostget live receipt auth does not match its request");
2186
+ }
2187
+ if (common.auth.kind === PUBLIC_WEB_SESSION_AUTHORITY_KIND) {
2188
+ const authority = publicWebSessionAuthority(request);
2189
+ if (
2190
+ receipt.schemaVersion !== 4
2191
+ || receipt.transport !== "web-session-api"
2192
+ || common.auth.hash !== sha256(canonicalJson(authority))
2193
+ ) {
2194
+ throw new Error("Ghostget live receipt public authority is malformed");
2195
+ }
2196
+ }
2197
+ if (common.inputHash !== expectedInputHash) {
2198
+ throw new Error("Ghostget live receipt input does not match its request");
2199
+ }
2200
+ if (receipt.schemaVersion === 2 && receipt.transport === "browser") {
2201
+ return Object.freeze({ ...common, schemaVersion: 2 as const, transport: "browser" as const });
2202
+ }
2203
+ if (receipt.schemaVersion === 3 && receipt.transport === "provider-api") {
2204
+ return Object.freeze({
2205
+ ...common,
2206
+ schemaVersion: 3 as const,
2207
+ transport: "provider-api" as const,
2208
+ providerContractHash: digest(
2209
+ receipt.providerContractHash,
2210
+ "Ghostget live receipt provider contract hash",
2211
+ ),
2212
+ });
2213
+ }
2214
+ if (receipt.schemaVersion === 4 && receipt.transport === "web-session-api") {
2215
+ return Object.freeze({
2216
+ ...common,
2217
+ schemaVersion: 4 as const,
2218
+ transport: "web-session-api" as const,
2219
+ webSessionContractHash: digest(
2220
+ receipt.webSessionContractHash,
2221
+ "Ghostget live receipt web-session contract hash",
2222
+ ),
2223
+ });
2224
+ }
2225
+ if (
2226
+ receipt.schemaVersion === 5
2227
+ && receipt.transport === "reviewed-template-api"
2228
+ ) {
2229
+ return Object.freeze({
2230
+ ...common,
2231
+ schemaVersion: 5 as const,
2232
+ transport: "reviewed-template-api" as const,
2233
+ reviewedTemplateContractHash: digest(
2234
+ receipt.reviewedTemplateContractHash,
2235
+ "Ghostget live receipt reviewed-template contract hash",
2236
+ ),
2237
+ });
2238
+ }
2239
+ if (
2240
+ receipt.schemaVersion === 6
2241
+ && receipt.transport === "portable-provider-plugin"
2242
+ ) {
2243
+ const portablePluginContract = parsePortableOperationIdentity(
2244
+ receipt.portablePluginContract,
2245
+ );
2246
+ if (
2247
+ portablePluginContract.adapterId !== common.adapter.id
2248
+ || portablePluginContract.operation !== common.operation
2249
+ ) {
2250
+ throw new Error("Ghostget live portable contract route is malformed");
2251
+ }
2252
+ return Object.freeze({
2253
+ ...common,
2254
+ schemaVersion: 6 as const,
2255
+ transport: "portable-provider-plugin" as const,
2256
+ portablePluginContract,
2257
+ });
2258
+ }
2259
+ if (receipt.schemaVersion === 7 && receipt.transport === "local-cli") {
2260
+ const localCliContract = parseLocalCliContractIdentity(
2261
+ receipt.localCliContract,
2262
+ );
2263
+ if (localCliContract.action !== common.operation) {
2264
+ throw new Error("Ghostget live local CLI contract route is malformed");
2265
+ }
2266
+ return Object.freeze({
2267
+ ...common,
2268
+ schemaVersion: 7 as const,
2269
+ transport: "local-cli" as const,
2270
+ localCliContract,
2271
+ });
2272
+ }
2273
+ throw new Error("Ghostget live receipt schema and transport are malformed");
2274
+ }
2275
+
2276
+ function parseLiveResult(
2277
+ value: JsonRecord,
2278
+ request: PreparedRequest,
2279
+ expectedInputHash: string,
2280
+ ): {
2281
+ readonly live: GhostgetClientInvocationResult;
2282
+ readonly cache: ReadProjectionCacheOutcome;
2283
+ } {
2284
+ assertExactKeys(
2285
+ value,
2286
+ ["ok", "source", "status", "runId", "replayed", "receipt", "output", "cache"],
2287
+ ["readFailure"],
2288
+ "Ghostget live response",
2289
+ );
2290
+ if (value.source !== "live") throw new Error("Ghostget live response has the wrong source");
2291
+ const receipt = parseLiveReceipt(value.receipt, request, expectedInputHash);
2292
+ const status = safeString(value.status, "Ghostget live status", 32);
2293
+ const responseRunId = safeString(value.runId, "Ghostget live run ID", 64);
2294
+ const expectedOk = receipt.status === "succeeded";
2295
+ if (
2296
+ status !== receipt.status
2297
+ || receipt.runId !== responseRunId
2298
+ || value.ok !== expectedOk
2299
+ ) {
2300
+ throw new Error("Ghostget live response is not bound to its receipt");
2301
+ }
2302
+ if (typeof value.replayed !== "boolean" || !("output" in value)) {
2303
+ throw new Error("Ghostget live response is incomplete");
2304
+ }
2305
+ const cache = parseCacheOutcome(value.cache);
2306
+ const cacheMatchesReceipt = receipt.status === "succeeded"
2307
+ ? cache.status === "stored"
2308
+ || cache.status === "error"
2309
+ || cache.status === "skipped"
2310
+ : cache.status === "retained"
2311
+ || cache.status === "miss"
2312
+ || cache.status === "error"
2313
+ || cache.status === "skipped";
2314
+ if (!cacheMatchesReceipt) {
2315
+ throw new Error(
2316
+ "Ghostget live cache outcome is inconsistent with its receipt",
2317
+ );
2318
+ }
2319
+ const live: GhostgetClientInvocationResult = receipt.status === "failed"
2320
+ ? (() => {
2321
+ if (value.output !== null) {
2322
+ throw new Error("Ghostget failed live response retained an output");
2323
+ }
2324
+ return Object.freeze({
2325
+ status: "failed" as const,
2326
+ receipt: receipt as GhostgetClientRunReceipt & {
2327
+ readonly status: "failed";
2328
+ },
2329
+ output: null,
2330
+ replayed: value.replayed,
2331
+ readFailure: parseClientReadFailure(value.readFailure),
2332
+ });
2333
+ })()
2334
+ : (() => {
2335
+ if (value.readFailure !== undefined) {
2336
+ throw new Error("Ghostget successful live response included a read failure");
2337
+ }
2338
+ return Object.freeze({
2339
+ status: "succeeded" as const,
2340
+ receipt: receipt as GhostgetClientRunReceipt & {
2341
+ readonly status: "succeeded";
2342
+ },
2343
+ output: value.output,
2344
+ replayed: value.replayed,
2345
+ });
2346
+ })();
2347
+ return Object.freeze({
2348
+ live,
2349
+ cache,
2350
+ });
2351
+ }
2352
+
2353
+ function assertLiveInvocationFences(
2354
+ request: PreparedRequest,
2355
+ options: PreparedClientOptions,
2356
+ executionIdentityBefore: ExecutionIdentity,
2357
+ identityBefore: ProjectionIdentity,
2358
+ parsed: Readonly<{
2359
+ live: GhostgetClientInvocationResult;
2360
+ cache: ReadProjectionCacheOutcome;
2361
+ }>,
2362
+ activity: "invocation" | "revalidation",
2363
+ ): void {
2364
+ const identityAfter = observeProjectionIdentity(request, options);
2365
+ if (!projectionIdentitiesMatch(identityBefore, identityAfter)) {
2366
+ throw new Error(
2367
+ `Ghostget projection identity changed while ${activity} was running; the live result was discarded`,
2368
+ );
2369
+ }
2370
+ if (!executionIdentitiesMatch(
2371
+ executionIdentityBefore,
2372
+ receiptExecutionIdentity(parsed.live.receipt),
2373
+ )) {
2374
+ throw new Error(
2375
+ `Ghostget execution identity changed while ${activity} was running; the live result was discarded`,
2376
+ );
2377
+ }
2378
+ if (parsed.live.receipt.auth.hash !== identityBefore.authHash) {
2379
+ throw new Error(
2380
+ `Ghostget projection identity changed while ${activity} was running; the live result was discarded`,
2381
+ );
2382
+ }
2383
+ if (identityBefore.status === "unbound") {
2384
+ if (parsed.cache.status !== "skipped") {
2385
+ throw new Error(
2386
+ `Ghostget projection identity changed while ${activity} was running; the live result was discarded`,
2387
+ );
2388
+ }
2389
+ return;
2390
+ }
2391
+ if (
2392
+ parsed.cache.status === "skipped"
2393
+ || (
2394
+ parsed.cache.status === "stored"
2395
+ && parsed.cache.publication.key !== identityBefore.key
2396
+ )
2397
+ ) {
2398
+ throw new Error(
2399
+ `Ghostget projection identity changed while ${activity} was running; the live result was discarded`,
2400
+ );
2401
+ }
2402
+ }
2403
+
2404
+ export function readCachedCapability(
2405
+ request: CapabilityReadRequest,
2406
+ options: ReadCapabilityOptions = {},
2407
+ ): ReadProjectionCacheResult {
2408
+ const preparedOptions = snapshotClientOptions(options, "read");
2409
+ const now = validateReadOptions(preparedOptions);
2410
+ const prepared = prepareRequest(request);
2411
+ return readCachedPreparedCapability(prepared, preparedOptions, now);
2412
+ }
2413
+
2414
+ function readCachedPreparedCapability(
2415
+ request: PreparedRequest,
2416
+ options: PreparedClientOptions,
2417
+ now: Date,
2418
+ ): ReadProjectionCacheResult {
2419
+ const command = preparedCommand(request, {
2420
+ cacheOnly: true,
2421
+ projectionIdentityOnly: false,
2422
+ headed: false,
2423
+ });
2424
+ return parseCacheResult(
2425
+ runCacheCommand(command, options).output,
2426
+ options,
2427
+ now,
2428
+ );
2429
+ }
2430
+
2431
+ function selectCurrentCapability(
2432
+ cachedBefore: ReadProjectionCacheResult | null,
2433
+ cachedAfter: ReadProjectionCacheResult | null,
2434
+ parsed: Pick<RevalidatedCapability, "live" | "cache">,
2435
+ ): RevalidatedCapabilityCurrent {
2436
+ const before = cachedBefore?.status === "hit" ? cachedBefore : null;
2437
+ const after = cachedAfter?.status === "hit" ? cachedAfter : null;
2438
+ if (parsed.live.status === "failed") return after;
2439
+ if (parsed.cache.status === "stored") {
2440
+ if (after !== null) return after;
2441
+ if (parsed.cache.publication.disposition === "superseded") return null;
2442
+ return Object.freeze({ source: "live", output: parsed.live.output });
2443
+ }
2444
+ if (parsed.cache.status === "error") {
2445
+ const cacheAdvanced = after !== null
2446
+ && (
2447
+ before === null
2448
+ || after.runId !== before.runId
2449
+ || after.dataRevision !== before.dataRevision
2450
+ || after.validatedAt !== before.validatedAt
2451
+ );
2452
+ if (cacheAdvanced) return after;
2453
+ }
2454
+ return Object.freeze({ source: "live", output: parsed.live.output });
2455
+ }
2456
+
2457
+ async function runRevalidation(
2458
+ request: PreparedRequest,
2459
+ options: PreparedClientOptions,
2460
+ executionIdentityBefore: ExecutionIdentity,
2461
+ identityBefore: ProjectionIdentity,
2462
+ cachedBefore: ReadProjectionCacheResult | null,
2463
+ observationTime: Date,
2464
+ ): Promise<RevalidatedCapability> {
2465
+ const parsed = parseLiveResult(
2466
+ await runLiveCommand(
2467
+ preparedCommand(request, {
2468
+ cacheOnly: false,
2469
+ projectionIdentityOnly: false,
2470
+ headed: options.headed ?? false,
2471
+ }),
2472
+ options,
2473
+ ),
2474
+ request,
2475
+ identityBefore.inputHash,
2476
+ );
2477
+ // Identity preflights do not decode projection payloads, so corrupt local
2478
+ // data can still be repaired by a live read. Every process receives one
2479
+ // frozen options/environment snapshot. Both the projection key and unbound
2480
+ // auth token include Ghostget's auth incarnation and detect A-to-B-to-A
2481
+ // mutations.
2482
+ assertLiveInvocationFences(
2483
+ request,
2484
+ options,
2485
+ executionIdentityBefore,
2486
+ identityBefore,
2487
+ parsed,
2488
+ "revalidation",
2489
+ );
2490
+ if (identityBefore.status === "unbound") {
2491
+ return Object.freeze({
2492
+ cachedBefore: null,
2493
+ cachedAfter: null,
2494
+ current: selectCurrentCapability(null, null, parsed),
2495
+ ...parsed,
2496
+ });
2497
+ }
2498
+ let cachedAfter: ReadProjectionCacheResult | null = null;
2499
+ try {
2500
+ cachedAfter = readCachedPreparedCapability(
2501
+ request,
2502
+ options,
2503
+ observationTime,
2504
+ );
2505
+ } catch {
2506
+ // The identity-only preflight remains authoritative when a corrupt cache
2507
+ // payload could not be decoded before or after live repair.
2508
+ }
2509
+ if (
2510
+ (cachedBefore !== null && cachedBefore.key !== identityBefore.key)
2511
+ || (cachedAfter !== null && cachedAfter.key !== identityBefore.key)
2512
+ ) {
2513
+ throw new Error(
2514
+ "Ghostget projection identity changed while revalidation was running; the live result was discarded",
2515
+ );
2516
+ }
2517
+ return Object.freeze({
2518
+ cachedBefore,
2519
+ cachedAfter,
2520
+ current: selectCurrentCapability(cachedBefore, cachedAfter, parsed),
2521
+ ...parsed,
2522
+ });
2523
+ }
2524
+
2525
+ type CachedBeforeSource =
2526
+ | { readonly status: "provided"; readonly value: ReadProjectionCacheResult | null }
2527
+ | { readonly status: "lookup" };
2528
+
2529
+ function scheduleRevalidation(
2530
+ request: PreparedRequest,
2531
+ options: PreparedClientOptions,
2532
+ observationTime: Date,
2533
+ cachedBeforeSource: CachedBeforeSource,
2534
+ ): Promise<RevalidatedCapability> {
2535
+ // Promise callbacks run after the public API returns. Keep every identity
2536
+ // and live subprocess behind this boundary while retaining the exact frozen
2537
+ // request, options, observation time, and any cache-only SWR result.
2538
+ return Promise.resolve().then(() => {
2539
+ const executionIdentityBefore = observeExecutionIdentity(request, options);
2540
+ const identityBefore = observeProjectionIdentity(request, options);
2541
+ let cachedBefore = cachedBeforeSource.status === "provided"
2542
+ ? cachedBeforeSource.value
2543
+ : null;
2544
+ if (
2545
+ cachedBeforeSource.status === "lookup"
2546
+ && identityBefore.status === "ready"
2547
+ ) {
2548
+ try {
2549
+ cachedBefore = readCachedPreparedCapability(
2550
+ request,
2551
+ options,
2552
+ observationTime,
2553
+ );
2554
+ } catch {
2555
+ // A valid identity preflight fences live repair even when the encrypted
2556
+ // projection itself is corrupt or temporarily unreadable.
2557
+ }
2558
+ }
2559
+ if (
2560
+ cachedBefore !== null
2561
+ && (
2562
+ identityBefore.status !== "ready"
2563
+ || cachedBefore.key !== identityBefore.key
2564
+ )
2565
+ ) {
2566
+ throw new Error(
2567
+ "Ghostget projection identity changed before revalidation started",
2568
+ );
2569
+ }
2570
+ return runRevalidation(
2571
+ request,
2572
+ options,
2573
+ executionIdentityBefore,
2574
+ identityBefore,
2575
+ cachedBefore,
2576
+ observationTime,
2577
+ );
2578
+ });
2579
+ }
2580
+
2581
+ /**
2582
+ * Invoke one semantic R1 read through the installed CLI and return only the
2583
+ * already-validated live receipt/output envelope. The promise defers every
2584
+ * subprocess until after this function returns.
2585
+ */
2586
+ export function invokeCapability(
2587
+ request: CapabilityReadRequest,
2588
+ options: InvokeCapabilityOptions = {},
2589
+ ): Promise<GhostgetClientInvocationResult> {
2590
+ const preparedOptions = snapshotClientOptions(options, "invoke-async");
2591
+ const prepared = prepareRequest(request);
2592
+ return Promise.resolve().then(async () => {
2593
+ const executionIdentityBefore = observeExecutionIdentity(
2594
+ prepared,
2595
+ preparedOptions,
2596
+ );
2597
+ const identityBefore = observeProjectionIdentity(prepared, preparedOptions);
2598
+ const parsed = parseLiveResult(
2599
+ await runLiveCommand(
2600
+ preparedCommand(prepared, {
2601
+ cacheOnly: false,
2602
+ projectionIdentityOnly: false,
2603
+ headed: preparedOptions.headed ?? false,
2604
+ }),
2605
+ preparedOptions,
2606
+ ),
2607
+ prepared,
2608
+ identityBefore.inputHash,
2609
+ );
2610
+ assertLiveInvocationFences(
2611
+ prepared,
2612
+ preparedOptions,
2613
+ executionIdentityBefore,
2614
+ identityBefore,
2615
+ parsed,
2616
+ "invocation",
2617
+ );
2618
+ return parsed.live;
2619
+ });
2620
+ }
2621
+
2622
+ /**
2623
+ * Synchronous live invocation for CLI applications whose command surface is
2624
+ * synchronous. It uses the same exact request snapshot, receipt parser, cache
2625
+ * outcome validation, and before/after identity fences as the async client.
2626
+ */
2627
+ export function invokeCapabilitySync(
2628
+ request: CapabilityReadRequest,
2629
+ options: InvokeCapabilitySyncOptions = {},
2630
+ ): GhostgetClientInvocationResult {
2631
+ const preparedOptions = snapshotClientOptions(options, "invoke-sync");
2632
+ const prepared = prepareRequest(request);
2633
+ const executionIdentityBefore = observeExecutionIdentity(
2634
+ prepared,
2635
+ preparedOptions,
2636
+ );
2637
+ const identityBefore = observeProjectionIdentity(prepared, preparedOptions);
2638
+ const parsed = parseLiveResult(
2639
+ runLiveCommandSync(
2640
+ preparedCommand(prepared, {
2641
+ cacheOnly: false,
2642
+ projectionIdentityOnly: false,
2643
+ headed: preparedOptions.headed ?? false,
2644
+ }),
2645
+ preparedOptions,
2646
+ ),
2647
+ prepared,
2648
+ identityBefore.inputHash,
2649
+ );
2650
+ assertLiveInvocationFences(
2651
+ prepared,
2652
+ preparedOptions,
2653
+ executionIdentityBefore,
2654
+ identityBefore,
2655
+ parsed,
2656
+ "invocation",
2657
+ );
2658
+ return parsed.live;
2659
+ }
2660
+
2661
+ export function revalidateCapability(
2662
+ request: CapabilityReadRequest,
2663
+ options: RevalidateCapabilityOptions = {},
2664
+ ): Promise<RevalidatedCapability> {
2665
+ const preparedOptions = snapshotClientOptions(options, "revalidation");
2666
+ const now = validateReadOptions(preparedOptions);
2667
+ const prepared = prepareRequest(request);
2668
+ return scheduleRevalidation(
2669
+ prepared,
2670
+ preparedOptions,
2671
+ now,
2672
+ { status: "lookup" },
2673
+ );
2674
+ }
2675
+
2676
+ /**
2677
+ * Read the current exact projection synchronously through Ghostget's installed
2678
+ * cache-only command, then start one semantic R1 revalidation through the same
2679
+ * CLI safety kernel. No provider runtime is bundled into this client module.
2680
+ */
2681
+ export function staleWhileRevalidateCapability(
2682
+ request: CapabilityReadRequest,
2683
+ options: RevalidateCapabilityOptions = {},
2684
+ ): {
2685
+ readonly cached: ReadProjectionCacheResult | null;
2686
+ readonly revalidation: Promise<RevalidatedCapability>;
2687
+ } {
2688
+ const preparedOptions = snapshotClientOptions(options, "revalidation");
2689
+ const now = validateReadOptions(preparedOptions);
2690
+ const prepared = prepareRequest(request);
2691
+ let cached: ReadProjectionCacheResult | null = null;
2692
+ try {
2693
+ cached = readCachedPreparedCapability(prepared, preparedOptions, now);
2694
+ } catch {
2695
+ // Cache corruption or an unbound subject must not suppress the separately
2696
+ // scheduled live path.
2697
+ }
2698
+ return Object.freeze({
2699
+ cached,
2700
+ revalidation: scheduleRevalidation(
2701
+ prepared,
2702
+ preparedOptions,
2703
+ now,
2704
+ { status: "provided", value: cached },
2705
+ ),
2706
+ });
2707
+ }
2708
+
2709
+ export type {
2710
+ CapabilityReadRequest,
2711
+ InvokeCapabilityOptions,
2712
+ InvokeCapabilitySyncOptions,
2713
+ ReadCapabilityOptions,
2714
+ ReadProjectionCacheResult,
2715
+ ReadProjectionCacheOutcome,
2716
+ ReadProjectionPublication,
2717
+ RevalidateCapabilityOptions,
2718
+ RevalidatedCapability,
2719
+ RevalidatedCapabilityCurrent,
2720
+ GhostgetClientEnvironment,
2721
+ GhostgetClientInvocationResult,
2722
+ GhostgetClientPortableOperationIdentity,
2723
+ GhostgetClientReadFailure,
2724
+ GhostgetClientRunReceipt,
2725
+ GhostgetClientRunReceiptCommon,
2726
+ } from "./client-types";