@hraness/ghostget 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (500) hide show
  1. package/CHANGELOG.md +458 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1359 -0
  4. package/bunfig.toml +2 -0
  5. package/dist/apple-photos-client.js +704 -0
  6. package/dist/beeper-client.js +4602 -0
  7. package/dist/client.js +1605 -0
  8. package/dist/index-26yq8q16.js +31 -0
  9. package/dist/index-9t13b4k7.js +508 -0
  10. package/dist/index-axwm9b3v.js +5 -0
  11. package/dist/index-gwk7rbyj.js +30 -0
  12. package/dist/index.js +701 -0
  13. package/dist/messaging.js +1130 -0
  14. package/dist/omni-client.js +1406 -0
  15. package/dist/whatsapp-client.js +355 -0
  16. package/docs/imessage-direct-provider.md +106 -0
  17. package/docs/rental-listings.md +94 -0
  18. package/package.json +600 -0
  19. package/skills/ghostget/SKILL.md +251 -0
  20. package/skills/ghostget/agents/openai.yaml +4 -0
  21. package/skills/ghostget/references/adapter-contract.md +181 -0
  22. package/skills/ghostget/references/apple-photos.md +115 -0
  23. package/skills/ghostget/references/article-drafts.md +235 -0
  24. package/skills/ghostget/references/code-owned-provider-scaffold.md +134 -0
  25. package/skills/ghostget/references/cross-posting-video.md +139 -0
  26. package/skills/ghostget/references/cross-posting.md +82 -0
  27. package/skills/ghostget/references/derivation.md +390 -0
  28. package/skills/ghostget/references/hraness-social-profile-stats.json +269 -0
  29. package/skills/ghostget/references/install.md +66 -0
  30. package/skills/ghostget/references/linkedin-adapter.md +442 -0
  31. package/skills/ghostget/references/messaging.md +168 -0
  32. package/skills/ghostget/references/meta-comet-contract.md +153 -0
  33. package/skills/ghostget/references/platform-patterns.md +122 -0
  34. package/skills/ghostget/references/provider-plugins.md +496 -0
  35. package/skills/ghostget/references/reddit-flair.md +78 -0
  36. package/skills/ghostget/references/safety-and-state.md +214 -0
  37. package/skills/ghostget/references/settlement-and-duplicate-cleanup.md +79 -0
  38. package/skills/ghostget/references/social-platform-routing.md +66 -0
  39. package/skills/ghostget/references/social-profile-stats.md +195 -0
  40. package/skills/ghostget/references/social-video-platform-routing.md +89 -0
  41. package/skills/ghostget/references/whatsapp-adapter.md +101 -0
  42. package/skills/ghostget/references/x-adapter.md +332 -0
  43. package/skills/ghostget/references/x-ai-disclosure.md +71 -0
  44. package/src/apple-photos-cli.ts +639 -0
  45. package/src/apple-photos-client-types.ts +168 -0
  46. package/src/apple-photos-client.ts +201 -0
  47. package/src/apple-photos-contact-evidence.ts +838 -0
  48. package/src/apple-photos-local-source.ts +1131 -0
  49. package/src/args.ts +2053 -0
  50. package/src/article-draft-document.ts +417 -0
  51. package/src/article-draft-embeds.ts +123 -0
  52. package/src/article-draft-images.ts +214 -0
  53. package/src/assets/adapter-template/wrench-adapter.json +40 -0
  54. package/src/assets/adapters/beeper/wrench-web-adapter.json +1500 -0
  55. package/src/assets/adapters/beeper/wrench-web-adapter.v1.0.0.json +129 -0
  56. package/src/assets/adapters/beeper/wrench-web-adapter.v1.1.0.json +207 -0
  57. package/src/assets/adapters/beeper/wrench-web-adapter.v2.0.0.json +1463 -0
  58. package/src/assets/adapters/beeper/wrench-web-adapter.v2.1.0.json +1463 -0
  59. package/src/assets/adapters/beeper/wrench-web-adapter.v2.2.0.json +1475 -0
  60. package/src/assets/adapters/beeper/wrench-web-adapter.v2.3.0.json +1488 -0
  61. package/src/assets/adapters/bluesky/wrench-web-adapter.json +684 -0
  62. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.0.0.json +581 -0
  63. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.1.0.json +581 -0
  64. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.2.0.json +581 -0
  65. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.3.0.json +615 -0
  66. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.4.0.json +684 -0
  67. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.5.0.json +684 -0
  68. package/src/assets/adapters/bluesky/wrench-web-adapter.v1.6.0.json +684 -0
  69. package/src/assets/adapters/clasificados/wrench-web-adapter.json +54 -0
  70. package/src/assets/adapters/facebook/wrench-web-adapter.json +750 -0
  71. package/src/assets/adapters/facebook/wrench-web-adapter.v1.0.0.json +706 -0
  72. package/src/assets/adapters/facebook/wrench-web-adapter.v1.1.0.json +703 -0
  73. package/src/assets/adapters/facebook/wrench-web-adapter.v1.2.0.json +750 -0
  74. package/src/assets/adapters/facebook-group/wrench-web-adapter.json +682 -0
  75. package/src/assets/adapters/facebook-group/wrench-web-adapter.v1.0.0.json +583 -0
  76. package/src/assets/adapters/facebook-group/wrench-web-adapter.v1.1.0.json +682 -0
  77. package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.json +437 -0
  78. package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.0.0.json +383 -0
  79. package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.1.0.json +437 -0
  80. package/src/assets/adapters/facebook-marketplace/wrench-web-adapter.v1.1.1.json +437 -0
  81. package/src/assets/adapters/facebook-page/wrench-web-adapter.json +880 -0
  82. package/src/assets/adapters/facebook-page/wrench-web-adapter.v1.0.0.json +740 -0
  83. package/src/assets/adapters/facebook-page/wrench-web-adapter.v1.1.0.json +880 -0
  84. package/src/assets/adapters/github/wrench-web-adapter.json +69 -0
  85. package/src/assets/adapters/github/wrench-web-adapter.v1.0.0.json +42 -0
  86. package/src/assets/adapters/github/wrench-web-adapter.v1.1.0.json +69 -0
  87. package/src/assets/adapters/gmail/wrench-adapter.json +161 -0
  88. package/src/assets/adapters/gmail/wrench-adapter.v1.2.0.json +157 -0
  89. package/src/assets/adapters/hacker-news/wrench-web-adapter.json +315 -0
  90. package/src/assets/adapters/hacker-news/wrench-web-adapter.v1.0.0.json +315 -0
  91. package/src/assets/adapters/imessage/wrench-web-adapter.json +236 -0
  92. package/src/assets/adapters/instagram/wrench-web-adapter.json +719 -0
  93. package/src/assets/adapters/instagram/wrench-web-adapter.v1.0.0.json +627 -0
  94. package/src/assets/adapters/instagram/wrench-web-adapter.v1.1.0.json +609 -0
  95. package/src/assets/adapters/instagram/wrench-web-adapter.v1.2.0.json +640 -0
  96. package/src/assets/adapters/instagram/wrench-web-adapter.v1.3.0.json +671 -0
  97. package/src/assets/adapters/instagram/wrench-web-adapter.v1.4.0.json +710 -0
  98. package/src/assets/adapters/instagram/wrench-web-adapter.v1.5.0.json +710 -0
  99. package/src/assets/adapters/instagram/wrench-web-adapter.v1.6.0.json +719 -0
  100. package/src/assets/adapters/instagram/wrench-web-adapter.v1.7.0.json +719 -0
  101. package/src/assets/adapters/linkedin/wrench-adapter.json +442 -0
  102. package/src/assets/adapters/linkedin/wrench-adapter.v0.4.0.json +124 -0
  103. package/src/assets/adapters/linkedin/wrench-adapter.v1.0.0.json +411 -0
  104. package/src/assets/adapters/linkedin/wrench-web-adapter.json +914 -0
  105. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.0.0.json +604 -0
  106. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.1.0.json +604 -0
  107. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.10.0.json +809 -0
  108. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.11.0.json +809 -0
  109. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.12.0.json +820 -0
  110. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.13.0.json +819 -0
  111. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.14.0.json +819 -0
  112. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.15.0.json +819 -0
  113. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.16.0.json +874 -0
  114. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.17.0.json +874 -0
  115. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.18.0.json +874 -0
  116. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.19.0.json +874 -0
  117. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.2.0.json +731 -0
  118. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.3.0.json +768 -0
  119. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.4.0.json +808 -0
  120. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.5.0.json +808 -0
  121. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.6.0.json +808 -0
  122. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.7.0.json +825 -0
  123. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.8.0.json +792 -0
  124. package/src/assets/adapters/linkedin/wrench-web-adapter.v1.9.0.json +809 -0
  125. package/src/assets/adapters/reddit/wrench-web-adapter.json +855 -0
  126. package/src/assets/adapters/reddit/wrench-web-adapter.v1.0.0.json +577 -0
  127. package/src/assets/adapters/reddit/wrench-web-adapter.v1.1.0.json +604 -0
  128. package/src/assets/adapters/reddit/wrench-web-adapter.v1.10.0.json +712 -0
  129. package/src/assets/adapters/reddit/wrench-web-adapter.v1.11.0.json +712 -0
  130. package/src/assets/adapters/reddit/wrench-web-adapter.v1.12.0.json +712 -0
  131. package/src/assets/adapters/reddit/wrench-web-adapter.v1.2.0.json +653 -0
  132. package/src/assets/adapters/reddit/wrench-web-adapter.v1.3.0.json +712 -0
  133. package/src/assets/adapters/reddit/wrench-web-adapter.v1.4.0.json +713 -0
  134. package/src/assets/adapters/reddit/wrench-web-adapter.v1.5.0.json +712 -0
  135. package/src/assets/adapters/reddit/wrench-web-adapter.v1.6.0.json +713 -0
  136. package/src/assets/adapters/reddit/wrench-web-adapter.v1.7.0.json +713 -0
  137. package/src/assets/adapters/reddit/wrench-web-adapter.v1.8.0.json +713 -0
  138. package/src/assets/adapters/reddit/wrench-web-adapter.v1.9.0.json +713 -0
  139. package/src/assets/adapters/substack/wrench-web-adapter.json +936 -0
  140. package/src/assets/adapters/substack/wrench-web-adapter.v1.0.0.json +817 -0
  141. package/src/assets/adapters/substack/wrench-web-adapter.v1.1.0.json +812 -0
  142. package/src/assets/adapters/substack/wrench-web-adapter.v1.2.0.json +812 -0
  143. package/src/assets/adapters/substack/wrench-web-adapter.v1.3.0.json +902 -0
  144. package/src/assets/adapters/substack/wrench-web-adapter.v1.4.0.json +936 -0
  145. package/src/assets/adapters/substack/wrench-web-adapter.v1.5.0.json +936 -0
  146. package/src/assets/adapters/substack/wrench-web-adapter.v1.6.0.json +936 -0
  147. package/src/assets/adapters/threads/wrench-web-adapter.json +692 -0
  148. package/src/assets/adapters/threads/wrench-web-adapter.v1.0.0.json +629 -0
  149. package/src/assets/adapters/threads/wrench-web-adapter.v1.1.0.json +623 -0
  150. package/src/assets/adapters/threads/wrench-web-adapter.v1.2.0.json +622 -0
  151. package/src/assets/adapters/threads/wrench-web-adapter.v1.3.0.json +622 -0
  152. package/src/assets/adapters/threads/wrench-web-adapter.v1.4.0.json +622 -0
  153. package/src/assets/adapters/threads/wrench-web-adapter.v1.5.0.json +699 -0
  154. package/src/assets/adapters/threads/wrench-web-adapter.v1.6.0.json +693 -0
  155. package/src/assets/adapters/threads/wrench-web-adapter.v1.7.0.json +692 -0
  156. package/src/assets/adapters/tiktok/wrench-web-adapter.json +762 -0
  157. package/src/assets/adapters/tiktok/wrench-web-adapter.v1.0.0.json +681 -0
  158. package/src/assets/adapters/tiktok/wrench-web-adapter.v1.1.0.json +708 -0
  159. package/src/assets/adapters/tiktok/wrench-web-adapter.v1.2.0.json +762 -0
  160. package/src/assets/adapters/tiktok/wrench-web-adapter.v1.3.0.json +762 -0
  161. package/src/assets/adapters/twitch/wrench-web-adapter.json +42 -0
  162. package/src/assets/adapters/twitch/wrench-web-adapter.v1.0.0.json +42 -0
  163. package/src/assets/adapters/whatsapp/wrench-web-adapter.json +400 -0
  164. package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.0.0.json +355 -0
  165. package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.1.0.json +386 -0
  166. package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.2.0.json +386 -0
  167. package/src/assets/adapters/whatsapp/wrench-web-adapter.v1.3.0.json +400 -0
  168. package/src/assets/adapters/x/wrench-adapter.json +749 -0
  169. package/src/assets/adapters/x/wrench-adapter.v1.0.0.json +700 -0
  170. package/src/assets/adapters/x/wrench-adapter.v1.1.0.json +707 -0
  171. package/src/assets/adapters/x/wrench-web-adapter.json +712 -0
  172. package/src/assets/adapters/x/wrench-web-adapter.v1.1.0.json +579 -0
  173. package/src/assets/adapters/x/wrench-web-adapter.v1.10.0.json +685 -0
  174. package/src/assets/adapters/x/wrench-web-adapter.v1.11.0.json +712 -0
  175. package/src/assets/adapters/x/wrench-web-adapter.v1.12.0.json +712 -0
  176. package/src/assets/adapters/x/wrench-web-adapter.v1.13.0.json +712 -0
  177. package/src/assets/adapters/x/wrench-web-adapter.v1.2.0.json +578 -0
  178. package/src/assets/adapters/x/wrench-web-adapter.v1.3.0.json +610 -0
  179. package/src/assets/adapters/x/wrench-web-adapter.v1.4.0.json +617 -0
  180. package/src/assets/adapters/x/wrench-web-adapter.v1.5.0.json +634 -0
  181. package/src/assets/adapters/x/wrench-web-adapter.v1.6.0.json +632 -0
  182. package/src/assets/adapters/x/wrench-web-adapter.v1.7.0.json +649 -0
  183. package/src/assets/adapters/x/wrench-web-adapter.v1.8.0.json +649 -0
  184. package/src/assets/adapters/x/wrench-web-adapter.v1.9.0.json +683 -0
  185. package/src/assets/adapters/youtube/wrench-web-adapter.json +561 -0
  186. package/src/assets/adapters/youtube/wrench-web-adapter.v1.0.0.json +465 -0
  187. package/src/assets/adapters/youtube/wrench-web-adapter.v1.1.0.json +519 -0
  188. package/src/assets/adapters/youtube/wrench-web-adapter.v1.2.0.json +561 -0
  189. package/src/assets/adapters/youtube/wrench-web-adapter.v1.3.0.json +561 -0
  190. package/src/assets/code-owned-provider-template/plugin.test.ts.template +54 -0
  191. package/src/assets/code-owned-provider-template/plugin.ts.template +102 -0
  192. package/src/assets/code-owned-provider-template/runtime.internal.test.ts.template +44 -0
  193. package/src/assets/code-owned-provider-template/runtime.ts.template +59 -0
  194. package/src/assets/code-owned-provider-template/wrench-adapter.json.template +33 -0
  195. package/src/assets/fonts/nebula-sans/LICENSE.txt +96 -0
  196. package/src/assets/fonts/nebula-sans/NebulaSans-Book.woff2 +0 -0
  197. package/src/assets/fonts/nebula-sans/PROVENANCE.md +14 -0
  198. package/src/assets.d.ts +4 -0
  199. package/src/auth.ts +1157 -0
  200. package/src/beeper-client-types.ts +169 -0
  201. package/src/beeper-client.ts +250 -0
  202. package/src/beeper-contact-interactions-cli.ts +180 -0
  203. package/src/beeper-contact-interactions.ts +1612 -0
  204. package/src/beeper-message-bundle-v1.ts +257 -0
  205. package/src/beeper-message-like-me-cli.ts +84 -0
  206. package/src/beeper-message-like-me-export.ts +2274 -0
  207. package/src/beeper-message-like-me-recovery.ts +931 -0
  208. package/src/beeper-message-like-me-source.ts +4034 -0
  209. package/src/browser-admission.ts +928 -0
  210. package/src/browser-snapshots.ts +260 -0
  211. package/src/browser.ts +3873 -0
  212. package/src/canonical-json.ts +60 -0
  213. package/src/catalog-cli.ts +638 -0
  214. package/src/cli.ts +208 -0
  215. package/src/client-types.ts +301 -0
  216. package/src/client.ts +2726 -0
  217. package/src/confirmed-write-failure.ts +44 -0
  218. package/src/confirmed-write-model.ts +72 -0
  219. package/src/confirmed-write-platform.ts +1055 -0
  220. package/src/confirmed-write-program.ts +224 -0
  221. package/src/confirmed-write-runtime.ts +16 -0
  222. package/src/cursor-token.ts +228 -0
  223. package/src/derivation-dnr-readiness.ts +367 -0
  224. package/src/derivation-file-chooser.ts +384 -0
  225. package/src/derivation-network-boundary.ts +835 -0
  226. package/src/derivation-network-guard.ts +782 -0
  227. package/src/derivation-network-proxy-helper.ts +440 -0
  228. package/src/derivation-network-proxy.ts +385 -0
  229. package/src/derive-command-helper.ts +693 -0
  230. package/src/derive-fixtures.ts +343 -0
  231. package/src/derive-review.ts +980 -0
  232. package/src/derive.ts +4754 -0
  233. package/src/fixtures/beeper-message-like-me-v1/accounts.ndjson +2 -0
  234. package/src/fixtures/beeper-message-like-me-v1/conversations.ndjson +1 -0
  235. package/src/fixtures/beeper-message-like-me-v1/manifest.json +1 -0
  236. package/src/fixtures/beeper-message-like-me-v1/messages.ndjson +2 -0
  237. package/src/fixtures/beeper-message-like-me-v1/participants.ndjson +3 -0
  238. package/src/fixtures/beeper-message-like-me-v1/reactions.ndjson +1 -0
  239. package/src/fixtures/beeper-message-like-me-v1/tombstones.ndjson +1 -0
  240. package/src/ghostget.ts +3672 -0
  241. package/src/gmail-capture.ts +1244 -0
  242. package/src/har-internal.ts +1242 -0
  243. package/src/har.ts +743 -0
  244. package/src/index.ts +218 -0
  245. package/src/invocation-read-platform.ts +55 -0
  246. package/src/invocation-read-program.ts +93 -0
  247. package/src/invocation-read-runtime.ts +11 -0
  248. package/src/linked-device-lifecycle-admission.ts +450 -0
  249. package/src/linked-device-lifecycle-journal.ts +1560 -0
  250. package/src/linked-device-lifecycle-runtime.ts +1602 -0
  251. package/src/local-cli-admission.ts +135 -0
  252. package/src/local-cli-contracts.ts +271 -0
  253. package/src/local-cli-execution.ts +98 -0
  254. package/src/local-cli-surface-contract.ts +1510 -0
  255. package/src/local-cli-tool-identity.ts +336 -0
  256. package/src/media/archive.ts +2325 -0
  257. package/src/media/args.ts +303 -0
  258. package/src/media/cli.ts +287 -0
  259. package/src/media/doctor.ts +475 -0
  260. package/src/media/ffmpeg.ts +1330 -0
  261. package/src/media/http-capture.ts +711 -0
  262. package/src/media/http-probe.ts +593 -0
  263. package/src/media/http.ts +387 -0
  264. package/src/media/index.ts +10 -0
  265. package/src/media/local-transcription.ts +613 -0
  266. package/src/media/lock.ts +321 -0
  267. package/src/media/manifest.ts +1712 -0
  268. package/src/media/metadata.ts +906 -0
  269. package/src/media/process.ts +723 -0
  270. package/src/media/revision.ts +166 -0
  271. package/src/media/runtime-closure.ts +1063 -0
  272. package/src/media/source-router.ts +187 -0
  273. package/src/media/transcriber-config.ts +1122 -0
  274. package/src/media/transcript-persistence-model.ts +15 -0
  275. package/src/media/transcript-persistence-platform.ts +89 -0
  276. package/src/media/transcript-persistence-program.ts +39 -0
  277. package/src/media/transcript-persistence-runtime.ts +24 -0
  278. package/src/media/transcript.ts +616 -0
  279. package/src/media/utf8-order.ts +30 -0
  280. package/src/media/whisper-cpp.ts +464 -0
  281. package/src/media/whisper-language.ts +58 -0
  282. package/src/media/yt-dlp.ts +439 -0
  283. package/src/message-like-me-agentic-messaging.ts +882 -0
  284. package/src/messaging-action-store.ts +793 -0
  285. package/src/messaging-runtime.ts +2136 -0
  286. package/src/messaging-store.ts +539 -0
  287. package/src/messaging-types.ts +1663 -0
  288. package/src/messaging.ts +334 -0
  289. package/src/model.ts +2671 -0
  290. package/src/oauth-google.ts +888 -0
  291. package/src/omni-client-types.ts +306 -0
  292. package/src/omni-client.ts +2225 -0
  293. package/src/omni-limits.ts +34 -0
  294. package/src/omni-model.ts +1614 -0
  295. package/src/omni-request.ts +379 -0
  296. package/src/omni-runtime.ts +1632 -0
  297. package/src/operation-deadline.ts +189 -0
  298. package/src/path-helper.ts +1656 -0
  299. package/src/pinned-https.ts +194 -0
  300. package/src/plan-assets.ts +832 -0
  301. package/src/platform-catalog.ts +1832 -0
  302. package/src/plugins/beeper-linked-device/plugin.ts +310 -0
  303. package/src/plugins/bluesky-web/plugin.ts +178 -0
  304. package/src/plugins/clasificados-web/plugin.ts +66 -0
  305. package/src/plugins/github-web/plugin.ts +80 -0
  306. package/src/plugins/gmail-official/plugin.ts +110 -0
  307. package/src/plugins/hacker-news-web/plugin.ts +52 -0
  308. package/src/plugins/imessage-direct/plugin.ts +195 -0
  309. package/src/plugins/imessage-direct/vendor/0001-fix-keep-AppleScript-send-payloads-out-of-child-argv.patch +1430 -0
  310. package/src/plugins/imessage-direct/vendor/0002-feat-rpc-add-exact-chat-lookup.patch +225 -0
  311. package/src/plugins/imessage-direct/vendor/provenance.json +80 -0
  312. package/src/plugins/linkedin-official/plugin.ts +84 -0
  313. package/src/plugins/linkedin-web/plugin.ts +412 -0
  314. package/src/plugins/meta-web/plugin.ts +317 -0
  315. package/src/plugins/reddit-web/flair.ts +492 -0
  316. package/src/plugins/reddit-web/plugin.ts +477 -0
  317. package/src/plugins/substack-web/plugin.ts +124 -0
  318. package/src/plugins/tiktok-web/plugin.ts +67 -0
  319. package/src/plugins/twitch-web/plugin.ts +54 -0
  320. package/src/plugins/whatsapp-linked-device/plugin.ts +105 -0
  321. package/src/plugins/x-official/plugin.ts +64 -0
  322. package/src/plugins/x-web/plugin.ts +488 -0
  323. package/src/plugins/youtube-web/plugin.ts +101 -0
  324. package/src/portable-run-recovery.ts +466 -0
  325. package/src/process-identity.ts +501 -0
  326. package/src/profile-clone-helper.ts +104 -0
  327. package/src/provider-catalog-views.ts +51 -0
  328. package/src/provider-context.ts +69 -0
  329. package/src/provider-contract-definitions-x.ts +45 -0
  330. package/src/provider-contract-definitions.ts +948 -0
  331. package/src/provider-contract-input-gmail.ts +176 -0
  332. package/src/provider-contract-input-linkedin.ts +77 -0
  333. package/src/provider-contract-input-x.ts +304 -0
  334. package/src/provider-contract-planning.ts +23 -0
  335. package/src/provider-contract-semantic-identity.ts +54 -0
  336. package/src/provider-contracts.ts +251 -0
  337. package/src/provider-http.ts +723 -0
  338. package/src/provider-plugin-auth.ts +36 -0
  339. package/src/provider-plugin-builtins.ts +229 -0
  340. package/src/provider-plugin-cleanup-barrier.ts +229 -0
  341. package/src/provider-plugin-cleanup-execution.ts +79 -0
  342. package/src/provider-plugin-cleanup-resource.ts +470 -0
  343. package/src/provider-plugin-contract-identity.ts +707 -0
  344. package/src/provider-plugin-host.ts +1972 -0
  345. package/src/provider-plugin-identifiers.ts +60 -0
  346. package/src/provider-plugin-invocation-lease.ts +889 -0
  347. package/src/provider-plugin-lifecycle-kernel.ts +566 -0
  348. package/src/provider-plugin-lifecycle.ts +1455 -0
  349. package/src/provider-plugin-module-analysis.ts +336 -0
  350. package/src/provider-plugin-package.ts +2530 -0
  351. package/src/provider-plugin-portable-authority.ts +106 -0
  352. package/src/provider-plugin-portable-catalog.ts +94 -0
  353. package/src/provider-plugin-portable-identity.ts +315 -0
  354. package/src/provider-plugin-portable-runtime.ts +2057 -0
  355. package/src/provider-plugin-protocol.ts +1625 -0
  356. package/src/provider-plugin-registry.ts +3342 -0
  357. package/src/provider-plugin-store.ts +2155 -0
  358. package/src/provider-plugin-test-fixture.ts +5 -0
  359. package/src/provider-plugin.ts +4907 -0
  360. package/src/provider-plugins.generated.ts +43 -0
  361. package/src/provider-plugins.ts +8 -0
  362. package/src/provider-subject.ts +41 -0
  363. package/src/provider.ts +315 -0
  364. package/src/providers/beeper-local-runtime.ts +5426 -0
  365. package/src/providers/beeper-local.ts +2739 -0
  366. package/src/providers/beeper-messaging.ts +462 -0
  367. package/src/providers/beeper-omni.ts +1159 -0
  368. package/src/providers/bluesky-web-runtime.ts +2895 -0
  369. package/src/providers/bluesky-web.ts +1614 -0
  370. package/src/providers/clasificados-web-runtime.ts +255 -0
  371. package/src/providers/clasificados-web.ts +503 -0
  372. package/src/providers/contact-projection.ts +301 -0
  373. package/src/providers/github-read-model.ts +94 -0
  374. package/src/providers/github-read-platform.ts +165 -0
  375. package/src/providers/github-read-program.ts +97 -0
  376. package/src/providers/github-web-runtime.ts +320 -0
  377. package/src/providers/github-web.ts +403 -0
  378. package/src/providers/gmail-api.ts +2834 -0
  379. package/src/providers/gmail-omni.ts +830 -0
  380. package/src/providers/gmail.ts +1130 -0
  381. package/src/providers/hacker-news-web-runtime.ts +220 -0
  382. package/src/providers/hacker-news-web.ts +809 -0
  383. package/src/providers/imessage-direct-install.ts +354 -0
  384. package/src/providers/imessage-direct-messaging.ts +532 -0
  385. package/src/providers/imessage-direct-omni.ts +400 -0
  386. package/src/providers/imessage-direct-runtime.ts +1498 -0
  387. package/src/providers/imessage-direct.ts +351 -0
  388. package/src/providers/instagram-video-foundations.ts +913 -0
  389. package/src/providers/instagram-web-profile-browser.ts +600 -0
  390. package/src/providers/iso-bmff.ts +338 -0
  391. package/src/providers/linkedin-company-platform.ts +42 -0
  392. package/src/providers/linkedin-company-program.ts +88 -0
  393. package/src/providers/linkedin-contact-failure.ts +84 -0
  394. package/src/providers/linkedin-contact-platform.ts +45 -0
  395. package/src/providers/linkedin-contact-program.ts +91 -0
  396. package/src/providers/linkedin-profile-activity-failure.ts +76 -0
  397. package/src/providers/linkedin-profile-activity-platform.ts +41 -0
  398. package/src/providers/linkedin-profile-activity-program.ts +67 -0
  399. package/src/providers/linkedin-read-failure.ts +52 -0
  400. package/src/providers/linkedin-self-platform.ts +53 -0
  401. package/src/providers/linkedin-self-program.ts +122 -0
  402. package/src/providers/linkedin-web-article-browser.ts +794 -0
  403. package/src/providers/linkedin-web-bootstrap.ts +146 -0
  404. package/src/providers/linkedin-web-contact.ts +830 -0
  405. package/src/providers/linkedin-web-feed-browser.ts +832 -0
  406. package/src/providers/linkedin-web-feed.ts +864 -0
  407. package/src/providers/linkedin-web-post-browser.ts +653 -0
  408. package/src/providers/linkedin-web-profile-browser.ts +683 -0
  409. package/src/providers/linkedin-web-runtime.ts +2388 -0
  410. package/src/providers/linkedin-web.ts +3305 -0
  411. package/src/providers/linkedin.ts +1885 -0
  412. package/src/providers/meta-bootstrap.ts +1043 -0
  413. package/src/providers/meta-facebook-group.ts +690 -0
  414. package/src/providers/meta-marketplace-relay.ts +747 -0
  415. package/src/providers/meta-omni.ts +373 -0
  416. package/src/providers/meta-relay-bundle.ts +1049 -0
  417. package/src/providers/meta-web-descriptors.ts +1740 -0
  418. package/src/providers/meta-web-runtime.ts +3388 -0
  419. package/src/providers/meta-web.ts +2931 -0
  420. package/src/providers/read-failure.ts +139 -0
  421. package/src/providers/reddit-omni.ts +462 -0
  422. package/src/providers/reddit-web-runtime.ts +1929 -0
  423. package/src/providers/reddit-web.ts +1611 -0
  424. package/src/providers/rental-listings.ts +460 -0
  425. package/src/providers/substack-omni.ts +406 -0
  426. package/src/providers/substack-video-mp4.ts +33 -0
  427. package/src/providers/substack-web-runtime.ts +2812 -0
  428. package/src/providers/substack-web.ts +1502 -0
  429. package/src/providers/tiktok-video-mp4.ts +33 -0
  430. package/src/providers/tiktok-web-runtime.ts +770 -0
  431. package/src/providers/tiktok-web.ts +2786 -0
  432. package/src/providers/twitch-web-runtime.ts +226 -0
  433. package/src/providers/twitch-web.ts +274 -0
  434. package/src/providers/whatsapp-account-identity.ts +55 -0
  435. package/src/providers/whatsapp-contact-projection-helper.ts +874 -0
  436. package/src/providers/whatsapp-contact-projection-protocol.ts +403 -0
  437. package/src/providers/whatsapp-interaction-projection-helper.ts +1452 -0
  438. package/src/providers/whatsapp-interaction-projection-protocol.ts +462 -0
  439. package/src/providers/whatsapp-message-export-projection-protocol.ts +532 -0
  440. package/src/providers/whatsapp-messaging.ts +132 -0
  441. package/src/providers/whatsapp-omni.ts +586 -0
  442. package/src/providers/whatsapp-web-runtime.ts +3108 -0
  443. package/src/providers/whatsapp-web.ts +1037 -0
  444. package/src/providers/x-image-provenance.ts +523 -0
  445. package/src/providers/x-made-with-ai.ts +106 -0
  446. package/src/providers/x-transaction-id.ts +413 -0
  447. package/src/providers/x-web-runtime.ts +3485 -0
  448. package/src/providers/x-web.ts +2332 -0
  449. package/src/providers/x.ts +1889 -0
  450. package/src/providers/youtube-web-runtime.ts +1400 -0
  451. package/src/providers/youtube-web.ts +1221 -0
  452. package/src/read-admission-runtime.ts +91 -0
  453. package/src/read-client.ts +521 -0
  454. package/src/read-effect-platform.ts +7 -0
  455. package/src/read-effect-runtime.ts +14 -0
  456. package/src/read-effect.ts +52 -0
  457. package/src/read-projection-admission.ts +866 -0
  458. package/src/read-projections.ts +3151 -0
  459. package/src/recovery.ts +1492 -0
  460. package/src/reviewed-template.ts +146 -0
  461. package/src/run-journal.ts +1272 -0
  462. package/src/runtime.ts +5116 -0
  463. package/src/scripts/generate-provider-plugin-catalog.ts +121 -0
  464. package/src/scripts/install-whatsapp-protocol.sh +232 -0
  465. package/src/scripts/recover-install-stages.ts +1491 -0
  466. package/src/scripts/resolve-state-home.ts +30 -0
  467. package/src/scripts/scaffold-web-provider.ts +973 -0
  468. package/src/scripts/sync-bundled-adapters.ts +695 -0
  469. package/src/session-secrets.ts +1183 -0
  470. package/src/state-helper.bunfig.toml +2 -0
  471. package/src/state-helper.ts +2914 -0
  472. package/src/storage.ts +3842 -0
  473. package/src/transport-policy.ts +2 -0
  474. package/src/usage.ts +182 -0
  475. package/src/version.ts +2 -0
  476. package/src/web-session-authentication-policy.ts +136 -0
  477. package/src/web-session-cleanup-admission.ts +2344 -0
  478. package/src/web-session-cleanup-join.ts +99 -0
  479. package/src/web-session-client.ts +996 -0
  480. package/src/web-session-contract-definitions.ts +820 -0
  481. package/src/web-session-contract-planning.ts +42 -0
  482. package/src/web-session-contracts.ts +228 -0
  483. package/src/web-session-cookies.ts +500 -0
  484. package/src/web-session-execution.ts +385 -0
  485. package/src/web-session-http.ts +373 -0
  486. package/src/web-session-read-errors.ts +19 -0
  487. package/src/web-session-read-runtime.ts +124 -0
  488. package/src/web-session-recovery.ts +818 -0
  489. package/src/web-session-sites.ts +58 -0
  490. package/src/web-session-template.ts +994 -0
  491. package/src/web-session.ts +93 -0
  492. package/src/whatsapp-client-binding.ts +21 -0
  493. package/src/whatsapp-client-types.ts +75 -0
  494. package/src/whatsapp-client.ts +395 -0
  495. package/src/whatsapp-export-coordinate.ts +17 -0
  496. package/src/whatsapp-message-bundle-v2.ts +195 -0
  497. package/src/whatsapp-message-like-me-cli.ts +195 -0
  498. package/src/whatsapp-message-like-me-export.ts +1072 -0
  499. package/src/whatsapp-message-like-me-source.ts +1012 -0
  500. package/tsconfig.json +67 -0
