@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,832 @@
1
+ import {
2
+ chmodSync,
3
+ closeSync,
4
+ constants,
5
+ existsSync,
6
+ fchmodSync,
7
+ fstatSync,
8
+ fsyncSync,
9
+ futimesSync,
10
+ lstatSync,
11
+ mkdtempSync,
12
+ mkdirSync,
13
+ openSync,
14
+ opendirSync,
15
+ readSync,
16
+ renameSync,
17
+ writeSync,
18
+ type BigIntStats,
19
+ } from "node:fs";
20
+ import { createHash } from "node:crypto";
21
+ import { extname, isAbsolute, join, resolve } from "node:path";
22
+
23
+ import type {
24
+ FileInputField,
25
+ FileInputValue,
26
+ InputSchema,
27
+ OperationInput,
28
+ } from "./model";
29
+ import {
30
+ ensurePrivateDirectory,
31
+ removePrivateStateDirectoryTree,
32
+ ghostgetStateHome,
33
+ type PrivateDirectoryIdentity,
34
+ } from "./storage";
35
+
36
+ export const MAX_PLAN_ASSET_TOTAL_BYTES = 1024 * 1024 * 1024;
37
+ export const MAX_PLAN_ASSET_AGGREGATE_BYTES = 2 * 1024 * 1024 * 1024;
38
+ export const PLAN_ASSET_GC_GRACE_MS = 15 * 60_000;
39
+ const FILE_REFERENCE_PREFIX = "sf1";
40
+ const MAX_ASSET_COUNT = 25;
41
+ const MAX_ASSET_ROOT_ENTRIES = 4_096;
42
+ const MAX_ASSET_DIRECTORY_ENTRIES = 64;
43
+ const MAX_ASSET_GC_REMOVALS = 256;
44
+ const ASSET_LOCK_NAME = ".asset-lock";
45
+
46
+ type BoundAsset = {
47
+ readonly index: number;
48
+ readonly sha256: string;
49
+ readonly bytes: number;
50
+ readonly mediaType: string;
51
+ readonly fileName: string;
52
+ };
53
+
54
+ export type PlanFileSummary = {
55
+ readonly kind: "file";
56
+ readonly sha256: string;
57
+ readonly bytes: number;
58
+ readonly mediaType: string;
59
+ };
60
+
61
+ export type StagedPlanAssets = {
62
+ readonly input: OperationInput;
63
+ readonly count: number;
64
+ readonly totalBytes: number;
65
+ commit: (planDigest: string) => void;
66
+ abort: () => void;
67
+ };
68
+
69
+ export type PlanAssetGcResult = {
70
+ readonly scanned: number;
71
+ readonly removed: number;
72
+ readonly retained: number;
73
+ readonly incomplete: boolean;
74
+ };
75
+
76
+ type AssetLock = {
77
+ readonly path: string;
78
+ readonly identity: PrivateDirectoryIdentity;
79
+ heartbeat: () => void;
80
+ release: () => void;
81
+ };
82
+
83
+ type BoundedDirectoryEntries = {
84
+ readonly names: readonly string[];
85
+ readonly overflow: boolean;
86
+ };
87
+
88
+ function digest(value: string): void {
89
+ if (!/^[a-f0-9]{64}$/u.test(value)) {
90
+ throw new Error("plan asset digest must be 64 lowercase hexadecimal characters");
91
+ }
92
+ }
93
+
94
+ function assetRoot(environment: Readonly<Record<string, string | undefined>>): string {
95
+ return join(ghostgetStateHome(environment), "plan-assets");
96
+ }
97
+
98
+ function hasCode(error: unknown, code: string): boolean {
99
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
100
+ }
101
+
102
+ function directoryIdentity(stats: BigIntStats): PrivateDirectoryIdentity {
103
+ return { device: stats.dev.toString(), inode: stats.ino.toString() };
104
+ }
105
+
106
+ function boundedDirectoryEntries(path: string, maximum: number): BoundedDirectoryEntries {
107
+ const directory = opendirSync(path);
108
+ const names: string[] = [];
109
+ let overflow = false;
110
+ try {
111
+ for (;;) {
112
+ const entry = directory.readSync();
113
+ if (entry === null) break;
114
+ if (names.length >= maximum) {
115
+ overflow = true;
116
+ break;
117
+ }
118
+ names.push(entry.name);
119
+ }
120
+ } finally {
121
+ directory.closeSync();
122
+ }
123
+ return { names, overflow };
124
+ }
125
+
126
+ function acquireAssetLock(
127
+ environment: Readonly<Record<string, string | undefined>>,
128
+ ): AssetLock {
129
+ const root = assetRoot(environment);
130
+ ensurePrivateDirectory(root);
131
+ const path = join(root, ASSET_LOCK_NAME);
132
+ for (let attempt = 0; attempt < 2; attempt += 1) {
133
+ let descriptor: number | null = null;
134
+ let createdIdentity: PrivateDirectoryIdentity | null = null;
135
+ try {
136
+ mkdirSync(path, { mode: 0o700 });
137
+ descriptor = openSync(
138
+ path,
139
+ constants.O_RDONLY
140
+ | ("O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0)
141
+ | ("O_DIRECTORY" in constants ? constants.O_DIRECTORY : 0),
142
+ );
143
+ const stats = fstatSync(descriptor, { bigint: true });
144
+ if (!stats.isDirectory()) throw new Error("plan asset lock is unsafe");
145
+ const identity = directoryIdentity(stats);
146
+ createdIdentity = identity;
147
+ chmodSync(path, 0o700);
148
+ let released = false;
149
+ let descriptorClosed = false;
150
+ let lastHeartbeat = Date.now();
151
+ return {
152
+ path,
153
+ identity,
154
+ heartbeat: (): void => {
155
+ const current = Date.now();
156
+ if (current - lastHeartbeat < 1_000) return;
157
+ const timestamp = new Date(current);
158
+ if (descriptor === null || descriptorClosed) throw new Error("plan asset lock is not active");
159
+ futimesSync(descriptor, timestamp, timestamp);
160
+ lastHeartbeat = current;
161
+ },
162
+ release: (): void => {
163
+ if (released) return;
164
+ if (descriptor !== null && !descriptorClosed) {
165
+ closeSync(descriptor);
166
+ descriptorClosed = true;
167
+ }
168
+ if (!removePrivateStateDirectoryTree(path, environment, identity)) {
169
+ throw new Error("plan asset lock ownership was lost");
170
+ }
171
+ released = true;
172
+ },
173
+ };
174
+ } catch (error) {
175
+ if (descriptor !== null) closeSync(descriptor);
176
+ if (createdIdentity !== null) {
177
+ try {
178
+ removePrivateStateDirectoryTree(path, environment, createdIdentity);
179
+ } catch (cleanupError) {
180
+ throw new AggregateError([error, cleanupError], "plan asset lock setup failed and cleanup was incomplete");
181
+ }
182
+ }
183
+ if (!hasCode(error, "EEXIST")) throw error;
184
+ let stats: BigIntStats;
185
+ try {
186
+ stats = lstatSync(path, { bigint: true });
187
+ } catch (inspectionError) {
188
+ if (hasCode(inspectionError, "ENOENT")) continue;
189
+ throw inspectionError;
190
+ }
191
+ if (!stats.isDirectory() || stats.isSymbolicLink()) throw new Error("plan asset lock is unsafe");
192
+ const age = Date.now() - Number(stats.mtimeMs);
193
+ if (!Number.isFinite(age) || age < PLAN_ASSET_GC_GRACE_MS) {
194
+ throw new Error("plan asset maintenance is busy; retry the command");
195
+ }
196
+ try {
197
+ if (!removePrivateStateDirectoryTree(path, environment, directoryIdentity(stats))) {
198
+ throw new Error("plan asset maintenance is busy; retry the command");
199
+ }
200
+ } catch (removalError) {
201
+ throw new Error("plan asset maintenance is busy; retry the command", { cause: removalError });
202
+ }
203
+ }
204
+ }
205
+ throw new Error("plan asset maintenance is busy; retry the command");
206
+ }
207
+
208
+ function addAssetBytes(total: number, size: bigint): number {
209
+ if (size < 0n || size > BigInt(MAX_PLAN_ASSET_AGGREGATE_BYTES - total)) {
210
+ throw new Error(`plan attachment storage exceeds its ${MAX_PLAN_ASSET_AGGREGATE_BYTES}-byte aggregate quota`);
211
+ }
212
+ return total + Number(size);
213
+ }
214
+
215
+ function assetDirectoryBytes(path: string): number {
216
+ const entries = boundedDirectoryEntries(path, MAX_ASSET_DIRECTORY_ENTRIES);
217
+ if (entries.overflow) throw new Error("plan attachment directory exceeds its bounded entry count");
218
+ let total = 0;
219
+ for (const name of entries.names) {
220
+ let stats: BigIntStats;
221
+ try {
222
+ stats = lstatSync(join(path, name), { bigint: true });
223
+ } catch (error) {
224
+ if (hasCode(error, "ENOENT")) continue;
225
+ throw error;
226
+ }
227
+ if (!stats.isFile() || stats.isSymbolicLink()) {
228
+ throw new Error("plan attachment storage contains an unsupported entry");
229
+ }
230
+ total = addAssetBytes(total, stats.size);
231
+ }
232
+ return total;
233
+ }
234
+
235
+ function currentAssetBytes(root: string): number {
236
+ const entries = boundedDirectoryEntries(root, MAX_ASSET_ROOT_ENTRIES);
237
+ if (entries.overflow) throw new Error("plan attachment storage exceeds its bounded bundle count");
238
+ let total = 0;
239
+ for (const name of entries.names) {
240
+ if (name === ASSET_LOCK_NAME) continue;
241
+ const path = join(root, name);
242
+ let stats: BigIntStats;
243
+ try {
244
+ stats = lstatSync(path, { bigint: true });
245
+ } catch (error) {
246
+ if (hasCode(error, "ENOENT")) continue;
247
+ throw error;
248
+ }
249
+ if (stats.isFile() && !stats.isSymbolicLink()) total = addAssetBytes(total, stats.size);
250
+ else if (stats.isDirectory() && !stats.isSymbolicLink()) {
251
+ total = addAssetBytes(total, BigInt(assetDirectoryBytes(path)));
252
+ } else throw new Error("plan attachment storage contains an unsupported root entry");
253
+ }
254
+ return total;
255
+ }
256
+
257
+ /**
258
+ * Remove only old, unreferenced staging/bundle directories. The grace window
259
+ * covers the short plan-to-receipt handoff where no durable reference exists.
260
+ */
261
+ export function purgeOrphanedPlanAssets(
262
+ protectedDigests: ReadonlySet<string> | (() => ReadonlySet<string>),
263
+ environment: Readonly<Record<string, string | undefined>> = process.env,
264
+ now = new Date(),
265
+ ): PlanAssetGcResult {
266
+ const root = assetRoot(environment);
267
+ if (!existsSync(root)) return { scanned: 0, removed: 0, retained: 0, incomplete: false };
268
+ const lock = acquireAssetLock(environment);
269
+ let resolvedProtectedDigests: ReadonlySet<string> | null = null;
270
+ const isProtected = (planDigest: string): boolean => {
271
+ resolvedProtectedDigests ??= typeof protectedDigests === "function" ? protectedDigests() : protectedDigests;
272
+ return resolvedProtectedDigests.has(planDigest);
273
+ };
274
+ let scanned = 0;
275
+ let removed = 0;
276
+ let retained = 0;
277
+ let incomplete = false;
278
+ try {
279
+ const entries = boundedDirectoryEntries(root, MAX_ASSET_ROOT_ENTRIES);
280
+ incomplete = entries.overflow;
281
+ for (const name of entries.names) {
282
+ if (name === ASSET_LOCK_NAME) continue;
283
+ const staged = /^\.stage-[A-Za-z0-9_-]{1,64}$/u.test(name);
284
+ const planDigest = /^[a-f0-9]{64}$/u.test(name) ? name : null;
285
+ if (!staged && planDigest === null) continue;
286
+ scanned += 1;
287
+ const path = join(root, name);
288
+ let stats: BigIntStats;
289
+ try {
290
+ stats = lstatSync(path, { bigint: true });
291
+ } catch (error) {
292
+ if (hasCode(error, "ENOENT")) continue;
293
+ throw error;
294
+ }
295
+ if (!stats.isDirectory() || stats.isSymbolicLink()) {
296
+ retained += 1;
297
+ incomplete = true;
298
+ continue;
299
+ }
300
+ const age = now.getTime() - Number(stats.mtimeMs);
301
+ if (
302
+ (planDigest !== null && age >= PLAN_ASSET_GC_GRACE_MS && isProtected(planDigest))
303
+ || !Number.isFinite(age)
304
+ || age < PLAN_ASSET_GC_GRACE_MS
305
+ || removed >= MAX_ASSET_GC_REMOVALS
306
+ ) {
307
+ retained += 1;
308
+ if (removed >= MAX_ASSET_GC_REMOVALS && age >= PLAN_ASSET_GC_GRACE_MS) incomplete = true;
309
+ continue;
310
+ }
311
+ if (removePrivateStateDirectoryTree(path, environment, directoryIdentity(stats))) removed += 1;
312
+ else {
313
+ retained += 1;
314
+ incomplete = true;
315
+ }
316
+ }
317
+ return { scanned, removed, retained, incomplete };
318
+ } finally {
319
+ lock.release();
320
+ }
321
+ }
322
+
323
+ export function planAssetBundlePath(
324
+ planDigest: string,
325
+ environment: Readonly<Record<string, string | undefined>> = process.env,
326
+ ): string {
327
+ digest(planDigest);
328
+ return join(assetRoot(environment), planDigest);
329
+ }
330
+
331
+ function safeExtension(mediaType: string): string {
332
+ const extensions: Readonly<Record<string, string>> = {
333
+ "application/octet-stream": ".bin",
334
+ "application/pdf": ".pdf",
335
+ "application/zip": ".zip",
336
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation": ".pptx",
337
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ".xlsx",
338
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ".docx",
339
+ "audio/mpeg": ".mp3",
340
+ "audio/mp4": ".m4a",
341
+ "audio/ogg": ".ogg",
342
+ "audio/wav": ".wav",
343
+ "image/gif": ".gif",
344
+ "image/jpeg": ".jpg",
345
+ "image/png": ".png",
346
+ "image/webp": ".webp",
347
+ "text/plain": ".txt",
348
+ "video/mp4": ".mp4",
349
+ "video/quicktime": ".mov",
350
+ "video/webm": ".webm",
351
+ };
352
+ return extensions[mediaType] ?? ".bin";
353
+ }
354
+
355
+ function ascii(buffer: Buffer, start: number, end: number): string {
356
+ return buffer.subarray(start, end).toString("ascii");
357
+ }
358
+
359
+ export function detectMediaType(buffer: Buffer): string {
360
+ if (buffer.length >= 8 && buffer.subarray(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) return "image/png";
361
+ if (buffer.length >= 3 && buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) return "image/jpeg";
362
+ if (buffer.length >= 6 && (ascii(buffer, 0, 6) === "GIF87a" || ascii(buffer, 0, 6) === "GIF89a")) return "image/gif";
363
+ if (buffer.length >= 12 && ascii(buffer, 0, 4) === "RIFF" && ascii(buffer, 8, 12) === "WEBP") return "image/webp";
364
+ if (buffer.length >= 12 && ascii(buffer, 0, 4) === "RIFF" && ascii(buffer, 8, 12) === "WAVE") return "audio/wav";
365
+ if (buffer.length >= 5 && ascii(buffer, 0, 5) === "%PDF-") return "application/pdf";
366
+ if (buffer.length >= 4 && buffer[0] === 0x50 && buffer[1] === 0x4b && buffer[2] === 0x03 && buffer[3] === 0x04) return "application/zip";
367
+ if (buffer.length >= 4 && buffer[0] === 0x1a && buffer[1] === 0x45 && buffer[2] === 0xdf && buffer[3] === 0xa3) return "video/webm";
368
+ if (buffer.length >= 4 && ascii(buffer, 0, 4) === "OggS") return "audio/ogg";
369
+ if (buffer.length >= 3 && ascii(buffer, 0, 3) === "ID3") return "audio/mpeg";
370
+ if (buffer.length >= 2 && buffer[0] === 0xff && (buffer[1] ?? 0) >= 0xe0) return "audio/mpeg";
371
+ if (buffer.length >= 12 && ascii(buffer, 4, 8) === "ftyp") {
372
+ const brand = ascii(buffer, 8, 12);
373
+ if (brand === "qt ") return "video/quicktime";
374
+ if (brand === "M4A " || brand === "M4B ") return "audio/mp4";
375
+ return "video/mp4";
376
+ }
377
+ if (buffer.length > 0 && !buffer.includes(0)) {
378
+ try {
379
+ new TextDecoder("utf-8", { fatal: true }).decode(buffer);
380
+ return "text/plain";
381
+ } catch {
382
+ // Binary data without a recognized signature remains opaque.
383
+ }
384
+ }
385
+ return "application/octet-stream";
386
+ }
387
+
388
+ const OOXML_CENTRAL_DIRECTORY_MAX_BYTES = 4 * 1024 * 1024;
389
+ const ZIP_EOCD_MAX_BYTES = 65_557;
390
+
391
+ function readExactAt(descriptor: number, length: number, position: number): Buffer {
392
+ const output = Buffer.alloc(length);
393
+ let offset = 0;
394
+ while (offset < length) {
395
+ const count = readSync(descriptor, output, offset, length - offset, position + offset);
396
+ if (count === 0) throw new Error("ZIP container ended before its declared central directory");
397
+ offset += count;
398
+ }
399
+ return output;
400
+ }
401
+
402
+ /** Inspect only bounded ZIP central-directory names; no archive entry is extracted. */
403
+ export function detectOfficeOpenXmlMediaType(path: string): string | null {
404
+ const descriptor = sourceDescriptor(path);
405
+ try {
406
+ const stats = fstatSync(descriptor);
407
+ if (!stats.isFile() || stats.size < 22) return null;
408
+ const tailLength = Math.min(stats.size, ZIP_EOCD_MAX_BYTES);
409
+ const tail = readExactAt(descriptor, tailLength, stats.size - tailLength);
410
+ let eocd = -1;
411
+ for (let index = tail.length - 22; index >= 0; index -= 1) {
412
+ if (tail.readUInt32LE(index) === 0x0605_4b50) {
413
+ eocd = index;
414
+ break;
415
+ }
416
+ }
417
+ if (eocd < 0) return null;
418
+ const entries = tail.readUInt16LE(eocd + 10);
419
+ const centralBytes = tail.readUInt32LE(eocd + 12);
420
+ const centralOffset = tail.readUInt32LE(eocd + 16);
421
+ if (
422
+ entries < 1
423
+ || entries > 10_000
424
+ || centralBytes < 46
425
+ || centralBytes > OOXML_CENTRAL_DIRECTORY_MAX_BYTES
426
+ || centralOffset + centralBytes > stats.size
427
+ ) return null;
428
+ const central = readExactAt(descriptor, centralBytes, centralOffset);
429
+ const names = new Set<string>();
430
+ let offset = 0;
431
+ for (let index = 0; index < entries; index += 1) {
432
+ if (offset + 46 > central.length || central.readUInt32LE(offset) !== 0x0201_4b50) return null;
433
+ const nameLength = central.readUInt16LE(offset + 28);
434
+ const extraLength = central.readUInt16LE(offset + 30);
435
+ const commentLength = central.readUInt16LE(offset + 32);
436
+ const end = offset + 46 + nameLength + extraLength + commentLength;
437
+ if (nameLength < 1 || nameLength > 4_096 || end > central.length) return null;
438
+ let name: string;
439
+ try {
440
+ name = new TextDecoder("utf-8", { fatal: true }).decode(central.subarray(offset + 46, offset + 46 + nameLength));
441
+ } catch {
442
+ return null;
443
+ }
444
+ if (name.includes("\u0000") || name.startsWith("/") || name.includes("../")) return null;
445
+ names.add(name);
446
+ offset = end;
447
+ }
448
+ if (!names.has("[Content_Types].xml") || !names.has("_rels/.rels")) return null;
449
+ const roots = [
450
+ { prefix: "word/", mediaType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document" },
451
+ { prefix: "ppt/", mediaType: "application/vnd.openxmlformats-officedocument.presentationml.presentation" },
452
+ { prefix: "xl/", mediaType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" },
453
+ ] as const;
454
+ const matched = roots.filter((root) => [...names].some((name) => name.startsWith(root.prefix)));
455
+ return matched.length === 1 ? matched[0]?.mediaType ?? null : null;
456
+ } finally {
457
+ closeSync(descriptor);
458
+ }
459
+ }
460
+
461
+ function mediaTypeAllowed(detected: string, allowed: readonly string[] | undefined): boolean {
462
+ if (allowed === undefined) return true;
463
+ return allowed.some((candidate) => candidate === detected || (candidate.endsWith("/*") && detected.startsWith(candidate.slice(0, -1))));
464
+ }
465
+
466
+ function encodedMediaType(value: string): string {
467
+ return Buffer.from(value, "utf8").toString("base64url");
468
+ }
469
+
470
+ function decodeMediaType(value: string): string {
471
+ if (!/^[A-Za-z0-9_-]{1,128}$/u.test(value)) throw new Error("plan-bound file reference is malformed");
472
+ const decoded = Buffer.from(value, "base64url").toString("utf8");
473
+ if (!/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+*-]+$/u.test(decoded)) {
474
+ throw new Error("plan-bound file media type is malformed");
475
+ }
476
+ return decoded;
477
+ }
478
+
479
+ function encodeReference(asset: BoundAsset): string {
480
+ return [
481
+ FILE_REFERENCE_PREFIX,
482
+ String(asset.index),
483
+ asset.sha256,
484
+ String(asset.bytes),
485
+ encodedMediaType(asset.mediaType),
486
+ asset.fileName,
487
+ ].join(":");
488
+ }
489
+
490
+ function decodeReference(reference: string): BoundAsset {
491
+ const parts = reference.split(":");
492
+ if (parts.length !== 6 || parts[0] !== FILE_REFERENCE_PREFIX) {
493
+ throw new Error("plan-bound file reference is malformed");
494
+ }
495
+ const [, rawIndex, sha256, rawBytes, rawMediaType, fileName] = parts;
496
+ const index = Number(rawIndex);
497
+ const bytes = Number(rawBytes);
498
+ if (
499
+ !Number.isSafeInteger(index)
500
+ || index < 1
501
+ || index > MAX_ASSET_COUNT
502
+ || typeof sha256 !== "string"
503
+ || !/^[a-f0-9]{64}$/u.test(sha256)
504
+ || !Number.isSafeInteger(bytes)
505
+ || bytes < 1
506
+ || bytes > MAX_PLAN_ASSET_TOTAL_BYTES
507
+ || typeof fileName !== "string"
508
+ || !/^asset-(?:0[1-9]|1[0-9]|2[0-5])\.[a-z0-9]{1,8}$/u.test(fileName)
509
+ || fileName !== `asset-${String(index).padStart(2, "0")}${extname(fileName)}`
510
+ || rawMediaType === undefined
511
+ ) throw new Error("plan-bound file reference is malformed");
512
+ return { index, sha256, bytes, mediaType: decodeMediaType(rawMediaType), fileName };
513
+ }
514
+
515
+ function sourceDescriptor(path: string): number {
516
+ const flags = constants.O_RDONLY
517
+ | ("O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0)
518
+ | ("O_NONBLOCK" in constants ? constants.O_NONBLOCK : 0);
519
+ try {
520
+ return openSync(path, flags);
521
+ } catch (error) {
522
+ throw new Error("could not safely open attachment", { cause: error });
523
+ }
524
+ }
525
+
526
+ function copyOne(
527
+ sourceReference: string,
528
+ field: FileInputField,
529
+ directory: string,
530
+ index: number,
531
+ remainingBytes: number,
532
+ heartbeat: () => void,
533
+ ): { readonly value: FileInputValue; readonly bytes: number } {
534
+ if (sourceReference.startsWith(`${FILE_REFERENCE_PREFIX}:`)) {
535
+ throw new Error("raw attachment input cannot impersonate a plan-bound file reference");
536
+ }
537
+ const source = isAbsolute(sourceReference) ? sourceReference : resolve(sourceReference);
538
+ const input = sourceDescriptor(source);
539
+ let output: number | null = null;
540
+ try {
541
+ const stats = fstatSync(input);
542
+ const maximum = Math.min(field.maxBytes, remainingBytes);
543
+ if (!stats.isFile() || stats.isSymbolicLink() || stats.size < 1 || stats.size > maximum) {
544
+ throw new Error(`attachment must be a non-empty regular file no larger than ${maximum} bytes`);
545
+ }
546
+ const buffer = Buffer.allocUnsafe(64 * 1024);
547
+ const chunks: Buffer[] = [];
548
+ let retainedBytes = 0;
549
+ const textDecoder = new TextDecoder("utf-8", { fatal: true });
550
+ let validText = true;
551
+ let total = 0;
552
+ const hash = createHash("sha256");
553
+ const temporaryName = `asset-${String(index).padStart(2, "0")}.tmp`;
554
+ const temporaryPath = join(directory, temporaryName);
555
+ output = openSync(
556
+ temporaryPath,
557
+ constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | ("O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0),
558
+ 0o600,
559
+ );
560
+ fchmodSync(output, 0o600);
561
+ heartbeat();
562
+ for (;;) {
563
+ const count = readSync(input, buffer, 0, buffer.byteLength, null);
564
+ if (count === 0) break;
565
+ total += count;
566
+ if (total > maximum) throw new Error(`attachment grew beyond ${maximum} bytes while being staged`);
567
+ const value = buffer.subarray(0, count);
568
+ if (retainedBytes < 4_096) {
569
+ const retained = Math.min(value.byteLength, 4_096 - retainedBytes);
570
+ if (retained > 0) {
571
+ chunks.push(Buffer.from(value.subarray(0, retained)));
572
+ retainedBytes += retained;
573
+ }
574
+ }
575
+ hash.update(value);
576
+ if (validText) {
577
+ if (value.includes(0)) validText = false;
578
+ else {
579
+ try {
580
+ textDecoder.decode(value, { stream: true });
581
+ } catch {
582
+ validText = false;
583
+ }
584
+ }
585
+ }
586
+ let written = 0;
587
+ while (written < value.byteLength) written += writeSync(output, value, written, value.byteLength - written);
588
+ heartbeat();
589
+ }
590
+ if (total !== stats.size) throw new Error("attachment changed size while being staged");
591
+ fsyncSync(output);
592
+ closeSync(output);
593
+ output = null;
594
+ const prefix = chunks.length === 0 ? Buffer.alloc(0) : Buffer.concat(chunks, retainedBytes);
595
+ if (validText) {
596
+ try {
597
+ textDecoder.decode();
598
+ } catch {
599
+ validText = false;
600
+ }
601
+ }
602
+ const detectedContainer = detectMediaType(prefix);
603
+ const detected = detectedContainer === "application/zip"
604
+ ? detectOfficeOpenXmlMediaType(temporaryPath) ?? detectedContainer
605
+ : detectedContainer;
606
+ const mediaType = detected === "text/plain" || detected === "application/octet-stream"
607
+ ? validText ? "text/plain" : "application/octet-stream"
608
+ : detected;
609
+ if (!mediaTypeAllowed(mediaType, field.mediaTypes)) {
610
+ throw new Error(`attachment content type ${mediaType} is not allowed by the adapter`);
611
+ }
612
+ const fileName = `asset-${String(index).padStart(2, "0")}${safeExtension(mediaType)}`;
613
+ renameSync(temporaryPath, join(directory, fileName));
614
+ const asset: BoundAsset = {
615
+ index,
616
+ sha256: hash.digest("hex"),
617
+ bytes: total,
618
+ mediaType,
619
+ fileName,
620
+ };
621
+ return { value: { kind: "file", reference: encodeReference(asset) }, bytes: total };
622
+ } finally {
623
+ if (output !== null) closeSync(output);
624
+ closeSync(input);
625
+ }
626
+ }
627
+
628
+ function fileReference(value: unknown, label: string): string {
629
+ if (
630
+ typeof value !== "object"
631
+ || value === null
632
+ || Array.isArray(value)
633
+ || (value as { readonly kind?: unknown }).kind !== "file"
634
+ || typeof (value as { readonly reference?: unknown }).reference !== "string"
635
+ ) throw new Error(`${label} is not a validated file input`);
636
+ return (value as FileInputValue).reference;
637
+ }
638
+
639
+ export function stagePlanAssets(
640
+ input: OperationInput,
641
+ schema: InputSchema,
642
+ environment: Readonly<Record<string, string | undefined>> = process.env,
643
+ ): StagedPlanAssets {
644
+ const hasFileFields = Object.values(schema.properties).some((field) =>
645
+ field.type === "file" || (field.type === "array" && field.items.type === "file"));
646
+ if (!hasFileFields) {
647
+ return { input, count: 0, totalBytes: 0, commit: () => {}, abort: () => {} };
648
+ }
649
+ const root = assetRoot(environment);
650
+ const lock = acquireAssetLock(environment);
651
+ let existingBytes: number;
652
+ let temporary: string | null = null;
653
+ try {
654
+ existingBytes = currentAssetBytes(root);
655
+ temporary = mkdtempSync(join(root, ".stage-"));
656
+ chmodSync(temporary, 0o700);
657
+ } catch (error) {
658
+ const cleanupErrors: unknown[] = [];
659
+ if (temporary !== null && existsSync(temporary)) {
660
+ try {
661
+ removePrivateStateDirectoryTree(temporary, environment);
662
+ } catch (cleanupError) {
663
+ cleanupErrors.push(cleanupError);
664
+ }
665
+ }
666
+ try {
667
+ lock.release();
668
+ } catch (cleanupError) {
669
+ cleanupErrors.push(cleanupError);
670
+ }
671
+ if (cleanupErrors.length > 0) {
672
+ throw new AggregateError([error, ...cleanupErrors], "plan attachment staging failed and cleanup was incomplete");
673
+ }
674
+ throw error;
675
+ }
676
+ if (temporary === null) throw new Error("plan attachment staging directory was not initialized");
677
+ let currentPath = temporary;
678
+ let committed = false;
679
+ let count = 0;
680
+ let totalBytes = 0;
681
+ const output: Record<string, OperationInput[string]> = { ...input };
682
+ const remainingCapacity = (): number => {
683
+ const aggregate = MAX_PLAN_ASSET_AGGREGATE_BYTES - existingBytes - totalBytes;
684
+ if (aggregate < 1) {
685
+ throw new Error(`plan attachment storage exceeds its ${MAX_PLAN_ASSET_AGGREGATE_BYTES}-byte aggregate quota`);
686
+ }
687
+ return Math.min(MAX_PLAN_ASSET_TOTAL_BYTES - totalBytes, aggregate);
688
+ };
689
+ const abort = (): void => {
690
+ try {
691
+ if (existsSync(currentPath)) removePrivateStateDirectoryTree(currentPath, environment);
692
+ } finally {
693
+ lock.release();
694
+ }
695
+ };
696
+ try {
697
+ for (const [name, field] of Object.entries(schema.properties)) {
698
+ if (field.type !== "file" && (field.type !== "array" || field.items.type !== "file")) continue;
699
+ const value = input[name];
700
+ if (value === undefined) continue;
701
+ if (field.type === "file") {
702
+ count += 1;
703
+ if (count > MAX_ASSET_COUNT) throw new Error(`a plan can bind at most ${MAX_ASSET_COUNT} attachments`);
704
+ const copied = copyOne(
705
+ fileReference(value, `input.${name}`),
706
+ field,
707
+ temporary,
708
+ count,
709
+ remainingCapacity(),
710
+ lock.heartbeat,
711
+ );
712
+ totalBytes += copied.bytes;
713
+ output[name] = copied.value;
714
+ continue;
715
+ }
716
+ if (field.items.type !== "file" || !Array.isArray(value)) throw new Error(`input.${name} is not a validated file array`);
717
+ const copiedValues: FileInputValue[] = [];
718
+ for (const [index, item] of value.entries()) {
719
+ count += 1;
720
+ if (count > MAX_ASSET_COUNT) throw new Error(`a plan can bind at most ${MAX_ASSET_COUNT} attachments`);
721
+ const copied = copyOne(
722
+ fileReference(item, `input.${name}[${index}]`),
723
+ field.items,
724
+ temporary,
725
+ count,
726
+ remainingCapacity(),
727
+ lock.heartbeat,
728
+ );
729
+ totalBytes += copied.bytes;
730
+ copiedValues.push(copied.value);
731
+ }
732
+ output[name] = copiedValues;
733
+ }
734
+ if (count === 0) abort();
735
+ } catch (error) {
736
+ abort();
737
+ throw error;
738
+ }
739
+ return {
740
+ input: output,
741
+ count,
742
+ totalBytes,
743
+ commit: (planDigest): void => {
744
+ if (count === 0) return;
745
+ if (committed) throw new Error("plan asset bundle was already committed");
746
+ const target = planAssetBundlePath(planDigest, environment);
747
+ if (existsSync(target)) throw new Error("plan asset bundle already exists");
748
+ renameSync(temporary, target);
749
+ currentPath = target;
750
+ committed = true;
751
+ lock.release();
752
+ },
753
+ abort,
754
+ };
755
+ }
756
+
757
+ function verifyAsset(path: string, expected: BoundAsset): void {
758
+ const descriptor = sourceDescriptor(path);
759
+ try {
760
+ const stats = fstatSync(descriptor);
761
+ if (!stats.isFile() || stats.isSymbolicLink() || stats.size !== expected.bytes) {
762
+ throw new Error("plan-bound attachment no longer matches its confirmed metadata");
763
+ }
764
+ const hash = createHash("sha256");
765
+ const buffer = Buffer.allocUnsafe(64 * 1024);
766
+ let total = 0;
767
+ for (;;) {
768
+ const count = readSync(descriptor, buffer, 0, buffer.byteLength, null);
769
+ if (count === 0) break;
770
+ total += count;
771
+ if (total > expected.bytes) throw new Error("plan-bound attachment grew after confirmation");
772
+ hash.update(buffer.subarray(0, count));
773
+ }
774
+ if (total !== expected.bytes || hash.digest("hex") !== expected.sha256) {
775
+ throw new Error("plan-bound attachment failed its content hash");
776
+ }
777
+ } finally {
778
+ closeSync(descriptor);
779
+ }
780
+ }
781
+
782
+ export function resolvePlanAssetFiles(
783
+ files: readonly FileInputValue[],
784
+ planDigest: string,
785
+ environment: Readonly<Record<string, string | undefined>> = process.env,
786
+ ): readonly string[] {
787
+ if (files.length < 1 || files.length > MAX_ASSET_COUNT) throw new Error("file resolution requires 1-25 plan-bound attachments");
788
+ const bundle = planAssetBundlePath(planDigest, environment);
789
+ const bundleStats = lstatSync(bundle);
790
+ if (!bundleStats.isDirectory() || bundleStats.isSymbolicLink()) throw new Error("plan asset bundle is not a real directory");
791
+ return files.map((file) => {
792
+ const expected = decodeReference(file.reference);
793
+ const path = join(bundle, expected.fileName);
794
+ verifyAsset(path, expected);
795
+ return path;
796
+ });
797
+ }
798
+
799
+ export function cleanupPlanAssets(
800
+ planDigest: string,
801
+ environment: Readonly<Record<string, string | undefined>> = process.env,
802
+ ): boolean {
803
+ const path = planAssetBundlePath(planDigest, environment);
804
+ return existsSync(path) ? removePrivateStateDirectoryTree(path, environment) : false;
805
+ }
806
+
807
+ export function isPlanBoundFile(value: unknown): value is FileInputValue {
808
+ try {
809
+ const reference = (value as { readonly reference?: unknown } | null)?.reference;
810
+ if (
811
+ typeof value !== "object"
812
+ || value === null
813
+ || Array.isArray(value)
814
+ || (value as { readonly kind?: unknown }).kind !== "file"
815
+ || typeof reference !== "string"
816
+ ) return false;
817
+ decodeReference(reference);
818
+ return true;
819
+ } catch {
820
+ return false;
821
+ }
822
+ }
823
+
824
+ export function summarizePlanFile(value: FileInputValue): PlanFileSummary {
825
+ const asset = decodeReference(value.reference);
826
+ return {
827
+ kind: "file",
828
+ sha256: asset.sha256,
829
+ bytes: asset.bytes,
830
+ mediaType: asset.mediaType,
831
+ };
832
+ }