@@ -0,0 +1,1122 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { constants, existsSync, type BigIntStats } from "node:fs";
3
+ import {
4
+ access,
5
+ chmod,
6
+ link,
7
+ lstat,
8
+ mkdir,
9
+ open,
10
+ realpath,
11
+ rename,
12
+ unlink,
13
+ } from "node:fs/promises";
14
+ import { homedir } from "node:os";
15
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
16
+ import {
17
+ findExecutable,
18
+ runProcess,
19
+ type CommandArgv,
20
+ type ExecutableDiscoveryOptions,
21
+ type ProcessResult,
22
+ type RunProcessOptions,
23
+ } from "./process";
24
+ import {
25
+ attestRuntimeClosure,
26
+ buildWhisperRuntimeEnvironment,
27
+ parseRuntimeClosureRecord,
28
+ sameRuntimeClosure,
29
+ stripRuntimeClosureRecord,
30
+ type RUNTIME_CLOSURE_PROFILE,
31
+ type AttestRuntimeClosureOptions,
32
+ type RuntimeClosureAttestation,
33
+ type RuntimeClosureRecord,
34
+ } from "./runtime-closure";
35
+
36
+ export const WHISPER_CPP_PROFILE = "wrench-media-whisper-cpp-v1" as const;
37
+
38
+ const CONFIG_SCHEMA_VERSION = 1 as const;
39
+ const CONFIG_DIRECTORY_MODE = 0o700;
40
+ const CONFIG_FILE_MODE = 0o600;
41
+ const MAX_CONFIG_BYTES = 2 * 1024 * 1024;
42
+ const MAX_EXECUTABLE_BYTES = 512 * 1024 * 1024;
43
+ const MAX_MODEL_BYTES = 8 * 1024 * 1024 * 1024;
44
+ const MAX_PATH_CODE_UNITS = 4_096;
45
+ const FILE_READ_CHUNK_BYTES = 1024 * 1024;
46
+ const HELP_TIMEOUT_MS = 5_000;
47
+ const HELP_OUTPUT_LIMIT_BYTES = 256 * 1024;
48
+ const MAX_PUBLISH_ATTEMPTS = 8;
49
+ const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
50
+ const REQUIRED_HELP_FLAGS = [
51
+ "--model",
52
+ "--file",
53
+ "--language",
54
+ "--threads",
55
+ "--processors",
56
+ "--no-gpu",
57
+ "--output-vtt",
58
+ "--output-json-full",
59
+ "--output-file",
60
+ "--no-prints",
61
+ ] as const;
62
+
63
+ export type WhisperCppTranscriberDescriptor = Readonly<{
64
+ adapter: "whisper-cpp";
65
+ profile: typeof WHISPER_CPP_PROFILE;
66
+ executableSha256: string;
67
+ modelSha256: string;
68
+ modelBytes: number;
69
+ runtimeProfile: typeof RUNTIME_CLOSURE_PROFILE;
70
+ runtimeSha256: string;
71
+ runtimeDependencyCount: number;
72
+ }>;
73
+
74
+ export type ReadyTranscriber = Readonly<{
75
+ executablePath: string;
76
+ modelPath: string;
77
+ descriptor: WhisperCppTranscriberDescriptor;
78
+ runtimeClosure: RuntimeClosureAttestation;
79
+ }>;
80
+
81
+ export type SetupWhisperCppTranscriberOptions = Readonly<{
82
+ modelPath: string;
83
+ executablePath?: string;
84
+ replace?: boolean;
85
+ env?: Readonly<Record<string, string | undefined>>;
86
+ homeDirectory?: string;
87
+ configPath?: string;
88
+ }>;
89
+
90
+ export type LoadConfiguredTranscriberOptions = Readonly<{
91
+ env?: Readonly<Record<string, string | undefined>>;
92
+ homeDirectory?: string;
93
+ configPath?: string;
94
+ }>;
95
+
96
+ export type ConfiguredTranscriberInvalidReason =
97
+ | "invalid-location"
98
+ | "unsafe-config"
99
+ | "config-too-large"
100
+ | "config-permissions"
101
+ | "unstable-config"
102
+ | "malformed-config"
103
+ | "missing-executable"
104
+ | "invalid-executable"
105
+ | "executable-too-large"
106
+ | "executable-hash-mismatch"
107
+ | "capability-mismatch"
108
+ | "runtime-attestation-failed"
109
+ | "runtime-closure-mismatch"
110
+ | "missing-model"
111
+ | "invalid-model"
112
+ | "model-too-large"
113
+ | "model-hash-mismatch"
114
+ | "unstable-file";
115
+
116
+ export type LoadConfiguredTranscriberResult =
117
+ | Readonly<{ kind: "not-configured" }>
118
+ | Readonly<{ kind: "ready"; transcriber: ReadyTranscriber }>
119
+ | Readonly<{
120
+ kind: "invalid";
121
+ reason: ConfiguredTranscriberInvalidReason;
122
+ message: string;
123
+ }>;
124
+
125
+ export type ReverifyReadyTranscriberResult = Exclude<
126
+ LoadConfiguredTranscriberResult,
127
+ Readonly<{ kind: "not-configured" }>
128
+ >;
129
+
130
+ export type WhisperCppTranscriberSetupErrorCode =
131
+ | "INVALID_OPTIONS"
132
+ | "EXECUTABLE_NOT_FOUND"
133
+ | "INVALID_EXECUTABLE"
134
+ | "INVALID_MODEL"
135
+ | "FILE_TOO_LARGE"
136
+ | "UNSTABLE_FILE"
137
+ | "CAPABILITY_MISMATCH"
138
+ | "RUNTIME_ATTESTATION_FAILED"
139
+ | "RUNTIME_CLOSURE_MISMATCH"
140
+ | "CONFIG_EXISTS"
141
+ | "CONFIG_UNSAFE"
142
+ | "CONFIG_WRITE_FAILED"
143
+ | "CONFIG_RACE";
144
+
145
+ export class WhisperCppTranscriberSetupError extends Error {
146
+ readonly code: WhisperCppTranscriberSetupErrorCode;
147
+
148
+ constructor(code: WhisperCppTranscriberSetupErrorCode, message: string) {
149
+ super(message);
150
+ this.name = "WhisperCppTranscriberSetupError";
151
+ this.code = code;
152
+ }
153
+ }
154
+
155
+ export type WhisperCppTranscriberDependencies = Readonly<{
156
+ findExecutable: (
157
+ name: string,
158
+ options: ExecutableDiscoveryOptions,
159
+ ) => Promise<string | null>;
160
+ runProcess: (argv: CommandArgv, options: RunProcessOptions) => Promise<ProcessResult>;
161
+ attestRuntimeClosure: (
162
+ options: AttestRuntimeClosureOptions,
163
+ ) => Promise<RuntimeClosureAttestation>;
164
+ randomToken: () => string;
165
+ }>;
166
+
167
+ export type ReverifyReadyTranscriberOptions = Readonly<{
168
+ signal?: AbortSignal;
169
+ }>;
170
+
171
+ interface WhisperCppConfigDocument {
172
+ readonly schemaVersion: typeof CONFIG_SCHEMA_VERSION;
173
+ readonly adapter: "whisper-cpp";
174
+ readonly profile: typeof WHISPER_CPP_PROFILE;
175
+ readonly executablePath: string;
176
+ readonly modelPath: string;
177
+ readonly executableSha256: string;
178
+ readonly modelSha256: string;
179
+ readonly modelBytes: number;
180
+ readonly runtimeClosure: RuntimeClosureRecord;
181
+ }
182
+
183
+ type VerifiedFileRole = "executable" | "model";
184
+ type VerifiedFileFailureReason = "missing" | "unsafe" | "too-large" | "unstable";
185
+
186
+ type VerifiedFileResult =
187
+ | Readonly<{
188
+ ok: true;
189
+ physicalPath: string;
190
+ sha256: string;
191
+ bytes: number;
192
+ }>
193
+ | Readonly<{ ok: false; reason: VerifiedFileFailureReason }>;
194
+
195
+ type ConfigObservation =
196
+ | Readonly<{ kind: "missing" }>
197
+ | Readonly<{
198
+ kind: "invalid";
199
+ reason:
200
+ | "unsafe-config"
201
+ | "config-too-large"
202
+ | "config-permissions"
203
+ | "unstable-config"
204
+ | "malformed-config";
205
+ }>
206
+ | Readonly<{ kind: "configured"; document: WhisperCppConfigDocument }>;
207
+
208
+ interface FileIdentity {
209
+ readonly dev: bigint;
210
+ readonly ino: bigint;
211
+ readonly size: bigint;
212
+ readonly mode: bigint;
213
+ readonly mtimeNs: bigint;
214
+ readonly ctimeNs: bigint;
215
+ }
216
+
217
+ const defaultDependencies: WhisperCppTranscriberDependencies = {
218
+ findExecutable: (name, options) => findExecutable(name, options),
219
+ runProcess: (argv, options) => runProcess(argv, options),
220
+ attestRuntimeClosure: (options) => attestRuntimeClosure(options),
221
+ randomToken: () => randomUUID(),
222
+ };
223
+
224
+ const INVALID_MESSAGES = {
225
+ "invalid-location": "The transcriber configuration location is invalid.",
226
+ "unsafe-config": "The transcriber configuration is not a private regular file.",
227
+ "config-too-large": "The transcriber configuration exceeds Ghostget media's size limit.",
228
+ "config-permissions": "The transcriber configuration must have mode 0600.",
229
+ "unstable-config": "The transcriber configuration changed while it was being read.",
230
+ "malformed-config": "The transcriber configuration does not match Ghostget media's schema.",
231
+ "missing-executable": "The configured transcriber executable is missing.",
232
+ "invalid-executable": "The configured transcriber executable is not a physical executable file.",
233
+ "executable-too-large": "The configured transcriber executable exceeds Ghostget media's size limit.",
234
+ "executable-hash-mismatch": "The configured transcriber executable no longer matches its recorded identity.",
235
+ "capability-mismatch": "The configured executable does not provide Ghostget media's required whisper.cpp capabilities.",
236
+ "runtime-attestation-failed": "Ghostget media could not attest the configured executable's native runtime closure.",
237
+ "runtime-closure-mismatch": "The configured executable's native runtime closure no longer matches its recorded identity.",
238
+ "missing-model": "The configured whisper.cpp model is missing.",
239
+ "invalid-model": "The configured whisper.cpp model is not a physical regular file.",
240
+ "model-too-large": "The configured whisper.cpp model exceeds Ghostget media's size limit.",
241
+ "model-hash-mismatch": "The configured whisper.cpp model no longer matches its recorded identity.",
242
+ "unstable-file": "A configured transcriber file changed while it was being verified.",
243
+ } as const satisfies Readonly<Record<ConfiguredTranscriberInvalidReason, string>>;
244
+
245
+ function invalid(
246
+ reason: ConfiguredTranscriberInvalidReason,
247
+ ): Extract<LoadConfiguredTranscriberResult, { readonly kind: "invalid" }> {
248
+ return { kind: "invalid", reason, message: INVALID_MESSAGES[reason] };
249
+ }
250
+
251
+ function isErrno(error: unknown, code: string): boolean {
252
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
253
+ }
254
+
255
+ function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
256
+ return typeof value === "object" && value !== null && !Array.isArray(value);
257
+ }
258
+
259
+ function hasControlCharacter(value: string): boolean {
260
+ for (const character of value) {
261
+ const point = character.codePointAt(0);
262
+ if (point !== undefined && (point <= 0x1f || (point >= 0x7f && point <= 0x9f))) {
263
+ return true;
264
+ }
265
+ }
266
+ return false;
267
+ }
268
+
269
+ function isSafeAbsolutePath(value: unknown): value is string {
270
+ return typeof value === "string"
271
+ && value.length > 0
272
+ && value.length <= MAX_PATH_CODE_UNITS
273
+ && !hasControlCharacter(value)
274
+ && isAbsolute(value)
275
+ && resolve(value) === value;
276
+ }
277
+
278
+ function configurationPath(options: LoadConfiguredTranscriberOptions): string | null {
279
+ if (options.configPath !== undefined) {
280
+ return isSafeAbsolutePath(options.configPath) ? options.configPath : null;
281
+ }
282
+
283
+ const homeDirectory = options.homeDirectory ?? homedir();
284
+ if (!isSafeAbsolutePath(homeDirectory)) return null;
285
+ const env = options.env ?? process.env;
286
+ const configuredRoot = env.XDG_CONFIG_HOME;
287
+ if (configuredRoot !== undefined && configuredRoot.length > 0 && !isSafeAbsolutePath(configuredRoot)) return null;
288
+ const configRoot = configuredRoot !== undefined && configuredRoot.length > 0
289
+ ? configuredRoot : join(homeDirectory, ".config");
290
+ const current = join(configRoot, "ghostget", "media");
291
+ const legacy = join(configRoot, "wrench", "media");
292
+ // Keep an installed transcriber and its interrupted publication state in place.
293
+ const existing = [current, legacy].filter((path) => existsSync(path));
294
+ if (existing.length > 1) return null;
295
+ return join(existing[0] ?? current, "transcriber.json");
296
+ }
297
+
298
+ function fileIdentity(metadata: BigIntStats): FileIdentity {
299
+ return {
300
+ dev: metadata.dev,
301
+ ino: metadata.ino,
302
+ size: metadata.size,
303
+ mode: metadata.mode,
304
+ mtimeNs: metadata.mtimeNs,
305
+ ctimeNs: metadata.ctimeNs,
306
+ };
307
+ }
308
+
309
+ function sameFileIdentity(left: FileIdentity, right: FileIdentity): boolean {
310
+ return left.dev === right.dev
311
+ && left.ino === right.ino
312
+ && left.size === right.size
313
+ && left.mode === right.mode
314
+ && left.mtimeNs === right.mtimeNs
315
+ && left.ctimeNs === right.ctimeNs;
316
+ }
317
+
318
+ async function verifyPhysicalFile(
319
+ requestedPath: string,
320
+ role: VerifiedFileRole,
321
+ ): Promise<VerifiedFileResult> {
322
+ if (!isSafeAbsolutePath(requestedPath)) return { ok: false, reason: "unsafe" };
323
+
324
+ let physicalPath: string;
325
+ try {
326
+ physicalPath = await realpath(requestedPath);
327
+ } catch (error) {
328
+ return { ok: false, reason: isErrno(error, "ENOENT") ? "missing" : "unsafe" };
329
+ }
330
+ if (!isSafeAbsolutePath(physicalPath)) return { ok: false, reason: "unsafe" };
331
+
332
+ let observed: BigIntStats;
333
+ try {
334
+ observed = await lstat(physicalPath, { bigint: true });
335
+ } catch (error) {
336
+ return { ok: false, reason: isErrno(error, "ENOENT") ? "missing" : "unsafe" };
337
+ }
338
+ const maximumBytes = role === "executable" ? MAX_EXECUTABLE_BYTES : MAX_MODEL_BYTES;
339
+ if (!observed.isFile() || observed.isSymbolicLink() || observed.size <= 0n) {
340
+ return { ok: false, reason: "unsafe" };
341
+ }
342
+ if (observed.size > BigInt(maximumBytes)) return { ok: false, reason: "too-large" };
343
+
344
+ let handle;
345
+ try {
346
+ handle = await open(
347
+ physicalPath,
348
+ constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
349
+ );
350
+ } catch (error) {
351
+ return { ok: false, reason: isErrno(error, "ENOENT") ? "missing" : "unsafe" };
352
+ }
353
+
354
+ try {
355
+ const opened = await handle.stat({ bigint: true });
356
+ const expected = fileIdentity(opened);
357
+ if (!opened.isFile() || !sameFileIdentity(fileIdentity(observed), expected)) {
358
+ return { ok: false, reason: "unstable" };
359
+ }
360
+ const bytes = Number(opened.size);
361
+ const hash = createHash("sha256");
362
+ const buffer = Buffer.allocUnsafe(Math.min(FILE_READ_CHUNK_BYTES, bytes));
363
+ let offset = 0;
364
+ while (offset < bytes) {
365
+ const length = Math.min(buffer.byteLength, bytes - offset);
366
+ const result = await handle.read(buffer, 0, length, offset);
367
+ if (result.bytesRead === 0) return { ok: false, reason: "unstable" };
368
+ hash.update(buffer.subarray(0, result.bytesRead));
369
+ offset += result.bytesRead;
370
+ }
371
+ const overflow = new Uint8Array(1);
372
+ if ((await handle.read(overflow, 0, 1, bytes)).bytesRead !== 0) {
373
+ return { ok: false, reason: "unstable" };
374
+ }
375
+ const [finished, finalPath] = await Promise.all([
376
+ handle.stat({ bigint: true }),
377
+ lstat(physicalPath, { bigint: true }),
378
+ ]);
379
+ if (
380
+ !finished.isFile()
381
+ || !finalPath.isFile()
382
+ || finalPath.isSymbolicLink()
383
+ || !sameFileIdentity(expected, fileIdentity(finished))
384
+ || !sameFileIdentity(expected, fileIdentity(finalPath))
385
+ ) {
386
+ return { ok: false, reason: "unstable" };
387
+ }
388
+ if (role === "executable") {
389
+ try {
390
+ await access(physicalPath, constants.X_OK);
391
+ } catch {
392
+ return { ok: false, reason: "unsafe" };
393
+ }
394
+ }
395
+ return { ok: true, physicalPath, sha256: hash.digest("hex"), bytes };
396
+ } catch (error) {
397
+ return { ok: false, reason: isErrno(error, "ENOENT") ? "missing" : "unstable" };
398
+ } finally {
399
+ try {
400
+ await handle.close();
401
+ } catch {
402
+ // Verification already owns a stable result; never expose host diagnostics.
403
+ }
404
+ }
405
+ }
406
+
407
+ function helpHasFlag(help: string, flag: string): boolean {
408
+ const escaped = flag.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
409
+ return new RegExp(`(?:^|[\\s,])${escaped}(?=$|[\\s,=])`, "u").test(help);
410
+ }
411
+
412
+ async function verifiesWhisperCppCapabilities(
413
+ executablePath: string,
414
+ dependencies: WhisperCppTranscriberDependencies,
415
+ ): Promise<boolean> {
416
+ let result: ProcessResult;
417
+ try {
418
+ result = await dependencies.runProcess([executablePath, "--help"], {
419
+ env: buildWhisperRuntimeEnvironment(),
420
+ timeoutMs: HELP_TIMEOUT_MS,
421
+ maxStdoutBytes: HELP_OUTPUT_LIMIT_BYTES,
422
+ maxStderrBytes: HELP_OUTPUT_LIMIT_BYTES,
423
+ });
424
+ } catch {
425
+ return false;
426
+ }
427
+ if (!result.ok || result.stdoutTruncated || result.stderrTruncated) return false;
428
+ const help = `${result.stdout}\n${result.stderr}`;
429
+ return REQUIRED_HELP_FLAGS.every((flag) => helpHasFlag(help, flag));
430
+ }
431
+
432
+ type ParseConfigResult =
433
+ | Readonly<{ kind: "configured"; document: WhisperCppConfigDocument }>
434
+ | Readonly<{ kind: "malformed" }>;
435
+
436
+ function parseConfig(value: unknown): ParseConfigResult {
437
+ if (!isRecord(value)) return { kind: "malformed" };
438
+ const expectedKeys = [
439
+ "adapter",
440
+ "executablePath",
441
+ "executableSha256",
442
+ "modelBytes",
443
+ "modelPath",
444
+ "modelSha256",
445
+ "profile",
446
+ "runtimeClosure",
447
+ "schemaVersion",
448
+ ];
449
+ if (JSON.stringify(Object.keys(value).toSorted()) !== JSON.stringify(expectedKeys)) {
450
+ return { kind: "malformed" };
451
+ }
452
+
453
+ const schemaVersion = value["schemaVersion"];
454
+ const adapter = value["adapter"];
455
+ const profile = value["profile"];
456
+ const executablePath = value["executablePath"];
457
+ const modelPath = value["modelPath"];
458
+ const executableSha256 = value["executableSha256"];
459
+ const modelSha256 = value["modelSha256"];
460
+ const modelBytes = value["modelBytes"];
461
+ const runtimeClosure = parseRuntimeClosureRecord(value["runtimeClosure"]);
462
+ if (
463
+ schemaVersion !== CONFIG_SCHEMA_VERSION
464
+ || adapter !== "whisper-cpp"
465
+ || profile !== WHISPER_CPP_PROFILE
466
+ || !isSafeAbsolutePath(executablePath)
467
+ || !isSafeAbsolutePath(modelPath)
468
+ || typeof executableSha256 !== "string"
469
+ || !SHA256_PATTERN.test(executableSha256)
470
+ || typeof modelSha256 !== "string"
471
+ || !SHA256_PATTERN.test(modelSha256)
472
+ || typeof modelBytes !== "number"
473
+ || !Number.isSafeInteger(modelBytes)
474
+ || modelBytes <= 0
475
+ || modelBytes > MAX_MODEL_BYTES
476
+ || !runtimeClosure.ok
477
+ || runtimeClosure.record.executableSha256 !== executableSha256
478
+ ) return { kind: "malformed" };
479
+
480
+ return {
481
+ kind: "configured",
482
+ document: {
483
+ schemaVersion,
484
+ adapter,
485
+ profile,
486
+ executablePath,
487
+ modelPath,
488
+ executableSha256,
489
+ modelSha256,
490
+ modelBytes,
491
+ runtimeClosure: runtimeClosure.record,
492
+ },
493
+ };
494
+ }
495
+
496
+ async function observeConfig(path: string): Promise<ConfigObservation> {
497
+ let observed: BigIntStats;
498
+ try {
499
+ observed = await lstat(path, { bigint: true });
500
+ } catch (error) {
501
+ return isErrno(error, "ENOENT")
502
+ ? { kind: "missing" }
503
+ : { kind: "invalid", reason: "unsafe-config" };
504
+ }
505
+ if (!observed.isFile() || observed.isSymbolicLink()) {
506
+ return { kind: "invalid", reason: "unsafe-config" };
507
+ }
508
+ if (observed.size > BigInt(MAX_CONFIG_BYTES)) {
509
+ return { kind: "invalid", reason: "config-too-large" };
510
+ }
511
+ if (process.platform !== "win32" && Number(observed.mode & 0o777n) !== CONFIG_FILE_MODE) {
512
+ return { kind: "invalid", reason: "config-permissions" };
513
+ }
514
+
515
+ let handle;
516
+ try {
517
+ handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
518
+ } catch (error) {
519
+ return isErrno(error, "ENOENT")
520
+ ? { kind: "missing" }
521
+ : { kind: "invalid", reason: "unsafe-config" };
522
+ }
523
+ try {
524
+ const opened = await handle.stat({ bigint: true });
525
+ const expected = fileIdentity(opened);
526
+ if (!opened.isFile() || !sameFileIdentity(fileIdentity(observed), expected)) {
527
+ return { kind: "invalid", reason: "unstable-config" };
528
+ }
529
+ if (opened.size > BigInt(MAX_CONFIG_BYTES)) {
530
+ return { kind: "invalid", reason: "config-too-large" };
531
+ }
532
+ const bytes = new Uint8Array(Number(opened.size));
533
+ let offset = 0;
534
+ while (offset < bytes.byteLength) {
535
+ const result = await handle.read(bytes, offset, bytes.byteLength - offset, offset);
536
+ if (result.bytesRead === 0) return { kind: "invalid", reason: "unstable-config" };
537
+ offset += result.bytesRead;
538
+ }
539
+ const overflow = new Uint8Array(1);
540
+ if ((await handle.read(overflow, 0, 1, bytes.byteLength)).bytesRead !== 0) {
541
+ return { kind: "invalid", reason: "unstable-config" };
542
+ }
543
+ const [finished, finalPath] = await Promise.all([
544
+ handle.stat({ bigint: true }),
545
+ lstat(path, { bigint: true }),
546
+ ]);
547
+ if (
548
+ !finished.isFile()
549
+ || !finalPath.isFile()
550
+ || finalPath.isSymbolicLink()
551
+ || !sameFileIdentity(expected, fileIdentity(finished))
552
+ || !sameFileIdentity(expected, fileIdentity(finalPath))
553
+ ) {
554
+ return { kind: "invalid", reason: "unstable-config" };
555
+ }
556
+ if (bytes[0] === 0xef && bytes[1] === 0xbb && bytes[2] === 0xbf) {
557
+ return { kind: "invalid", reason: "malformed-config" };
558
+ }
559
+ let value: unknown;
560
+ try {
561
+ const source = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
562
+ value = JSON.parse(source) as unknown;
563
+ } catch {
564
+ return { kind: "invalid", reason: "malformed-config" };
565
+ }
566
+ const parsed = parseConfig(value);
567
+ return parsed.kind === "malformed"
568
+ ? { kind: "invalid", reason: "malformed-config" }
569
+ : parsed;
570
+ } catch (error) {
571
+ return isErrno(error, "ENOENT")
572
+ ? { kind: "missing" }
573
+ : { kind: "invalid", reason: "unstable-config" };
574
+ } finally {
575
+ try {
576
+ await handle.close();
577
+ } catch {
578
+ // The read result is already bounded and host diagnostics stay private.
579
+ }
580
+ }
581
+ }
582
+
583
+ function configSource(document: WhisperCppConfigDocument): string {
584
+ return `${JSON.stringify(document, null, 2)}\n`;
585
+ }
586
+
587
+ function sameConfig(left: WhisperCppConfigDocument, right: WhisperCppConfigDocument): boolean {
588
+ return configSource(left) === configSource(right);
589
+ }
590
+
591
+ async function prepareConfigDestination(requestedPath: string): Promise<string | null> {
592
+ const requestedDirectory = dirname(requestedPath);
593
+ try {
594
+ await mkdir(requestedDirectory, { recursive: true, mode: CONFIG_DIRECTORY_MODE });
595
+ const metadata = await lstat(requestedDirectory);
596
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) return null;
597
+ await chmod(requestedDirectory, CONFIG_DIRECTORY_MODE);
598
+ const physicalDirectory = await realpath(requestedDirectory);
599
+ return join(physicalDirectory, basename(requestedPath));
600
+ } catch {
601
+ return null;
602
+ }
603
+ }
604
+
605
+ async function createTemporaryConfig(
606
+ destination: string,
607
+ source: string,
608
+ dependencies: WhisperCppTranscriberDependencies,
609
+ ): Promise<string> {
610
+ for (let attempt = 0; attempt < MAX_PUBLISH_ATTEMPTS; attempt += 1) {
611
+ let token: string;
612
+ try {
613
+ token = dependencies.randomToken().replace(/[^A-Za-z0-9-]/gu, "").slice(0, 64);
614
+ } catch {
615
+ throw new WhisperCppTranscriberSetupError(
616
+ "CONFIG_WRITE_FAILED",
617
+ "Ghostget media could not allocate a private transcriber configuration file.",
618
+ );
619
+ }
620
+ if (token.length === 0) continue;
621
+ const temporary = join(dirname(destination), `.${basename(destination)}.${token}.${attempt}.tmp`);
622
+ let handle;
623
+ try {
624
+ handle = await open(
625
+ temporary,
626
+ constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW,
627
+ CONFIG_FILE_MODE,
628
+ );
629
+ } catch (error) {
630
+ if (isErrno(error, "EEXIST")) continue;
631
+ throw new WhisperCppTranscriberSetupError(
632
+ "CONFIG_WRITE_FAILED",
633
+ "Ghostget media could not create the private transcriber configuration.",
634
+ );
635
+ }
636
+ try {
637
+ await handle.writeFile(source, { encoding: "utf8" });
638
+ await handle.chmod(CONFIG_FILE_MODE);
639
+ await handle.sync();
640
+ return temporary;
641
+ } catch {
642
+ try {
643
+ await unlink(temporary);
644
+ } catch {
645
+ // The private temporary is safe to leave when cleanup itself fails.
646
+ }
647
+ throw new WhisperCppTranscriberSetupError(
648
+ "CONFIG_WRITE_FAILED",
649
+ "Ghostget media could not write the private transcriber configuration.",
650
+ );
651
+ } finally {
652
+ try {
653
+ await handle.close();
654
+ } catch {
655
+ // The file was fsynced before publication; never expose host diagnostics.
656
+ }
657
+ }
658
+ }
659
+ throw new WhisperCppTranscriberSetupError(
660
+ "CONFIG_WRITE_FAILED",
661
+ "Ghostget media could not allocate a private transcriber configuration file.",
662
+ );
663
+ }
664
+
665
+ async function verifyPublishedConfig(
666
+ destination: string,
667
+ expected: WhisperCppConfigDocument,
668
+ ): Promise<void> {
669
+ const published = await observeConfig(destination);
670
+ if (published.kind !== "configured" || !sameConfig(published.document, expected)) {
671
+ throw new WhisperCppTranscriberSetupError(
672
+ "CONFIG_RACE",
673
+ "The transcriber configuration changed during setup.",
674
+ );
675
+ }
676
+ }
677
+
678
+ async function publishConfig(
679
+ requestedDestination: string,
680
+ document: WhisperCppConfigDocument,
681
+ replace: boolean,
682
+ dependencies: WhisperCppTranscriberDependencies,
683
+ ): Promise<void> {
684
+ const destination = await prepareConfigDestination(requestedDestination);
685
+ if (destination === null) {
686
+ throw new WhisperCppTranscriberSetupError(
687
+ "CONFIG_UNSAFE",
688
+ "Ghostget media could not prepare a private transcriber configuration directory.",
689
+ );
690
+ }
691
+ const source = configSource(document);
692
+
693
+ for (let attempt = 0; attempt < MAX_PUBLISH_ATTEMPTS; attempt += 1) {
694
+ const observed = await observeConfig(destination);
695
+ if (observed.kind === "configured" && sameConfig(observed.document, document)) return;
696
+ if (observed.kind !== "missing" && !replace) {
697
+ throw new WhisperCppTranscriberSetupError(
698
+ "CONFIG_EXISTS",
699
+ "A different transcriber configuration already exists; pass replace to change it.",
700
+ );
701
+ }
702
+
703
+ const temporary = await createTemporaryConfig(destination, source, dependencies);
704
+ let published = false;
705
+ try {
706
+ if (observed.kind === "missing") {
707
+ try {
708
+ await link(temporary, destination);
709
+ published = true;
710
+ } catch (error) {
711
+ if (isErrno(error, "EEXIST")) continue;
712
+ throw new WhisperCppTranscriberSetupError(
713
+ "CONFIG_WRITE_FAILED",
714
+ "Ghostget media could not publish the private transcriber configuration.",
715
+ );
716
+ }
717
+ } else {
718
+ try {
719
+ await rename(temporary, destination);
720
+ published = true;
721
+ } catch {
722
+ throw new WhisperCppTranscriberSetupError(
723
+ "CONFIG_WRITE_FAILED",
724
+ "Ghostget media could not replace the private transcriber configuration.",
725
+ );
726
+ }
727
+ }
728
+ await verifyPublishedConfig(destination, document);
729
+ return;
730
+ } finally {
731
+ if (!published || observed.kind === "missing") {
732
+ try {
733
+ await unlink(temporary);
734
+ } catch (error) {
735
+ if (!isErrno(error, "ENOENT")) {
736
+ // The published destination, if any, remains complete and private.
737
+ }
738
+ }
739
+ }
740
+ }
741
+ }
742
+ throw new WhisperCppTranscriberSetupError(
743
+ "CONFIG_RACE",
744
+ "The transcriber configuration was repeatedly changed during setup.",
745
+ );
746
+ }
747
+
748
+ function setupFileError(role: VerifiedFileRole, reason: VerifiedFileFailureReason): never {
749
+ if (reason === "too-large") {
750
+ throw new WhisperCppTranscriberSetupError(
751
+ "FILE_TOO_LARGE",
752
+ `The whisper.cpp ${role} exceeds Ghostget media's size limit.`,
753
+ );
754
+ }
755
+ if (reason === "unstable") {
756
+ throw new WhisperCppTranscriberSetupError(
757
+ "UNSTABLE_FILE",
758
+ `The whisper.cpp ${role} changed while Ghostget media verified it.`,
759
+ );
760
+ }
761
+ throw new WhisperCppTranscriberSetupError(
762
+ role === "executable" ? "INVALID_EXECUTABLE" : "INVALID_MODEL",
763
+ `The whisper.cpp ${role} must resolve to a physical regular${role === "executable" ? " executable" : ""} file.`,
764
+ );
765
+ }
766
+
767
+ type VerifiedFile = Extract<VerifiedFileResult, { readonly ok: true }>;
768
+
769
+ function sameVerifiedFile(left: VerifiedFile, right: VerifiedFile): boolean {
770
+ return left.physicalPath === right.physicalPath
771
+ && left.sha256 === right.sha256
772
+ && left.bytes === right.bytes;
773
+ }
774
+
775
+ async function tryAttestRuntimeClosure(
776
+ executable: VerifiedFile,
777
+ dependencies: WhisperCppTranscriberDependencies,
778
+ options: ReverifyReadyTranscriberOptions = {},
779
+ ): Promise<RuntimeClosureAttestation | null> {
780
+ try {
781
+ const attestation = await dependencies.attestRuntimeClosure({
782
+ executablePath: executable.physicalPath,
783
+ executableSha256: executable.sha256,
784
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
785
+ probeArguments: ["--help"],
786
+ });
787
+ const record = stripRuntimeClosureRecord(attestation);
788
+ return record.executableSha256 === executable.sha256 ? record : null;
789
+ } catch {
790
+ return null;
791
+ }
792
+ }
793
+
794
+ function readyTranscriber(
795
+ executable: VerifiedFile,
796
+ model: VerifiedFile,
797
+ runtimeClosure: RuntimeClosureAttestation,
798
+ ): ReadyTranscriber {
799
+ return {
800
+ executablePath: executable.physicalPath,
801
+ modelPath: model.physicalPath,
802
+ runtimeClosure,
803
+ descriptor: {
804
+ adapter: "whisper-cpp",
805
+ profile: WHISPER_CPP_PROFILE,
806
+ executableSha256: executable.sha256,
807
+ modelSha256: model.sha256,
808
+ modelBytes: model.bytes,
809
+ runtimeProfile: runtimeClosure.profile,
810
+ runtimeSha256: runtimeClosure.closureSha256,
811
+ runtimeDependencyCount: runtimeClosure.dependencyCount,
812
+ },
813
+ };
814
+ }
815
+
816
+ function configDocument(transcriber: ReadyTranscriber): WhisperCppConfigDocument {
817
+ return {
818
+ schemaVersion: CONFIG_SCHEMA_VERSION,
819
+ adapter: transcriber.descriptor.adapter,
820
+ profile: transcriber.descriptor.profile,
821
+ executablePath: transcriber.executablePath,
822
+ modelPath: transcriber.modelPath,
823
+ executableSha256: transcriber.descriptor.executableSha256,
824
+ modelSha256: transcriber.descriptor.modelSha256,
825
+ modelBytes: transcriber.descriptor.modelBytes,
826
+ runtimeClosure: stripRuntimeClosureRecord(transcriber.runtimeClosure),
827
+ };
828
+ }
829
+
830
+ /**
831
+ * Verifies a local whisper.cpp installation and atomically records it without
832
+ * downloading an executable or model.
833
+ */
834
+ export async function setupWhisperCppTranscriber(
835
+ options: SetupWhisperCppTranscriberOptions,
836
+ dependencies: WhisperCppTranscriberDependencies = defaultDependencies,
837
+ ): Promise<ReadyTranscriber> {
838
+ if (
839
+ !isSafeAbsolutePath(options.modelPath)
840
+ || (options.replace !== undefined && typeof options.replace !== "boolean")
841
+ ) {
842
+ throw new WhisperCppTranscriberSetupError(
843
+ "INVALID_OPTIONS",
844
+ "A physical absolute model path and a boolean replace option are required.",
845
+ );
846
+ }
847
+ if (options.executablePath !== undefined && !isSafeAbsolutePath(options.executablePath)) {
848
+ throw new WhisperCppTranscriberSetupError(
849
+ "INVALID_OPTIONS",
850
+ "The whisper.cpp executable path must be absolute when provided.",
851
+ );
852
+ }
853
+ const requestedConfigPath = configurationPath(options);
854
+ if (requestedConfigPath === null) {
855
+ throw new WhisperCppTranscriberSetupError(
856
+ "INVALID_OPTIONS",
857
+ "The transcriber configuration path must be absolute and bounded.",
858
+ );
859
+ }
860
+
861
+ let executablePath: string | null | undefined = options.executablePath;
862
+ if (executablePath === undefined) {
863
+ try {
864
+ executablePath = await dependencies.findExecutable("whisper-cli", {
865
+ ...(options.env === undefined ? {} : { env: options.env }),
866
+ ...(options.homeDirectory === undefined ? {} : { homeDirectory: options.homeDirectory }),
867
+ });
868
+ } catch {
869
+ executablePath = null;
870
+ }
871
+ if (executablePath === null) {
872
+ throw new WhisperCppTranscriberSetupError(
873
+ "EXECUTABLE_NOT_FOUND",
874
+ "Ghostget media could not find a local whisper-cli executable.",
875
+ );
876
+ }
877
+ }
878
+
879
+ const [executable, model] = await Promise.all([
880
+ verifyPhysicalFile(executablePath, "executable"),
881
+ verifyPhysicalFile(options.modelPath, "model"),
882
+ ]);
883
+ if (!executable.ok) setupFileError("executable", executable.reason);
884
+ if (!model.ok) setupFileError("model", model.reason);
885
+
886
+ const runtimeBeforeProbe = await tryAttestRuntimeClosure(executable, dependencies);
887
+ if (runtimeBeforeProbe === null) {
888
+ throw new WhisperCppTranscriberSetupError(
889
+ "RUNTIME_ATTESTATION_FAILED",
890
+ "Ghostget media could not attest the executable's native runtime closure.",
891
+ );
892
+ }
893
+ if (!await verifiesWhisperCppCapabilities(executable.physicalPath, dependencies)) {
894
+ throw new WhisperCppTranscriberSetupError(
895
+ "CAPABILITY_MISMATCH",
896
+ "The executable does not provide Ghostget media's required whisper.cpp CLI capabilities.",
897
+ );
898
+ }
899
+ const runtimeAfterProbe = await tryAttestRuntimeClosure(executable, dependencies);
900
+ if (runtimeAfterProbe === null) {
901
+ throw new WhisperCppTranscriberSetupError(
902
+ "RUNTIME_ATTESTATION_FAILED",
903
+ "Ghostget media could not attest the executable's native runtime closure.",
904
+ );
905
+ }
906
+ const [executableAfterProbe, modelAfterProbe] = await Promise.all([
907
+ verifyPhysicalFile(executable.physicalPath, "executable"),
908
+ verifyPhysicalFile(model.physicalPath, "model"),
909
+ ]);
910
+ if (
911
+ !executableAfterProbe.ok
912
+ || !sameVerifiedFile(executableAfterProbe, executable)
913
+ ) {
914
+ throw new WhisperCppTranscriberSetupError(
915
+ "UNSTABLE_FILE",
916
+ "The whisper.cpp executable changed while Ghostget media verified its capabilities.",
917
+ );
918
+ }
919
+ if (
920
+ !modelAfterProbe.ok
921
+ || !sameVerifiedFile(modelAfterProbe, model)
922
+ ) {
923
+ throw new WhisperCppTranscriberSetupError(
924
+ "UNSTABLE_FILE",
925
+ "The whisper.cpp model changed while Ghostget media verified the executable's capabilities.",
926
+ );
927
+ }
928
+ if (!sameRuntimeClosure(runtimeBeforeProbe, runtimeAfterProbe)) {
929
+ throw new WhisperCppTranscriberSetupError(
930
+ "RUNTIME_CLOSURE_MISMATCH",
931
+ "The executable's native runtime closure changed during setup.",
932
+ );
933
+ }
934
+
935
+ const transcriber = readyTranscriber(
936
+ executableAfterProbe,
937
+ modelAfterProbe,
938
+ runtimeAfterProbe,
939
+ );
940
+ await publishConfig(
941
+ requestedConfigPath,
942
+ configDocument(transcriber),
943
+ options.replace ?? false,
944
+ dependencies,
945
+ );
946
+ return transcriber;
947
+ }
948
+
949
+ function fileInvalidReason(
950
+ role: VerifiedFileRole,
951
+ reason: VerifiedFileFailureReason,
952
+ ): ConfiguredTranscriberInvalidReason {
953
+ if (reason === "unstable") return "unstable-file";
954
+ if (role === "executable") {
955
+ if (reason === "missing") return "missing-executable";
956
+ if (reason === "too-large") return "executable-too-large";
957
+ return "invalid-executable";
958
+ }
959
+ if (reason === "missing") return "missing-model";
960
+ if (reason === "too-large") return "model-too-large";
961
+ return "invalid-model";
962
+ }
963
+
964
+ function readyRuntimeIsConsistent(transcriber: ReadyTranscriber): boolean {
965
+ return transcriber.descriptor.runtimeProfile === transcriber.runtimeClosure.profile
966
+ && transcriber.descriptor.runtimeSha256 === transcriber.runtimeClosure.closureSha256
967
+ && transcriber.descriptor.runtimeDependencyCount === transcriber.runtimeClosure.dependencyCount
968
+ && transcriber.descriptor.executableSha256 === transcriber.runtimeClosure.executableSha256;
969
+ }
970
+
971
+ /**
972
+ * Accepts the attestation produced by the native inference process itself,
973
+ * exact-compares it, and rehashes executable and model without another child
974
+ * process. Call this before publishing any transcript output.
975
+ */
976
+ export async function reverifyReadyTranscriberAfterRun(
977
+ expected: ReadyTranscriber,
978
+ observedRuntimeClosure: RuntimeClosureAttestation,
979
+ ): Promise<ReverifyReadyTranscriberResult> {
980
+ if (!readyRuntimeIsConsistent(expected)) return invalid("runtime-closure-mismatch");
981
+ let observedRecord: RuntimeClosureRecord;
982
+ try {
983
+ observedRecord = stripRuntimeClosureRecord(observedRuntimeClosure);
984
+ } catch {
985
+ return invalid("runtime-attestation-failed");
986
+ }
987
+ if (!sameRuntimeClosure(observedRecord, expected.runtimeClosure)) {
988
+ return invalid("runtime-closure-mismatch");
989
+ }
990
+
991
+ const [executable, model] = await Promise.all([
992
+ verifyPhysicalFile(expected.executablePath, "executable"),
993
+ verifyPhysicalFile(expected.modelPath, "model"),
994
+ ]);
995
+ if (!executable.ok) return invalid(fileInvalidReason("executable", executable.reason));
996
+ if (!model.ok) return invalid(fileInvalidReason("model", model.reason));
997
+ if (executable.physicalPath !== expected.executablePath) return invalid("invalid-executable");
998
+ if (model.physicalPath !== expected.modelPath) return invalid("invalid-model");
999
+ if (executable.sha256 !== expected.descriptor.executableSha256) {
1000
+ return invalid("executable-hash-mismatch");
1001
+ }
1002
+ if (
1003
+ model.sha256 !== expected.descriptor.modelSha256
1004
+ || model.bytes !== expected.descriptor.modelBytes
1005
+ ) return invalid("model-hash-mismatch");
1006
+ return {
1007
+ kind: "ready",
1008
+ transcriber: readyTranscriber(executable, model, observedRecord),
1009
+ };
1010
+ }
1011
+
1012
+ /**
1013
+ * Re-verifies a ready transcriber after any long-running preparation step and
1014
+ * returns a fresh attestation without trusting its earlier file observations.
1015
+ */
1016
+ export async function reverifyReadyTranscriber(
1017
+ expected: ReadyTranscriber,
1018
+ dependencies: WhisperCppTranscriberDependencies = defaultDependencies,
1019
+ options: ReverifyReadyTranscriberOptions = {},
1020
+ ): Promise<ReverifyReadyTranscriberResult> {
1021
+ if (!readyRuntimeIsConsistent(expected)) return invalid("runtime-closure-mismatch");
1022
+
1023
+ const [executable, model] = await Promise.all([
1024
+ verifyPhysicalFile(expected.executablePath, "executable"),
1025
+ verifyPhysicalFile(expected.modelPath, "model"),
1026
+ ]);
1027
+ if (!executable.ok) return invalid(fileInvalidReason("executable", executable.reason));
1028
+ if (!model.ok) return invalid(fileInvalidReason("model", model.reason));
1029
+ if (executable.physicalPath !== expected.executablePath) return invalid("invalid-executable");
1030
+ if (model.physicalPath !== expected.modelPath) return invalid("invalid-model");
1031
+ if (executable.sha256 !== expected.descriptor.executableSha256) {
1032
+ return invalid("executable-hash-mismatch");
1033
+ }
1034
+ if (
1035
+ model.sha256 !== expected.descriptor.modelSha256
1036
+ || model.bytes !== expected.descriptor.modelBytes
1037
+ ) return invalid("model-hash-mismatch");
1038
+
1039
+ const runtimeClosure = await tryAttestRuntimeClosure(executable, dependencies, options);
1040
+ if (runtimeClosure === null) return invalid("runtime-attestation-failed");
1041
+ if (!sameRuntimeClosure(runtimeClosure, expected.runtimeClosure)) {
1042
+ return invalid("runtime-closure-mismatch");
1043
+ }
1044
+
1045
+ const [executableAfterAttestation, modelAfterAttestation] = await Promise.all([
1046
+ verifyPhysicalFile(executable.physicalPath, "executable"),
1047
+ verifyPhysicalFile(model.physicalPath, "model"),
1048
+ ]);
1049
+ if (!executableAfterAttestation.ok || !sameVerifiedFile(executableAfterAttestation, executable)) {
1050
+ return invalid("unstable-file");
1051
+ }
1052
+ if (!modelAfterAttestation.ok || !sameVerifiedFile(modelAfterAttestation, model)) {
1053
+ return invalid("unstable-file");
1054
+ }
1055
+ return {
1056
+ kind: "ready",
1057
+ transcriber: readyTranscriber(
1058
+ executableAfterAttestation,
1059
+ modelAfterAttestation,
1060
+ runtimeClosure,
1061
+ ),
1062
+ };
1063
+ }
1064
+
1065
+ /** Loads a configured local transcriber and re-verifies every persisted claim. */
1066
+ export async function loadConfiguredTranscriber(
1067
+ options: LoadConfiguredTranscriberOptions = {},
1068
+ dependencies: WhisperCppTranscriberDependencies = defaultDependencies,
1069
+ ): Promise<LoadConfiguredTranscriberResult> {
1070
+ const path = configurationPath(options);
1071
+ if (path === null) return invalid("invalid-location");
1072
+ const observed = await observeConfig(path);
1073
+ if (observed.kind === "missing") return { kind: "not-configured" };
1074
+ if (observed.kind === "invalid") return invalid(observed.reason);
1075
+ const document = observed.document;
1076
+
1077
+ const [executable, model] = await Promise.all([
1078
+ verifyPhysicalFile(document.executablePath, "executable"),
1079
+ verifyPhysicalFile(document.modelPath, "model"),
1080
+ ]);
1081
+ if (!executable.ok) return invalid(fileInvalidReason("executable", executable.reason));
1082
+ if (!model.ok) return invalid(fileInvalidReason("model", model.reason));
1083
+ if (executable.physicalPath !== document.executablePath) return invalid("invalid-executable");
1084
+ if (model.physicalPath !== document.modelPath) return invalid("invalid-model");
1085
+ if (executable.sha256 !== document.executableSha256) return invalid("executable-hash-mismatch");
1086
+ if (model.sha256 !== document.modelSha256 || model.bytes !== document.modelBytes) {
1087
+ return invalid("model-hash-mismatch");
1088
+ }
1089
+ const runtimeBeforeProbe = await tryAttestRuntimeClosure(executable, dependencies);
1090
+ if (runtimeBeforeProbe === null) return invalid("runtime-attestation-failed");
1091
+ if (!sameRuntimeClosure(runtimeBeforeProbe, document.runtimeClosure)) {
1092
+ return invalid("runtime-closure-mismatch");
1093
+ }
1094
+ if (!await verifiesWhisperCppCapabilities(executable.physicalPath, dependencies)) {
1095
+ return invalid("capability-mismatch");
1096
+ }
1097
+ const runtimeAfterProbe = await tryAttestRuntimeClosure(executable, dependencies);
1098
+ if (runtimeAfterProbe === null) return invalid("runtime-attestation-failed");
1099
+ const [executableAfterProbe, modelAfterProbe] = await Promise.all([
1100
+ verifyPhysicalFile(executable.physicalPath, "executable"),
1101
+ verifyPhysicalFile(model.physicalPath, "model"),
1102
+ ]);
1103
+ if (!executableAfterProbe.ok) {
1104
+ return invalid(fileInvalidReason("executable", executableAfterProbe.reason));
1105
+ }
1106
+ if (
1107
+ !sameVerifiedFile(executableAfterProbe, executable)
1108
+ ) return invalid("unstable-file");
1109
+ if (
1110
+ !modelAfterProbe.ok
1111
+ || !sameVerifiedFile(modelAfterProbe, model)
1112
+ ) return invalid("unstable-file");
1113
+ if (
1114
+ !sameRuntimeClosure(runtimeBeforeProbe, runtimeAfterProbe)
1115
+ || !sameRuntimeClosure(runtimeAfterProbe, document.runtimeClosure)
1116
+ ) return invalid("runtime-closure-mismatch");
1117
+
1118
+ return {
1119
+ kind: "ready",
1120
+ transcriber: readyTranscriber(executableAfterProbe, modelAfterProbe, runtimeAfterProbe),
1121
+ };
1122
+ }