@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,1972 @@
1
+ import {
2
+ spawn,
3
+ spawnSync,
4
+ type ChildProcess,
5
+ type ChildProcessWithoutNullStreams,
6
+ } from "node:child_process";
7
+ import { once } from "node:events";
8
+ import {
9
+ chmodSync,
10
+ closeSync,
11
+ constants,
12
+ existsSync,
13
+ fstatSync,
14
+ fsyncSync,
15
+ mkdirSync,
16
+ mkdtempSync,
17
+ openSync,
18
+ readSync,
19
+ rmSync,
20
+ writeSync,
21
+ } from "node:fs";
22
+ import { tmpdir } from "node:os";
23
+ import { dirname, join } from "node:path";
24
+ import type { Writable } from "node:stream";
25
+
26
+ import {
27
+ PORTABLE_PROVIDER_PLUGIN_HOST_API_VERSION,
28
+ PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME,
29
+ verifyPortableProviderPluginPackageDirectory,
30
+ type PortableProviderPluginBindingV1,
31
+ type PortableProviderPluginOperationV1,
32
+ type PortableProviderPluginSessionMaterialName,
33
+ type VerifiedPortableProviderPluginPackage,
34
+ } from "./provider-plugin-package";
35
+ import {
36
+ announcePortableProviderPluginHostStarted,
37
+ announcePortableProviderPluginHostStarting,
38
+ registerPortableProviderPluginCleanupBarrier,
39
+ type PortableProviderPluginCleanupBarrier,
40
+ } from "./provider-plugin-cleanup-barrier";
41
+ import {
42
+ captureProcessOwnerIdentity,
43
+ processOwnerStatus,
44
+ type ProcessOwnerIdentity,
45
+ } from "./process-identity";
46
+ import {
47
+ validateOperationInput,
48
+ type FileInputField,
49
+ } from "./model";
50
+ import {
51
+ encodePortableProviderPluginMessage,
52
+ parsePortableProviderPluginMessage,
53
+ PortableProviderPluginFrameDecoder,
54
+ PORTABLE_PROVIDER_PLUGIN_PROTOCOL_VERSION,
55
+ type PortablePluginCapabilityRequest,
56
+ type PortablePluginCapabilityResult,
57
+ type PortablePluginInvocationAuth,
58
+ type PortablePluginInvocationFile,
59
+ type PortablePluginJsonObject,
60
+ type PortablePluginJsonValue,
61
+ type PortablePluginRoute,
62
+ type PortableProviderPluginHostMessage,
63
+ type PortableProviderPluginMessage,
64
+ type PortableProviderPluginProcessMessage,
65
+ } from "./provider-plugin-protocol";
66
+
67
+ const MAX_HOST_MESSAGES = 256;
68
+ const MAX_HOST_STDERR_BYTES = 64 * 1024;
69
+ const MAX_HOST_STDOUT_BYTES = 8 * 1024 * 1024;
70
+ const MAX_HOST_STDIN_BYTES = 8 * 1024 * 1024;
71
+ const MIN_INVOCATION_TIMEOUT_MS = 100;
72
+ const MAX_INVOCATION_TIMEOUT_MS = 120_000;
73
+ const TERMINATION_GRACE_MS = 250;
74
+ const TERMINATION_CONFIRMATION_MS = 2_000;
75
+ const TERMINATION_STREAM_CLOSE_MS = 250;
76
+ const RUNTIME_CHILD_FD = 3;
77
+ const EMPTY_HOST_CONFIG_PATH = "/dev/null";
78
+ const HOST_GUARD_NAME = ".wrench-portable-host-guard.mjs";
79
+ const MAX_CONTAINED_DESCENDANTS = 256;
80
+ const HOST_GUARD_SOURCE = `
81
+ import childProcess from "node:child_process";
82
+ import cluster from "node:cluster";
83
+ import { syncBuiltinESMExports } from "node:module";
84
+ import workerThreads from "node:worker_threads";
85
+
86
+ const unavailable = () => {
87
+ throw new Error("portable plugin subprocess creation is unavailable");
88
+ };
89
+ for (const name of ["$", "openInEditor", "spawn", "spawnSync"]) {
90
+ Object.defineProperty(Bun, name, {
91
+ value: unavailable,
92
+ writable: false,
93
+ configurable: false,
94
+ });
95
+ }
96
+ for (const name of ["dlopen", "linkSymbols"]) {
97
+ if (typeof Bun.FFI?.[name] !== "function") continue;
98
+ Object.defineProperty(Bun.FFI, name, {
99
+ value: unavailable,
100
+ writable: false,
101
+ configurable: false,
102
+ });
103
+ }
104
+ const ChildProcess = childProcess.ChildProcess;
105
+ Object.defineProperty(ChildProcess.prototype, "spawn", {
106
+ value: unavailable,
107
+ writable: false,
108
+ configurable: false,
109
+ });
110
+ for (
111
+ const [module, names] of [
112
+ [
113
+ childProcess,
114
+ [
115
+ "exec",
116
+ "execFile",
117
+ "execFileSync",
118
+ "execSync",
119
+ "fork",
120
+ "spawn",
121
+ "spawnSync",
122
+ ],
123
+ ],
124
+ [
125
+ cluster,
126
+ ["disconnect", "fork", "setupMaster", "setupPrimary"],
127
+ ],
128
+ [workerThreads, ["Worker"]],
129
+ ]
130
+ ) {
131
+ for (const name of names) {
132
+ if (typeof module[name] !== "function") continue;
133
+ Object.defineProperty(module, name, {
134
+ value: unavailable,
135
+ writable: false,
136
+ configurable: false,
137
+ });
138
+ }
139
+ }
140
+ if (typeof globalThis.Worker === "function") {
141
+ Object.defineProperty(globalThis, "Worker", {
142
+ value: unavailable,
143
+ writable: false,
144
+ configurable: false,
145
+ });
146
+ }
147
+ syncBuiltinESMExports();
148
+ for (
149
+ const name of [
150
+ "_kill",
151
+ "_linkedBinding",
152
+ "binding",
153
+ "dlopen",
154
+ "execve",
155
+ "getBuiltinModule",
156
+ "kill",
157
+ ]
158
+ ) {
159
+ if (typeof process[name] !== "function") continue;
160
+ Object.defineProperty(process, name, {
161
+ value: unavailable,
162
+ writable: false,
163
+ configurable: false,
164
+ });
165
+ }
166
+
167
+ const admission = process.stdin;
168
+ await new Promise((resolve) => {
169
+ let admitted = false;
170
+ const stop = () => process.exit(125);
171
+ const onData = (chunk) => {
172
+ if (
173
+ admitted
174
+ || !(chunk instanceof Uint8Array)
175
+ || chunk.byteLength < 1
176
+ || chunk[0] !== 1
177
+ ) stop();
178
+ admitted = true;
179
+ admission.removeListener("error", stop);
180
+ admission.removeListener("end", stop);
181
+ admission.removeListener("data", onData);
182
+ if (chunk.byteLength > 1) admission.unshift(chunk.subarray(1));
183
+ admission.pause();
184
+ resolve();
185
+ };
186
+ admission.once("error", stop);
187
+ admission.once("end", stop);
188
+ admission.once("data", onData);
189
+ admission.resume();
190
+ });
191
+ `.trimStart();
192
+
193
+ const PORTABLE_RUNTIME_PROCESS_MODULES = new Set([
194
+ "bun",
195
+ "bun:ffi",
196
+ "child_process",
197
+ "cluster",
198
+ "node:child_process",
199
+ "node:cluster",
200
+ "node:worker_threads",
201
+ "worker_threads",
202
+ ]);
203
+
204
+ type ProcessMessage = PortableProviderPluginProcessMessage;
205
+
206
+ export type PortableProviderPluginHostProcessObserverForTest = {
207
+ readonly staged?: (stage: { readonly cwd: string }) => void;
208
+ readonly beforeSpawn?: (command: {
209
+ readonly executable: string;
210
+ readonly args: readonly string[];
211
+ readonly cwd: string;
212
+ }) => void;
213
+ readonly beforeParentDescriptorRelease?: () => void;
214
+ readonly descendantProcessIds?: (
215
+ host: ProcessOwnerIdentity,
216
+ ) => readonly number[];
217
+ readonly exit?: (result: {
218
+ readonly code: number | null;
219
+ readonly signal: NodeJS.Signals | null;
220
+ }) => void;
221
+ };
222
+
223
+ let hostProcessObserverForTest:
224
+ PortableProviderPluginHostProcessObserverForTest | undefined;
225
+
226
+ export function observePortableProviderPluginHostProcessForTest(
227
+ observer: PortableProviderPluginHostProcessObserverForTest,
228
+ ): () => void {
229
+ if (process.env.NODE_ENV !== "test") {
230
+ throw new Error("portable plugin host observation is available only in tests");
231
+ }
232
+ if (hostProcessObserverForTest !== undefined) {
233
+ throw new Error("portable plugin host observation is already active");
234
+ }
235
+ hostProcessObserverForTest = observer;
236
+ return () => {
237
+ if (hostProcessObserverForTest === observer) {
238
+ hostProcessObserverForTest = undefined;
239
+ }
240
+ };
241
+ }
242
+
243
+ export type PortableProviderPluginCapabilityContext = {
244
+ readonly invocationId: string;
245
+ readonly requestId: string;
246
+ readonly route: PortablePluginRoute;
247
+ /** One total invocation deadline; capability implementations must obey it. */
248
+ readonly signal: AbortSignal;
249
+ };
250
+
251
+ export type PortableProviderPluginCapabilityHost = {
252
+ /**
253
+ * Exercise one host-owned capability. The host calls this only after the
254
+ * request passes the installed manifest and invocation policy.
255
+ *
256
+ * Throwing is converted to a value-free capability error. Exception text is
257
+ * never returned to plugin code or included in the public host diagnostic.
258
+ */
259
+ readonly handle: (
260
+ request: PortablePluginCapabilityRequest,
261
+ context: PortableProviderPluginCapabilityContext,
262
+ ) => Promise<PortablePluginCapabilityResult>;
263
+ };
264
+
265
+ export type PortableProviderPluginHostInvocation = {
266
+ readonly package: VerifiedPortableProviderPluginPackage;
267
+ readonly route: PortablePluginRoute;
268
+ readonly input: PortablePluginJsonObject;
269
+ readonly auth: PortablePluginInvocationAuth;
270
+ readonly files: readonly PortablePluginInvocationFile[];
271
+ readonly timeoutMs: number;
272
+ readonly hostVersion: string;
273
+ readonly plannedDispatchIds?: readonly string[];
274
+ readonly capabilityHost?: PortableProviderPluginCapabilityHost;
275
+ readonly signal?: AbortSignal;
276
+ /** Test-only executable seam. Production callers leave this unset. */
277
+ readonly bunExecutable?: string;
278
+ };
279
+
280
+ export type PortableProviderPluginHostResult = {
281
+ readonly output: PortablePluginJsonValue;
282
+ readonly finalUrl: string | null;
283
+ readonly dispatch: {
284
+ readonly planned: number;
285
+ readonly started: number;
286
+ readonly verified: number;
287
+ };
288
+ };
289
+
290
+ export class PortableProviderPluginHostError extends Error {
291
+ readonly code: string;
292
+ readonly dispatch: PortableProviderPluginHostResult["dispatch"];
293
+
294
+ constructor(
295
+ code: string,
296
+ message: string,
297
+ dispatch: PortableProviderPluginHostResult["dispatch"],
298
+ ) {
299
+ super(message);
300
+ this.name = "PortableProviderPluginHostError";
301
+ this.code = code;
302
+ this.dispatch = Object.freeze({ ...dispatch });
303
+ }
304
+ }
305
+
306
+ type PendingMessage = {
307
+ readonly resolve: (value: ProcessMessage) => void;
308
+ readonly reject: (error: Error) => void;
309
+ };
310
+
311
+ class ProcessMessageQueue {
312
+ readonly #messages: ProcessMessage[] = [];
313
+ readonly #pending: PendingMessage[] = [];
314
+ #failure: Error | null = null;
315
+
316
+ push(message: ProcessMessage): void {
317
+ if (this.#failure !== null) return;
318
+ const pending = this.#pending.shift();
319
+ if (pending === undefined) {
320
+ this.#messages.push(message);
321
+ return;
322
+ }
323
+ pending.resolve(message);
324
+ }
325
+
326
+ fail(error: Error): void {
327
+ if (this.#failure !== null) return;
328
+ this.#failure = error;
329
+ for (const pending of this.#pending.splice(0)) pending.reject(error);
330
+ }
331
+
332
+ next(): Promise<ProcessMessage> {
333
+ const message = this.#messages.shift();
334
+ if (message !== undefined) return Promise.resolve(message);
335
+ if (this.#failure !== null) return Promise.reject(this.#failure);
336
+ return new Promise<ProcessMessage>((resolve, reject) => {
337
+ this.#pending.push({ resolve, reject });
338
+ });
339
+ }
340
+ }
341
+
342
+ function isProcessMessage(
343
+ message: PortableProviderPluginMessage,
344
+ ): message is ProcessMessage {
345
+ return message.kind.startsWith("plugin.");
346
+ }
347
+
348
+ function operationOwnsRoute(
349
+ packageValue: VerifiedPortableProviderPluginPackage,
350
+ route: PortablePluginRoute,
351
+ ): boolean {
352
+ const binding = packageValue.manifest.bindings.find((candidate) =>
353
+ candidate.transport === route.transport
354
+ && candidate.surfaceId === route.surfaceId);
355
+ if (binding === undefined) return false;
356
+ return binding.operations.some((operation) =>
357
+ operation.name === route.operation
358
+ && operation.contractVersion === route.contractVersion);
359
+ }
360
+
361
+ function routeBinding(
362
+ packageValue: VerifiedPortableProviderPluginPackage,
363
+ route: PortablePluginRoute,
364
+ ): VerifiedPortableProviderPluginPackage["manifest"]["bindings"][number] {
365
+ const binding = packageValue.manifest.bindings.find((candidate) =>
366
+ candidate.transport === route.transport
367
+ && candidate.surfaceId === route.surfaceId);
368
+ if (binding === undefined || !operationOwnsRoute(packageValue, route)) {
369
+ throw new Error("portable plugin route is not declared by the installed package");
370
+ }
371
+ return binding;
372
+ }
373
+
374
+ function routeOperation(
375
+ packageValue: VerifiedPortableProviderPluginPackage,
376
+ route: PortablePluginRoute,
377
+ ): PortableProviderPluginOperationV1 {
378
+ const binding = routeBinding(packageValue, route);
379
+ const operation = binding.operations.find((candidate) =>
380
+ candidate.name === route.operation
381
+ && candidate.contractVersion === route.contractVersion);
382
+ if (operation === undefined) {
383
+ throw new Error("portable plugin operation is not declared by the installed package");
384
+ }
385
+ return operation;
386
+ }
387
+
388
+ type ExpectedInvocationFileBinding = {
389
+ readonly input: string;
390
+ readonly field: FileInputField;
391
+ };
392
+
393
+ function mediaTypeIsAllowed(
394
+ mediaType: string,
395
+ allowed: readonly string[] | undefined,
396
+ ): boolean {
397
+ if (allowed === undefined) return true;
398
+ const essence = mediaType.split(";", 1)[0]?.toLowerCase();
399
+ if (essence === undefined) return false;
400
+ return allowed.some((candidate) =>
401
+ candidate === essence
402
+ || (
403
+ candidate.endsWith("/*")
404
+ && essence.startsWith(candidate.slice(0, -1))
405
+ ));
406
+ }
407
+
408
+ function assertInvocationFileBindings(
409
+ operation: PortableProviderPluginOperationV1,
410
+ input: PortablePluginJsonObject,
411
+ files: readonly PortablePluginInvocationFile[],
412
+ ): void {
413
+ const expectedByHandle = new Map<string, ExpectedInvocationFileBinding>();
414
+ const addExpected = (
415
+ inputName: string,
416
+ handle: PortablePluginJsonValue,
417
+ field: FileInputField,
418
+ ): void => {
419
+ // validateOperationInput has already established this value as an opaque
420
+ // file-reference string. Keep this guard local so the binding invariant
421
+ // cannot silently depend on a future parser representation.
422
+ if (typeof handle !== "string") {
423
+ throw new Error("portable plugin invocation file input is invalid");
424
+ }
425
+ if (expectedByHandle.has(handle)) {
426
+ throw new Error(
427
+ "portable plugin invocation repeats a file handle in its operation input",
428
+ );
429
+ }
430
+ expectedByHandle.set(handle, { input: inputName, field });
431
+ };
432
+
433
+ for (const [inputName, field] of Object.entries(
434
+ operation.input.properties,
435
+ )) {
436
+ const value = input[inputName];
437
+ if (value === undefined) continue;
438
+ if (field.type === "file") {
439
+ addExpected(inputName, value, field);
440
+ continue;
441
+ }
442
+ if (field.type !== "array" || field.items.type !== "file") continue;
443
+ if (!Array.isArray(value)) {
444
+ throw new Error("portable plugin invocation file array input is invalid");
445
+ }
446
+ for (const handle of value as readonly PortablePluginJsonValue[]) {
447
+ addExpected(inputName, handle, field.items);
448
+ }
449
+ }
450
+
451
+ const seenHandles = new Set<string>();
452
+ for (const file of files) {
453
+ if (seenHandles.has(file.handle)) {
454
+ throw new Error("portable plugin invocation repeats a file handle");
455
+ }
456
+ seenHandles.add(file.handle);
457
+ const expected = expectedByHandle.get(file.handle);
458
+ if (expected === undefined) {
459
+ throw new Error(
460
+ "portable plugin invocation includes a file that is not referenced by its operation input",
461
+ );
462
+ }
463
+ if (file.input !== expected.input) {
464
+ throw new Error(
465
+ "portable plugin invocation file does not match its operation input name",
466
+ );
467
+ }
468
+ if (
469
+ !Number.isSafeInteger(file.bytes)
470
+ || file.bytes < 1
471
+ || file.bytes > expected.field.maxBytes
472
+ ) {
473
+ throw new Error(
474
+ "portable plugin invocation file exceeds its operation input byte bound",
475
+ );
476
+ }
477
+ if (!mediaTypeIsAllowed(file.mediaType, expected.field.mediaTypes)) {
478
+ throw new Error(
479
+ "portable plugin invocation file media type is not allowed by its operation input",
480
+ );
481
+ }
482
+ }
483
+ if (seenHandles.size !== expectedByHandle.size) {
484
+ throw new Error(
485
+ "portable plugin invocation omits a file referenced by its operation input",
486
+ );
487
+ }
488
+ }
489
+
490
+ function assertInvocation(
491
+ invocation: PortableProviderPluginHostInvocation,
492
+ verified: VerifiedPortableProviderPluginPackage,
493
+ ): void {
494
+ if (
495
+ verified.bundleSha256 !== invocation.package.bundleSha256
496
+ || verified.manifestSha256 !== invocation.package.manifestSha256
497
+ || verified.manifest.id !== invocation.package.manifest.id
498
+ || verified.manifest.version !== invocation.package.manifest.version
499
+ ) {
500
+ throw new Error("portable plugin package changed after its verified snapshot");
501
+ }
502
+ if (
503
+ !Number.isSafeInteger(invocation.timeoutMs)
504
+ || invocation.timeoutMs < MIN_INVOCATION_TIMEOUT_MS
505
+ || invocation.timeoutMs > MAX_INVOCATION_TIMEOUT_MS
506
+ ) {
507
+ throw new Error("portable plugin invocation timeout is outside host bounds");
508
+ }
509
+ if (
510
+ typeof invocation.hostVersion !== "string"
511
+ || invocation.hostVersion.length < 1
512
+ || invocation.hostVersion.length > 128
513
+ || /[\0\r\n]/u.test(invocation.hostVersion)
514
+ ) {
515
+ throw new Error("portable plugin host version is invalid");
516
+ }
517
+ const binding = routeBinding(verified, invocation.route);
518
+ const operation = routeOperation(verified, invocation.route);
519
+ if (operation.state !== "observed") {
520
+ throw new Error("capture-required portable plugin operations are not executable");
521
+ }
522
+ const parsedInput = validateOperationInput(
523
+ operation.input,
524
+ invocation.input,
525
+ [binding.origin],
526
+ );
527
+ if (!parsedInput.ok) {
528
+ throw new Error(
529
+ `portable plugin invocation input is invalid: ${parsedInput.issues.join("; ")}`,
530
+ );
531
+ }
532
+ if (!binding.authKinds.includes(invocation.auth.kind)) {
533
+ throw new Error("portable plugin invocation auth kind is not declared");
534
+ }
535
+ assertInvocationFileBindings(operation, invocation.input, invocation.files);
536
+ const planned = invocation.plannedDispatchIds ?? [];
537
+ if (
538
+ planned.length > 25
539
+ || new Set(planned).size !== planned.length
540
+ || planned.some((id) =>
541
+ !/^[a-z][a-z0-9-]*(?:\.[a-z][a-z0-9-]*)*(?:\[[1-9][0-9]*\])?$/u
542
+ .test(id))
543
+ ) {
544
+ throw new Error("portable plugin invocation dispatch plan is invalid");
545
+ }
546
+ if (
547
+ (operation.dispatch === "none" && planned.length !== 0)
548
+ || (operation.dispatch === "single" && planned.length !== 1)
549
+ ) {
550
+ throw new Error(
551
+ "portable plugin invocation dispatch plan does not match its static operation descriptor",
552
+ );
553
+ }
554
+ }
555
+
556
+ function exactOrigin(urlValue: string): string | null {
557
+ try {
558
+ const url = new URL(urlValue);
559
+ if (
560
+ url.protocol !== "https:"
561
+ || url.username !== ""
562
+ || url.password !== ""
563
+ ) return null;
564
+ return url.origin;
565
+ } catch {
566
+ return null;
567
+ }
568
+ }
569
+
570
+ type InvocationPolicyState = {
571
+ readonly requestIds: Set<string>;
572
+ readonly filesByHandle: ReadonlyMap<string, PortablePluginInvocationFile>;
573
+ readonly materialHandles: Map<
574
+ string,
575
+ PortableProviderPluginSessionMaterialName
576
+ >;
577
+ readonly dispatchHandles: Map<string, {
578
+ readonly dispatchId: string;
579
+ verified: boolean;
580
+ mutationAttempted: boolean;
581
+ mutationCompleted: boolean;
582
+ }>;
583
+ started: number;
584
+ verified: number;
585
+ };
586
+
587
+ function capabilityIsDeclared(
588
+ request: PortablePluginCapabilityRequest,
589
+ granted:
590
+ VerifiedPortableProviderPluginPackage["manifest"]["capabilities"],
591
+ binding: PortableProviderPluginBindingV1,
592
+ auth: PortablePluginInvocationAuth,
593
+ plannedDispatchIds: readonly string[],
594
+ state: InvocationPolicyState,
595
+ ): boolean {
596
+ if (request.kind === "log.write") return true;
597
+ if (request.kind === "dispatch.begin") {
598
+ return (
599
+ plannedDispatchIds[state.started] === request.dispatchId
600
+ );
601
+ }
602
+ if (request.kind === "dispatch.verify") {
603
+ const dispatch = state.dispatchHandles.get(request.dispatchHandle);
604
+ return dispatch !== undefined
605
+ && !dispatch.verified
606
+ && dispatch.mutationCompleted;
607
+ }
608
+ if (request.kind === "http.request") {
609
+ const origin = exactOrigin(request.url);
610
+ if (
611
+ origin === null
612
+ || origin !== binding.origin
613
+ || !granted.networkOrigins.includes(origin)
614
+ ) return false;
615
+ for (const credential of request.credentials) {
616
+ const material = state.materialHandles.get(credential.handle);
617
+ if (
618
+ material === undefined
619
+ || (
620
+ material === "cookie-jar"
621
+ ? credential.sink.kind !== "cookie-jar"
622
+ : credential.sink.kind !== "header"
623
+ || credential.sink.name !== "authorization"
624
+ )
625
+ ) return false;
626
+ }
627
+ const mutating = request.method !== "GET" && request.method !== "HEAD";
628
+ if (!mutating) return request.dispatchHandle === undefined;
629
+ if (request.dispatchHandle === undefined) return false;
630
+ const dispatch = state.dispatchHandles.get(request.dispatchHandle);
631
+ return dispatch !== undefined
632
+ && !dispatch.verified
633
+ && !dispatch.mutationAttempted;
634
+ }
635
+ if (request.kind === "file.read") {
636
+ return (
637
+ granted.planFiles === "read"
638
+ && state.filesByHandle.has(request.handle)
639
+ );
640
+ }
641
+ if (
642
+ request.kind === "state.read"
643
+ || request.kind === "state.write"
644
+ || request.kind === "state.delete"
645
+ ) {
646
+ return granted.state === "namespaced";
647
+ }
648
+ return (
649
+ request.kind === "session.acquire"
650
+ && granted.sessionMaterial.includes(request.name)
651
+ && (
652
+ request.name === "oauth-access-token"
653
+ ? binding.transport === "provider-api"
654
+ && auth.kind === "oauth-token-file"
655
+ : binding.transport === "web-session-api"
656
+ && (
657
+ auth.kind === "cookie-source"
658
+ || auth.kind === "cookies-file"
659
+ || auth.kind === "browser-profile"
660
+ )
661
+ )
662
+ );
663
+ }
664
+
665
+ function matchingCapabilityResult(
666
+ request: PortablePluginCapabilityRequest,
667
+ result: PortablePluginCapabilityResult,
668
+ filesByHandle: ReadonlyMap<string, PortablePluginInvocationFile>,
669
+ ): boolean {
670
+ if (request.kind !== result.kind) return false;
671
+ if (request.kind === "http.request" && result.kind === "http.request") {
672
+ const bodyBytes = result.body.kind === "utf8"
673
+ ? Buffer.byteLength(result.body.text, "utf8")
674
+ : Buffer.byteLength(result.body.data, "base64");
675
+ return bodyBytes <= request.maxOutputBytes
676
+ && result.finalUrl === request.url;
677
+ }
678
+ if (request.kind === "file.read" && result.kind === "file.read") {
679
+ const descriptor = filesByHandle.get(request.handle);
680
+ if (descriptor === undefined) return false;
681
+ const bytes = Buffer.byteLength(result.data, "base64");
682
+ const remaining = Math.max(0, descriptor.bytes - request.offset);
683
+ const maximum = Math.min(request.length, remaining);
684
+ if (bytes > maximum || (bytes === 0 && remaining > 0)) return false;
685
+ const reachedDeclaredEnd = request.offset >= descriptor.bytes
686
+ || request.offset + bytes === descriptor.bytes;
687
+ return result.eof === reachedDeclaredEnd;
688
+ }
689
+ if (request.kind === "state.read" && result.kind === "state.read") {
690
+ return ("version" in result) === (request.includeVersion === true);
691
+ }
692
+ if (request.kind === "state.write" && result.kind === "state.write") {
693
+ return ("version" in result) === (request.expectedVersion !== undefined);
694
+ }
695
+ return true;
696
+ }
697
+
698
+ function normalizedCapabilityResult(
699
+ invocationId: string,
700
+ requestId: string,
701
+ result: PortablePluginCapabilityResult,
702
+ ): PortablePluginCapabilityResult {
703
+ const parsed = parsePortableProviderPluginMessage({
704
+ protocolVersion: PORTABLE_PROVIDER_PLUGIN_PROTOCOL_VERSION,
705
+ kind: "host.capability.result",
706
+ invocationId,
707
+ requestId,
708
+ result,
709
+ });
710
+ if (!parsed.ok || parsed.value.kind !== "host.capability.result") {
711
+ throw new Error("portable plugin capability host returned an invalid result");
712
+ }
713
+ return parsed.value.result;
714
+ }
715
+
716
+ function hostMessage(
717
+ value: PortableProviderPluginHostMessage,
718
+ ): PortableProviderPluginHostMessage {
719
+ return value;
720
+ }
721
+
722
+ async function writeHostMessage(
723
+ child: ChildProcessWithoutNullStreams,
724
+ message: PortableProviderPluginHostMessage,
725
+ signal: AbortSignal,
726
+ written: { bytes: number },
727
+ ): Promise<void> {
728
+ if (!child.stdin.writable) {
729
+ throw new Error("portable plugin stdin closed before host response");
730
+ }
731
+ const frame = encodePortableProviderPluginMessage(message);
732
+ written.bytes += Buffer.byteLength(frame, "utf8");
733
+ if (written.bytes > MAX_HOST_STDIN_BYTES) {
734
+ throw new Error("portable plugin host exceeded its outbound byte bound");
735
+ }
736
+ if (child.stdin.write(frame, "utf8")) return;
737
+ if (signal.aborted) {
738
+ throw new Error("portable plugin invocation was cancelled");
739
+ }
740
+ let rejectAbort: ((error: Error) => void) | null = null;
741
+ const onAbort = (): void => {
742
+ rejectAbort?.(new Error("portable plugin invocation was cancelled"));
743
+ };
744
+ try {
745
+ await Promise.race([
746
+ once(child.stdin, "drain").then(() => undefined),
747
+ new Promise<never>((_resolve, reject) => {
748
+ rejectAbort = reject;
749
+ signal.addEventListener("abort", onAbort, { once: true });
750
+ if (signal.aborted) onAbort();
751
+ }),
752
+ ]);
753
+ } finally {
754
+ rejectAbort = null;
755
+ signal.removeEventListener("abort", onAbort);
756
+ }
757
+ }
758
+
759
+ function valueFreeError(
760
+ code: string,
761
+ message: string,
762
+ planned: number,
763
+ started: number,
764
+ verified: number,
765
+ ): PortableProviderPluginHostError {
766
+ return new PortableProviderPluginHostError(
767
+ code,
768
+ message,
769
+ { planned, started, verified },
770
+ );
771
+ }
772
+
773
+ function asHostError(
774
+ error: unknown,
775
+ planned: number,
776
+ state: Pick<InvocationPolicyState, "started" | "verified">,
777
+ ): PortableProviderPluginHostError {
778
+ if (error instanceof PortableProviderPluginHostError) return error;
779
+ return valueFreeError(
780
+ "host-failed",
781
+ "portable provider plugin host failed",
782
+ planned,
783
+ state.started,
784
+ state.verified,
785
+ );
786
+ }
787
+
788
+ function hasProtocolStreams(
789
+ child: ChildProcess,
790
+ ): child is ChildProcessWithoutNullStreams {
791
+ return (
792
+ child.stdin !== null
793
+ && child.stdout !== null
794
+ && child.stderr !== null
795
+ );
796
+ }
797
+
798
+ async function admitPortableProviderPluginHost(
799
+ stream: Writable,
800
+ signal: AbortSignal,
801
+ ): Promise<void> {
802
+ await new Promise<void>((resolve, reject) => {
803
+ let settled = false;
804
+ const finish = (error?: Error | null): void => {
805
+ if (settled) return;
806
+ if (error !== undefined && error !== null) {
807
+ fail("portable plugin admission pipe failed");
808
+ return;
809
+ }
810
+ settled = true;
811
+ signal.removeEventListener("abort", onAbort);
812
+ resolve();
813
+ };
814
+ const fail = (message: string): void => {
815
+ if (settled) return;
816
+ settled = true;
817
+ signal.removeEventListener("abort", onAbort);
818
+ reject(new Error(message));
819
+ };
820
+ const onAbort = (): void => {
821
+ fail("portable plugin admission exceeded the invocation deadline");
822
+ };
823
+ const onError = (): void => {
824
+ // Retain this listener after settlement: a child pipe may report another
825
+ // close error while process cleanup is already in progress.
826
+ fail("portable plugin admission pipe failed");
827
+ };
828
+ stream.on("error", onError);
829
+ signal.addEventListener("abort", onAbort, { once: true });
830
+ if (signal.aborted) {
831
+ onAbort();
832
+ return;
833
+ }
834
+ // The guard consumes exactly the first stdin byte before the runtime starts
835
+ // and preserves any coalesced remainder. Writing it before host.hello
836
+ // orders admission ahead of every frame on the same durable stream.
837
+ stream.write(Buffer.from([1]), finish);
838
+ });
839
+ }
840
+
841
+ type StagedPortablePackage = {
842
+ readonly cwd: string;
843
+ readonly fd: number;
844
+ readonly childPath: string;
845
+ readonly guardPath: string;
846
+ readonly releaseParentDescriptor: () => void;
847
+ readonly dispose: () => void;
848
+ };
849
+
850
+ class UnverifiedPortablePackageStagingCleanupError extends Error {
851
+ constructor(cause: unknown, cleanupCause: unknown) {
852
+ super(
853
+ "portable provider plugin staging rollback could not be verified",
854
+ { cause: new AggregateError([cause, cleanupCause]) },
855
+ );
856
+ this.name = "UnverifiedPortablePackageStagingCleanupError";
857
+ }
858
+ }
859
+
860
+ function assertNoPortableRuntimeProcessImports(
861
+ runtimeBytes: Uint8Array,
862
+ ): void {
863
+ let source: string;
864
+ try {
865
+ source = new TextDecoder("utf-8", { fatal: true }).decode(runtimeBytes);
866
+ } catch (cause) {
867
+ throw new Error("portable plugin runtime is not valid UTF-8", { cause });
868
+ }
869
+ let imports: readonly { readonly path: string }[];
870
+ try {
871
+ imports = new Bun.Transpiler({ loader: "js" }).scan(source).imports;
872
+ } catch (cause) {
873
+ throw new Error("portable plugin runtime could not be parsed safely", {
874
+ cause,
875
+ });
876
+ }
877
+ if (
878
+ imports.some(({ path }) =>
879
+ PORTABLE_RUNTIME_PROCESS_MODULES.has(path)
880
+ )
881
+ ) {
882
+ throw new Error(
883
+ "portable plugin runtime process-creation imports are unavailable",
884
+ );
885
+ }
886
+ }
887
+
888
+ function stageVerifiedPortablePackage(
889
+ packageValue: VerifiedPortableProviderPluginPackage,
890
+ ): StagedPortablePackage {
891
+ if (
892
+ process.platform === "win32"
893
+ || !existsSync("/dev/fd")
894
+ || !existsSync(EMPTY_HOST_CONFIG_PATH)
895
+ ) {
896
+ throw new Error(
897
+ "portable provider plugin host cannot bind exact runtime bytes on this platform",
898
+ );
899
+ }
900
+ const directory = mkdtempSync(
901
+ join(tmpdir(), "wrench-portable-plugin-package-"),
902
+ );
903
+ const directories = new Set([directory]);
904
+ let reader = -1;
905
+ let descriptorState: "closed" | "open" | "unknown" = "closed";
906
+ let stagingDescriptorUnknown = false;
907
+ const cleanup = (): void => {
908
+ const failures: unknown[] = [];
909
+ if (stagingDescriptorUnknown) {
910
+ failures.push(new Error(
911
+ "staged portable plugin writer descriptor state is unknown",
912
+ ));
913
+ }
914
+ if (descriptorState === "open") {
915
+ try {
916
+ closeSync(reader);
917
+ descriptorState = "closed";
918
+ } catch (error) {
919
+ descriptorState = "unknown";
920
+ failures.push(error);
921
+ }
922
+ } else if (descriptorState === "unknown") {
923
+ failures.push(new Error(
924
+ "staged portable plugin runtime descriptor state is unknown",
925
+ ));
926
+ }
927
+ for (const stagedDirectory of directories) {
928
+ try {
929
+ chmodSync(stagedDirectory, 0o700);
930
+ } catch {
931
+ // The stage may not have reached directory creation or hardening.
932
+ }
933
+ }
934
+ try {
935
+ rmSync(directory, { recursive: true, force: true });
936
+ } catch (error) {
937
+ failures.push(error);
938
+ }
939
+ if (existsSync(directory)) {
940
+ failures.push(new Error(
941
+ "staged portable plugin package directory survived cleanup",
942
+ ));
943
+ }
944
+ if (failures.length > 0) {
945
+ throw new AggregateError(
946
+ failures,
947
+ "portable provider plugin stage cleanup failed",
948
+ );
949
+ }
950
+ };
951
+ const writeFile = (relativePath: string, bytes: Uint8Array): string => {
952
+ const segments = relativePath.split("/");
953
+ let parent = directory;
954
+ for (const segment of segments.slice(0, -1)) {
955
+ parent = join(parent, segment);
956
+ if (directories.has(parent)) continue;
957
+ mkdirSync(parent, { mode: 0o700 });
958
+ directories.add(parent);
959
+ }
960
+ const path = join(parent, segments.at(-1)!);
961
+ let writer = -1;
962
+ const closeWriter = (): void => {
963
+ try {
964
+ closeSync(writer);
965
+ writer = -1;
966
+ } catch (error) {
967
+ writer = -2;
968
+ stagingDescriptorUnknown = true;
969
+ throw error;
970
+ }
971
+ };
972
+ try {
973
+ writer = openSync(
974
+ path,
975
+ constants.O_WRONLY
976
+ | constants.O_CREAT
977
+ | constants.O_EXCL
978
+ | constants.O_NOFOLLOW,
979
+ 0o400,
980
+ );
981
+ let written = 0;
982
+ while (written < bytes.byteLength) {
983
+ written += writeSync(
984
+ writer,
985
+ bytes,
986
+ written,
987
+ bytes.byteLength - written,
988
+ );
989
+ }
990
+ fsyncSync(writer);
991
+ closeWriter();
992
+ return path;
993
+ } finally {
994
+ if (writer >= 0) closeWriter();
995
+ }
996
+ };
997
+ try {
998
+ chmodSync(directory, 0o700);
999
+ hostProcessObserverForTest?.staged?.({ cwd: directory });
1000
+ const guardPath = writeFile(
1001
+ HOST_GUARD_NAME,
1002
+ Buffer.from(HOST_GUARD_SOURCE, "utf8"),
1003
+ );
1004
+ writeFile(
1005
+ PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME,
1006
+ packageValue.manifestBytes,
1007
+ );
1008
+ let runtimePath: string | null = null;
1009
+ let runtimeBytes: Buffer | null = null;
1010
+ for (const file of packageValue.files) {
1011
+ const path = writeFile(file.path, file.bytes);
1012
+ if (file.kind === "runtime") {
1013
+ runtimePath = path;
1014
+ runtimeBytes = file.bytes;
1015
+ }
1016
+ }
1017
+ if (runtimePath === null || runtimeBytes === null) {
1018
+ throw new Error("portable provider plugin verified runtime is absent");
1019
+ }
1020
+ assertNoPortableRuntimeProcessImports(runtimeBytes);
1021
+ reader = openSync(
1022
+ runtimePath,
1023
+ constants.O_RDONLY | constants.O_NOFOLLOW,
1024
+ );
1025
+ descriptorState = "open";
1026
+ const stats = fstatSync(reader);
1027
+ if (!stats.isFile() || stats.size !== runtimeBytes.byteLength) {
1028
+ throw new Error("staged portable plugin runtime has the wrong identity");
1029
+ }
1030
+ const checked = Buffer.allocUnsafe(runtimeBytes.byteLength);
1031
+ let read = 0;
1032
+ while (read < checked.byteLength) {
1033
+ const count = readSync(
1034
+ reader,
1035
+ checked,
1036
+ read,
1037
+ checked.byteLength - read,
1038
+ read,
1039
+ );
1040
+ if (count === 0) break;
1041
+ read += count;
1042
+ }
1043
+ if (
1044
+ read !== runtimeBytes.byteLength
1045
+ || !checked.equals(runtimeBytes)
1046
+ ) {
1047
+ throw new Error("staged portable plugin runtime bytes changed");
1048
+ }
1049
+ for (const stagedDirectory of directories) {
1050
+ chmodSync(stagedDirectory, 0o500);
1051
+ }
1052
+ return {
1053
+ cwd: directory,
1054
+ fd: reader,
1055
+ childPath: `/dev/fd/${RUNTIME_CHILD_FD}`,
1056
+ guardPath,
1057
+ releaseParentDescriptor: () => {
1058
+ if (descriptorState === "closed") return;
1059
+ if (descriptorState === "unknown") {
1060
+ throw new Error(
1061
+ "staged portable plugin runtime descriptor state is unknown",
1062
+ );
1063
+ }
1064
+ hostProcessObserverForTest?.beforeParentDescriptorRelease?.();
1065
+ closeSync(reader);
1066
+ descriptorState = "closed";
1067
+ },
1068
+ dispose: cleanup,
1069
+ };
1070
+ } catch (error) {
1071
+ try {
1072
+ cleanup();
1073
+ } catch (cleanupError) {
1074
+ throw new UnverifiedPortablePackageStagingCleanupError(
1075
+ error,
1076
+ cleanupError,
1077
+ );
1078
+ }
1079
+ throw error;
1080
+ }
1081
+ }
1082
+
1083
+ function descendantProcessIds(rootPid: number): readonly number[] {
1084
+ const probe = spawnSync(
1085
+ "/bin/ps",
1086
+ ["-axo", "pid=,ppid="],
1087
+ {
1088
+ encoding: "utf8",
1089
+ env: {
1090
+ LANG: "C",
1091
+ LC_ALL: "C",
1092
+ NODE_ENV: "production",
1093
+ PATH: "/usr/bin:/bin",
1094
+ },
1095
+ maxBuffer: 1024 * 1024,
1096
+ },
1097
+ );
1098
+ if (probe.error !== undefined) throw probe.error;
1099
+ if (probe.status !== 0) {
1100
+ throw new Error("portable plugin process tree could not be inspected");
1101
+ }
1102
+ const children = new Map<number, number[]>();
1103
+ for (const line of probe.stdout.split("\n")) {
1104
+ if (line.trim() === "") continue;
1105
+ const match = /^\s*(\d+)\s+(\d+)\s*$/u.exec(line);
1106
+ if (match === null) {
1107
+ throw new Error("portable plugin process tree returned malformed data");
1108
+ }
1109
+ const pid = Number(match[1]);
1110
+ const parentPid = Number(match[2]);
1111
+ if (
1112
+ !Number.isSafeInteger(pid)
1113
+ || pid < 1
1114
+ || !Number.isSafeInteger(parentPid)
1115
+ || parentPid < 0
1116
+ ) {
1117
+ throw new Error("portable plugin process tree returned invalid identities");
1118
+ }
1119
+ const values = children.get(parentPid) ?? [];
1120
+ values.push(pid);
1121
+ children.set(parentPid, values);
1122
+ }
1123
+ const descendants: number[] = [];
1124
+ const pending = [...(children.get(rootPid) ?? [])];
1125
+ while (pending.length > 0) {
1126
+ const pid = pending.shift();
1127
+ if (pid === undefined) break;
1128
+ descendants.push(pid);
1129
+ if (descendants.length > MAX_CONTAINED_DESCENDANTS) {
1130
+ throw new Error("portable plugin exceeded the descendant process bound");
1131
+ }
1132
+ pending.push(...(children.get(pid) ?? []));
1133
+ }
1134
+ return Object.freeze(descendants);
1135
+ }
1136
+
1137
+ async function terminateChild(
1138
+ child: ChildProcess,
1139
+ closed: Promise<void>,
1140
+ options: {
1141
+ readonly beforeTermination?: () => Promise<void>;
1142
+ readonly hostIdentity?: ProcessOwnerIdentity;
1143
+ readonly portableCodeAdmitted: boolean;
1144
+ },
1145
+ ): Promise<void> {
1146
+ let descendantObserved = false;
1147
+ let containmentProofError: unknown = null;
1148
+ const retainProofError = (error: unknown): void => {
1149
+ if (containmentProofError === null) containmentProofError = error;
1150
+ };
1151
+ const inspectContainment = (): void => {
1152
+ if (!options.portableCodeAdmitted) return;
1153
+ const hostIdentity = options.hostIdentity;
1154
+ if (hostIdentity === undefined) {
1155
+ retainProofError(new Error(
1156
+ "admitted portable plugin host identity is unavailable",
1157
+ ));
1158
+ return;
1159
+ }
1160
+ if (processOwnerStatus(hostIdentity) !== "exact-live-owner") {
1161
+ retainProofError(new Error(
1162
+ "portable plugin host identity was not live during containment inspection",
1163
+ ));
1164
+ return;
1165
+ }
1166
+ let descendants: readonly number[];
1167
+ try {
1168
+ descendants = (
1169
+ hostProcessObserverForTest?.descendantProcessIds?.(hostIdentity)
1170
+ ?? descendantProcessIds(hostIdentity.pid)
1171
+ );
1172
+ if (descendants.length > MAX_CONTAINED_DESCENDANTS) {
1173
+ throw new Error("portable plugin exceeded the descendant process bound");
1174
+ }
1175
+ for (const pid of descendants) {
1176
+ if (!Number.isSafeInteger(pid) || pid < 1) {
1177
+ throw new Error(
1178
+ "portable plugin process tree returned invalid identities",
1179
+ );
1180
+ }
1181
+ }
1182
+ } catch (error) {
1183
+ retainProofError(error);
1184
+ return;
1185
+ }
1186
+ if (processOwnerStatus(hostIdentity) !== "exact-live-owner") {
1187
+ retainProofError(new Error(
1188
+ "portable plugin host identity changed during containment inspection",
1189
+ ));
1190
+ return;
1191
+ }
1192
+ if (descendants.length > 0) descendantObserved = true;
1193
+ };
1194
+ const signalDirectChild = (value: NodeJS.Signals): void => {
1195
+ if (child.exitCode !== null || child.signalCode !== null) return;
1196
+ try {
1197
+ // The owned ChildProcess handle remains bound until reaping. Never turn
1198
+ // its numeric PID into a later process or process-group signal target.
1199
+ child.kill(value);
1200
+ } catch (error) {
1201
+ retainProofError(error);
1202
+ }
1203
+ };
1204
+ const closesWithin = async (maximumMs: number): Promise<boolean> => {
1205
+ let timer: ReturnType<typeof setTimeout> | undefined;
1206
+ try {
1207
+ return await Promise.race([
1208
+ closed.then(() => true as const),
1209
+ new Promise<false>((resolve) => {
1210
+ timer = setTimeout(() => resolve(false), maximumMs);
1211
+ }),
1212
+ ]);
1213
+ } finally {
1214
+ if (timer !== undefined) clearTimeout(timer);
1215
+ }
1216
+ };
1217
+ const destroyProtocolStreams = (): void => {
1218
+ for (const stream of child.stdio) stream?.destroy();
1219
+ };
1220
+
1221
+ inspectContainment();
1222
+ try {
1223
+ await options.beforeTermination?.();
1224
+ } catch {
1225
+ // Direct process termination remains the cleanup boundary.
1226
+ }
1227
+ child.stdin?.end();
1228
+ signalDirectChild("SIGTERM");
1229
+ let directChildClosed = await closesWithin(TERMINATION_GRACE_MS);
1230
+ if (!directChildClosed) {
1231
+ inspectContainment();
1232
+ signalDirectChild("SIGKILL");
1233
+ directChildClosed = await closesWithin(TERMINATION_CONFIRMATION_MS);
1234
+ }
1235
+ if (!directChildClosed) {
1236
+ destroyProtocolStreams();
1237
+ await closesWithin(TERMINATION_STREAM_CLOSE_MS);
1238
+ throw new Error(
1239
+ "portable plugin invocation process survived forced termination",
1240
+ containmentProofError === null
1241
+ ? undefined
1242
+ : { cause: containmentProofError },
1243
+ );
1244
+ }
1245
+ if (descendantObserved) {
1246
+ retainProofError(new Error(
1247
+ "portable plugin descendant cleanup cannot be proven without atomic process ownership",
1248
+ ));
1249
+ }
1250
+ if (containmentProofError !== null) {
1251
+ destroyProtocolStreams();
1252
+ throw new Error(
1253
+ "portable plugin invocation containment could not be verified",
1254
+ { cause: containmentProofError },
1255
+ );
1256
+ }
1257
+ }
1258
+
1259
+ /**
1260
+ * Run one exact installed portable provider plugin through the v1 framed
1261
+ * protocol.
1262
+ *
1263
+ * The child process isolates crashes and dependencies. It is deliberately not
1264
+ * described as a hostile-code sandbox: explicitly trusted same-account
1265
+ * JavaScript still has ambient OS authority. Every host-provided capability,
1266
+ * however, is denied unless both the immutable manifest and invocation grant
1267
+ * it.
1268
+ */
1269
+ async function runPortableProviderPluginHostCore(
1270
+ invocation: PortableProviderPluginHostInvocation,
1271
+ cleanupBarrier: PortableProviderPluginCleanupBarrier,
1272
+ ): Promise<PortableProviderPluginHostResult> {
1273
+ let verified: VerifiedPortableProviderPluginPackage;
1274
+ try {
1275
+ verified = verifyPortableProviderPluginPackageDirectory(
1276
+ invocation.package.root,
1277
+ );
1278
+ assertInvocation(invocation, verified);
1279
+ } catch (error) {
1280
+ // Validation has not staged bytes or started a child.
1281
+ cleanupBarrier.verified();
1282
+ throw error;
1283
+ }
1284
+ const binding = routeBinding(verified, invocation.route);
1285
+ const plannedDispatchIds = invocation.plannedDispatchIds ?? [];
1286
+ const invocationId = crypto.randomUUID();
1287
+ const identity = Object.freeze({
1288
+ id: verified.manifest.id,
1289
+ version: verified.manifest.version,
1290
+ bundleSha256: verified.bundleSha256,
1291
+ });
1292
+ const executable = invocation.bunExecutable ?? process.execPath;
1293
+ let stagedPackage: StagedPortablePackage;
1294
+ try {
1295
+ stagedPackage = stageVerifiedPortablePackage(verified);
1296
+ } catch (error) {
1297
+ // Ordinary staging failures own and prove their synchronous rollback.
1298
+ // Only an explicitly unverified descriptor/directory cleanup retains the
1299
+ // invocation lease.
1300
+ if (error instanceof UnverifiedPortablePackageStagingCleanupError) {
1301
+ cleanupBarrier.unsafe(error);
1302
+ } else {
1303
+ cleanupBarrier.verified();
1304
+ }
1305
+ throw error;
1306
+ }
1307
+ const childArgs = Object.freeze([
1308
+ "--no-env-file",
1309
+ "--no-install",
1310
+ "--no-macros",
1311
+ "--no-addons",
1312
+ `--config=${EMPTY_HOST_CONFIG_PATH}`,
1313
+ `--preload=${stagedPackage.guardPath}`,
1314
+ stagedPackage.childPath,
1315
+ ]);
1316
+ const processObserver = hostProcessObserverForTest;
1317
+ const isolatesProcessGroup = process.platform !== "win32";
1318
+ let child: ChildProcessWithoutNullStreams;
1319
+ let childClosed: Promise<void>;
1320
+ let childIdentity: ProcessOwnerIdentity;
1321
+ let spawned: ChildProcess | undefined;
1322
+ let spawnedClosed: Promise<void> | undefined;
1323
+ let spawnedIdentity: ProcessOwnerIdentity | undefined;
1324
+ let portableCodeAdmitted = false;
1325
+ const timeoutController = new AbortController();
1326
+ const timeout = setTimeout(
1327
+ () => timeoutController.abort(),
1328
+ invocation.timeoutMs,
1329
+ );
1330
+ timeout.unref();
1331
+ const onCallerAbort = (): void => timeoutController.abort();
1332
+ if (invocation.signal?.aborted === true) timeoutController.abort();
1333
+ else invocation.signal?.addEventListener("abort", onCallerAbort, { once: true });
1334
+ try {
1335
+ processObserver?.beforeSpawn?.({
1336
+ executable,
1337
+ args: childArgs,
1338
+ cwd: stagedPackage.cwd,
1339
+ });
1340
+ spawned = spawn(executable, childArgs, {
1341
+ cwd: stagedPackage.cwd,
1342
+ env: {
1343
+ LANG: "C",
1344
+ LC_ALL: "C",
1345
+ NODE_ENV: "production",
1346
+ NO_COLOR: "1",
1347
+ PATH: dirname(executable),
1348
+ TZ: "UTC",
1349
+ },
1350
+ stdio: ["pipe", "pipe", "pipe", stagedPackage.fd],
1351
+ detached: isolatesProcessGroup,
1352
+ windowsHide: true,
1353
+ });
1354
+ spawnedClosed = new Promise<void>((resolve) => {
1355
+ spawned?.once("close", () => resolve());
1356
+ });
1357
+ spawned.once("exit", (code, signal) => {
1358
+ processObserver?.exit?.({ code, signal });
1359
+ });
1360
+ const spawnedPid = spawned.pid;
1361
+ if (spawnedPid === undefined) {
1362
+ throw new Error("portable plugin process has no process identity");
1363
+ }
1364
+ spawnedIdentity = captureProcessOwnerIdentity(spawnedPid);
1365
+ announcePortableProviderPluginHostStarting(spawnedIdentity);
1366
+ stagedPackage.releaseParentDescriptor();
1367
+ if (!hasProtocolStreams(spawned)) {
1368
+ throw new Error("portable plugin process has no protocol streams");
1369
+ }
1370
+ announcePortableProviderPluginHostStarted(spawnedIdentity);
1371
+ portableCodeAdmitted = true;
1372
+ await admitPortableProviderPluginHost(
1373
+ spawned.stdin,
1374
+ timeoutController.signal,
1375
+ );
1376
+ child = spawned;
1377
+ childClosed = spawnedClosed;
1378
+ childIdentity = spawnedIdentity;
1379
+ } catch (error) {
1380
+ const setupError = timeoutController.signal.aborted
1381
+ ? valueFreeError(
1382
+ invocation.signal?.aborted === true ? "cancelled" : "timeout",
1383
+ invocation.signal?.aborted === true
1384
+ ? "portable provider plugin invocation was cancelled"
1385
+ : "portable provider plugin invocation exceeded its total deadline",
1386
+ plannedDispatchIds.length,
1387
+ 0,
1388
+ 0,
1389
+ )
1390
+ : asHostError(
1391
+ error,
1392
+ plannedDispatchIds.length,
1393
+ { started: 0, verified: 0 },
1394
+ );
1395
+ clearTimeout(timeout);
1396
+ invocation.signal?.removeEventListener("abort", onCallerAbort);
1397
+ const cleanupFailures: unknown[] = [];
1398
+ if (spawned !== undefined && spawnedClosed !== undefined) {
1399
+ try {
1400
+ await terminateChild(spawned, spawnedClosed, {
1401
+ ...(spawnedIdentity === undefined
1402
+ ? {}
1403
+ : { hostIdentity: spawnedIdentity }),
1404
+ portableCodeAdmitted,
1405
+ });
1406
+ } catch (candidate) {
1407
+ cleanupFailures.push(candidate);
1408
+ }
1409
+ }
1410
+ try {
1411
+ stagedPackage.dispose();
1412
+ } catch (candidate) {
1413
+ cleanupFailures.push(candidate);
1414
+ }
1415
+ if (cleanupFailures.length > 0) {
1416
+ cleanupBarrier.unsafe(new AggregateError(
1417
+ cleanupFailures,
1418
+ "portable plugin setup cleanup failed",
1419
+ ));
1420
+ throw asHostError(
1421
+ new AggregateError(
1422
+ [setupError, ...cleanupFailures],
1423
+ "portable plugin setup and cleanup failed",
1424
+ ),
1425
+ plannedDispatchIds.length,
1426
+ { started: 0, verified: 0 },
1427
+ );
1428
+ } else {
1429
+ cleanupBarrier.verified();
1430
+ }
1431
+ throw setupError;
1432
+ }
1433
+ const queue = new ProcessMessageQueue();
1434
+ const decoder = new PortableProviderPluginFrameDecoder();
1435
+ let stdoutBytes = 0;
1436
+ let stderrBytes = 0;
1437
+ let messageCount = 0;
1438
+ let streamFinished = false;
1439
+ let resultReceived = false;
1440
+ const state: InvocationPolicyState = {
1441
+ requestIds: new Set(),
1442
+ filesByHandle: new Map(
1443
+ invocation.files.map((file) => [file.handle, file] as const),
1444
+ ),
1445
+ materialHandles: new Map(),
1446
+ dispatchHandles: new Map(),
1447
+ started: 0,
1448
+ verified: 0,
1449
+ };
1450
+
1451
+ child.stdout.on("data", (chunkValue: unknown) => {
1452
+ if (!Buffer.isBuffer(chunkValue)) {
1453
+ queue.fail(new Error("portable plugin stdout returned non-byte data"));
1454
+ return;
1455
+ }
1456
+ stdoutBytes += chunkValue.byteLength;
1457
+ if (stdoutBytes > MAX_HOST_STDOUT_BYTES) {
1458
+ queue.fail(new Error("portable plugin stdout exceeded the invocation bound"));
1459
+ return;
1460
+ }
1461
+ try {
1462
+ for (const message of decoder.push(chunkValue)) {
1463
+ messageCount += 1;
1464
+ if (messageCount > MAX_HOST_MESSAGES) {
1465
+ throw new Error("portable plugin exceeded the message-count bound");
1466
+ }
1467
+ if (!isProcessMessage(message)) {
1468
+ throw new Error("portable plugin emitted a host-direction message");
1469
+ }
1470
+ queue.push(message);
1471
+ }
1472
+ } catch (error) {
1473
+ queue.fail(
1474
+ error instanceof Error
1475
+ ? error
1476
+ : new Error("portable plugin stdout was malformed"),
1477
+ );
1478
+ }
1479
+ });
1480
+ child.stdout.once("end", () => {
1481
+ if (streamFinished) return;
1482
+ streamFinished = true;
1483
+ try {
1484
+ decoder.finish();
1485
+ } catch (error) {
1486
+ queue.fail(
1487
+ error instanceof Error
1488
+ ? error
1489
+ : new Error("portable plugin stdout ended incorrectly"),
1490
+ );
1491
+ }
1492
+ });
1493
+ child.stderr.on("data", (chunkValue: unknown) => {
1494
+ if (!Buffer.isBuffer(chunkValue)) {
1495
+ queue.fail(new Error("portable plugin stderr returned non-byte data"));
1496
+ return;
1497
+ }
1498
+ stderrBytes += chunkValue.byteLength;
1499
+ if (stderrBytes > MAX_HOST_STDERR_BYTES) {
1500
+ queue.fail(new Error("portable plugin stderr exceeded the invocation bound"));
1501
+ }
1502
+ });
1503
+ child.once("error", () => {
1504
+ queue.fail(new Error("portable plugin process could not be started"));
1505
+ });
1506
+ child.stdin.once("error", () => {
1507
+ queue.fail(new Error("portable plugin stdin failed"));
1508
+ });
1509
+ child.once("exit", () => {
1510
+ if (!resultReceived) {
1511
+ queue.fail(new Error("portable plugin process exited before completing a response"));
1512
+ }
1513
+ });
1514
+
1515
+ const aborted = new Promise<never>((_resolve, reject) => {
1516
+ if (timeoutController.signal.aborted) {
1517
+ reject(new Error("portable plugin invocation was cancelled"));
1518
+ return;
1519
+ }
1520
+ timeoutController.signal.addEventListener(
1521
+ "abort",
1522
+ () => reject(new Error("portable plugin invocation was cancelled")),
1523
+ { once: true },
1524
+ );
1525
+ });
1526
+ const nextMessage = (): Promise<ProcessMessage> =>
1527
+ Promise.race([queue.next(), aborted]);
1528
+ const written = { bytes: 0 };
1529
+
1530
+ let invoked = false;
1531
+ let executionResult: PortableProviderPluginHostResult | null = null;
1532
+ let executionError: PortableProviderPluginHostError | null = null;
1533
+ try {
1534
+ await writeHostMessage(child, hostMessage({
1535
+ protocolVersion: PORTABLE_PROVIDER_PLUGIN_PROTOCOL_VERSION,
1536
+ kind: "host.hello",
1537
+ hostVersion: invocation.hostVersion,
1538
+ hostApiVersion: PORTABLE_PROVIDER_PLUGIN_HOST_API_VERSION,
1539
+ plugin: identity,
1540
+ granted: verified.manifest.capabilities,
1541
+ }), timeoutController.signal, written);
1542
+ const ready = await nextMessage();
1543
+ if (
1544
+ ready.kind !== "plugin.ready"
1545
+ || ready.plugin.id !== identity.id
1546
+ || ready.plugin.version !== identity.version
1547
+ || ready.plugin.bundleSha256 !== identity.bundleSha256
1548
+ ) {
1549
+ throw valueFreeError(
1550
+ "handshake-failed",
1551
+ "portable provider plugin did not accept its exact installed identity",
1552
+ plannedDispatchIds.length,
1553
+ state.started,
1554
+ state.verified,
1555
+ );
1556
+ }
1557
+ await writeHostMessage(child, hostMessage({
1558
+ protocolVersion: PORTABLE_PROVIDER_PLUGIN_PROTOCOL_VERSION,
1559
+ kind: "host.invoke",
1560
+ invocationId,
1561
+ route: invocation.route,
1562
+ input: invocation.input,
1563
+ auth: invocation.auth,
1564
+ files: invocation.files,
1565
+ timeoutMs: invocation.timeoutMs,
1566
+ }), timeoutController.signal, written);
1567
+ invoked = true;
1568
+
1569
+ for (;;) {
1570
+ const message = await nextMessage();
1571
+ if (message.kind === "plugin.capability.request") {
1572
+ if (
1573
+ message.invocationId !== invocationId
1574
+ || state.requestIds.has(message.requestId)
1575
+ || state.requestIds.size >= MAX_HOST_MESSAGES
1576
+ ) {
1577
+ throw valueFreeError(
1578
+ "protocol-violation",
1579
+ "portable provider plugin emitted an invalid capability request",
1580
+ plannedDispatchIds.length,
1581
+ state.started,
1582
+ state.verified,
1583
+ );
1584
+ }
1585
+ state.requestIds.add(message.requestId);
1586
+ const declared = capabilityIsDeclared(
1587
+ message.request,
1588
+ verified.manifest.capabilities,
1589
+ binding,
1590
+ invocation.auth,
1591
+ plannedDispatchIds,
1592
+ state,
1593
+ );
1594
+ if (
1595
+ !declared
1596
+ && (
1597
+ message.request.kind === "dispatch.begin"
1598
+ || message.request.kind === "dispatch.verify"
1599
+ || (
1600
+ message.request.kind === "http.request"
1601
+ && (
1602
+ message.request.dispatchHandle !== undefined
1603
+ || (
1604
+ message.request.method !== "GET"
1605
+ && message.request.method !== "HEAD"
1606
+ )
1607
+ )
1608
+ )
1609
+ )
1610
+ ) {
1611
+ throw valueFreeError(
1612
+ "protocol-violation",
1613
+ "portable provider plugin violated its confirmed dispatch boundary",
1614
+ plannedDispatchIds.length,
1615
+ state.started,
1616
+ state.verified,
1617
+ );
1618
+ }
1619
+ const mutationDispatch = (
1620
+ declared
1621
+ && invocation.capabilityHost !== undefined
1622
+ && message.request.kind === "http.request"
1623
+ && message.request.method !== "GET"
1624
+ && message.request.method !== "HEAD"
1625
+ && message.request.dispatchHandle !== undefined
1626
+ )
1627
+ ? state.dispatchHandles.get(message.request.dispatchHandle)
1628
+ : undefined;
1629
+ if (mutationDispatch !== undefined) {
1630
+ // Consume the one remote-write attempt before crossing the host
1631
+ // capability boundary. A reset, timeout, oversized response, or
1632
+ // malformed host result may occur after the provider accepted the
1633
+ // write, so the same dispatch handle must never become reusable.
1634
+ mutationDispatch.mutationAttempted = true;
1635
+ }
1636
+ let result: PortablePluginCapabilityResult | null = null;
1637
+ if (declared && message.request.kind === "log.write") {
1638
+ result = { kind: "log.write", accepted: true };
1639
+ } else if (declared && invocation.capabilityHost !== undefined) {
1640
+ try {
1641
+ const candidate = await Promise.race([
1642
+ invocation.capabilityHost.handle(
1643
+ message.request,
1644
+ {
1645
+ invocationId,
1646
+ requestId: message.requestId,
1647
+ route: invocation.route,
1648
+ signal: timeoutController.signal,
1649
+ },
1650
+ ),
1651
+ aborted,
1652
+ ]);
1653
+ const normalized = normalizedCapabilityResult(
1654
+ invocationId,
1655
+ message.requestId,
1656
+ candidate,
1657
+ );
1658
+ if (matchingCapabilityResult(
1659
+ message.request,
1660
+ normalized,
1661
+ state.filesByHandle,
1662
+ )) {
1663
+ result = normalized;
1664
+ }
1665
+ } catch {
1666
+ if (mutationDispatch !== undefined) {
1667
+ throw valueFreeError(
1668
+ "dispatch-indeterminate",
1669
+ "portable provider plugin mutation failed after its dispatch boundary began",
1670
+ plannedDispatchIds.length,
1671
+ state.started,
1672
+ state.verified,
1673
+ );
1674
+ }
1675
+ if (timeoutController.signal.aborted) {
1676
+ throw new Error("portable plugin capability exceeded the invocation deadline");
1677
+ }
1678
+ // Capability failures are returned without carrying foreign values.
1679
+ }
1680
+ }
1681
+ if (result === null) {
1682
+ if (mutationDispatch !== undefined) {
1683
+ throw valueFreeError(
1684
+ "dispatch-indeterminate",
1685
+ "portable provider plugin mutation returned no valid result after its dispatch boundary began",
1686
+ plannedDispatchIds.length,
1687
+ state.started,
1688
+ state.verified,
1689
+ );
1690
+ }
1691
+ await writeHostMessage(child, hostMessage({
1692
+ protocolVersion: PORTABLE_PROVIDER_PLUGIN_PROTOCOL_VERSION,
1693
+ kind: "host.capability.error",
1694
+ invocationId,
1695
+ requestId: message.requestId,
1696
+ capability: message.request.kind,
1697
+ error: {
1698
+ code: declared ? "CAPABILITY_FAILED" : "CAPABILITY_DENIED",
1699
+ message: declared
1700
+ ? "host capability failed"
1701
+ : "requested capability is not granted",
1702
+ },
1703
+ }), timeoutController.signal, written);
1704
+ continue;
1705
+ }
1706
+ if (
1707
+ message.request.kind === "dispatch.begin"
1708
+ && result.kind === "dispatch.begin"
1709
+ ) {
1710
+ if (state.dispatchHandles.has(result.dispatchHandle)) {
1711
+ throw valueFreeError(
1712
+ "protocol-violation",
1713
+ "portable provider plugin host returned a repeated dispatch handle",
1714
+ plannedDispatchIds.length,
1715
+ state.started,
1716
+ state.verified,
1717
+ );
1718
+ }
1719
+ state.dispatchHandles.set(result.dispatchHandle, {
1720
+ dispatchId: message.request.dispatchId,
1721
+ verified: false,
1722
+ mutationAttempted: false,
1723
+ mutationCompleted: false,
1724
+ });
1725
+ state.started += 1;
1726
+ } else if (
1727
+ message.request.kind === "http.request"
1728
+ && message.request.method !== "GET"
1729
+ && message.request.method !== "HEAD"
1730
+ && message.request.dispatchHandle !== undefined
1731
+ && result.kind === "http.request"
1732
+ ) {
1733
+ const dispatch = state.dispatchHandles.get(
1734
+ message.request.dispatchHandle,
1735
+ );
1736
+ if (
1737
+ dispatch === undefined
1738
+ || dispatch.verified
1739
+ || !dispatch.mutationAttempted
1740
+ || dispatch.mutationCompleted
1741
+ ) {
1742
+ throw valueFreeError(
1743
+ "protocol-violation",
1744
+ "portable provider plugin used an invalid mutation dispatch handle",
1745
+ plannedDispatchIds.length,
1746
+ state.started,
1747
+ state.verified,
1748
+ );
1749
+ }
1750
+ dispatch.mutationCompleted = true;
1751
+ } else if (
1752
+ message.request.kind === "dispatch.verify"
1753
+ && result.kind === "dispatch.verify"
1754
+ ) {
1755
+ const dispatch = state.dispatchHandles.get(
1756
+ message.request.dispatchHandle,
1757
+ );
1758
+ if (dispatch === undefined || dispatch.verified) {
1759
+ throw valueFreeError(
1760
+ "protocol-violation",
1761
+ "portable provider plugin verified an unknown dispatch",
1762
+ plannedDispatchIds.length,
1763
+ state.started,
1764
+ state.verified,
1765
+ );
1766
+ }
1767
+ dispatch.verified = true;
1768
+ state.verified += 1;
1769
+ } else if (
1770
+ message.request.kind === "session.acquire"
1771
+ && result.kind === "session.acquire"
1772
+ ) {
1773
+ if (state.materialHandles.has(result.materialHandle)) {
1774
+ throw valueFreeError(
1775
+ "protocol-violation",
1776
+ "portable provider plugin host returned a repeated material handle",
1777
+ plannedDispatchIds.length,
1778
+ state.started,
1779
+ state.verified,
1780
+ );
1781
+ }
1782
+ state.materialHandles.set(
1783
+ result.materialHandle,
1784
+ message.request.name,
1785
+ );
1786
+ }
1787
+ await writeHostMessage(child, hostMessage({
1788
+ protocolVersion: PORTABLE_PROVIDER_PLUGIN_PROTOCOL_VERSION,
1789
+ kind: "host.capability.result",
1790
+ invocationId,
1791
+ requestId: message.requestId,
1792
+ result,
1793
+ }), timeoutController.signal, written);
1794
+ continue;
1795
+ }
1796
+ if (message.kind === "plugin.result") {
1797
+ if (message.invocationId !== invocationId) {
1798
+ throw valueFreeError(
1799
+ "protocol-violation",
1800
+ "portable provider plugin returned a result for another invocation",
1801
+ plannedDispatchIds.length,
1802
+ state.started,
1803
+ state.verified,
1804
+ );
1805
+ }
1806
+ if (
1807
+ state.started !== plannedDispatchIds.length
1808
+ || state.verified !== state.started
1809
+ ) {
1810
+ throw valueFreeError(
1811
+ state.started > state.verified
1812
+ ? "dispatch-indeterminate"
1813
+ : "dispatch-incomplete",
1814
+ state.started > state.verified
1815
+ ? "portable provider plugin returned before verifying a started dispatch"
1816
+ : "portable provider plugin returned without completing its confirmed dispatch plan",
1817
+ plannedDispatchIds.length,
1818
+ state.started,
1819
+ state.verified,
1820
+ );
1821
+ }
1822
+ if (
1823
+ message.finalUrl !== null
1824
+ && !verified.manifest.capabilities.networkOrigins.includes(
1825
+ exactOrigin(message.finalUrl) as `https://${string}`,
1826
+ )
1827
+ ) {
1828
+ throw valueFreeError(
1829
+ "origin-violation",
1830
+ "portable provider plugin returned a final URL outside its granted origins",
1831
+ plannedDispatchIds.length,
1832
+ state.started,
1833
+ state.verified,
1834
+ );
1835
+ }
1836
+ resultReceived = true;
1837
+ executionResult = Object.freeze({
1838
+ output: message.output,
1839
+ finalUrl: message.finalUrl,
1840
+ dispatch: Object.freeze({
1841
+ planned: plannedDispatchIds.length,
1842
+ started: state.started,
1843
+ verified: state.verified,
1844
+ }),
1845
+ });
1846
+ break;
1847
+ }
1848
+ if (message.kind === "plugin.cancelled") {
1849
+ throw valueFreeError(
1850
+ "plugin-cancelled",
1851
+ "portable provider plugin cancelled its invocation",
1852
+ plannedDispatchIds.length,
1853
+ state.started,
1854
+ state.verified,
1855
+ );
1856
+ }
1857
+ throw valueFreeError(
1858
+ message.kind === "plugin.error"
1859
+ ? "plugin-error"
1860
+ : "protocol-violation",
1861
+ message.kind === "plugin.error"
1862
+ ? "portable provider plugin reported an execution failure"
1863
+ : "portable provider plugin emitted an unexpected message",
1864
+ plannedDispatchIds.length,
1865
+ state.started,
1866
+ state.verified,
1867
+ );
1868
+ }
1869
+ } catch (error) {
1870
+ if (
1871
+ timeoutController.signal.aborted
1872
+ && !(error instanceof PortableProviderPluginHostError)
1873
+ ) {
1874
+ executionError = valueFreeError(
1875
+ invocation.signal?.aborted === true ? "cancelled" : "timeout",
1876
+ invocation.signal?.aborted === true
1877
+ ? "portable provider plugin invocation was cancelled"
1878
+ : "portable provider plugin invocation exceeded its total deadline",
1879
+ plannedDispatchIds.length,
1880
+ state.started,
1881
+ state.verified,
1882
+ );
1883
+ } else {
1884
+ executionError = asHostError(error, plannedDispatchIds.length, state);
1885
+ }
1886
+ }
1887
+
1888
+ clearTimeout(timeout);
1889
+ invocation.signal?.removeEventListener("abort", onCallerAbort);
1890
+ let cleanupError: unknown = null;
1891
+ try {
1892
+ await terminateChild(
1893
+ child,
1894
+ childClosed,
1895
+ {
1896
+ beforeTermination: async () => {
1897
+ if (!invoked || !child.stdin.writable) return;
1898
+ const cleanupController = new AbortController();
1899
+ const cleanupTimer = setTimeout(
1900
+ () => cleanupController.abort(),
1901
+ TERMINATION_GRACE_MS,
1902
+ );
1903
+ try {
1904
+ await writeHostMessage(child, hostMessage({
1905
+ protocolVersion: PORTABLE_PROVIDER_PLUGIN_PROTOCOL_VERSION,
1906
+ kind: "host.cancel",
1907
+ invocationId,
1908
+ reason: timeoutController.signal.aborted
1909
+ ? "timeout"
1910
+ : "shutdown",
1911
+ }), cleanupController.signal, written);
1912
+ } finally {
1913
+ clearTimeout(cleanupTimer);
1914
+ }
1915
+ },
1916
+ hostIdentity: childIdentity,
1917
+ portableCodeAdmitted: true,
1918
+ },
1919
+ );
1920
+ } catch (error) {
1921
+ cleanupError = error;
1922
+ }
1923
+ try {
1924
+ stagedPackage.dispose();
1925
+ } catch (error) {
1926
+ if (cleanupError === null) cleanupError = error;
1927
+ }
1928
+ if (cleanupError !== null) {
1929
+ cleanupBarrier.unsafe(cleanupError);
1930
+ if (state.started > state.verified) {
1931
+ throw valueFreeError(
1932
+ "dispatch-indeterminate",
1933
+ "portable provider plugin process cleanup failed after dispatch began",
1934
+ plannedDispatchIds.length,
1935
+ state.started,
1936
+ state.verified,
1937
+ );
1938
+ }
1939
+ throw asHostError(cleanupError, plannedDispatchIds.length, state);
1940
+ }
1941
+ cleanupBarrier.verified();
1942
+ if (executionError !== null) throw executionError;
1943
+ if (executionResult === null) {
1944
+ throw valueFreeError(
1945
+ "host-failed",
1946
+ "portable provider plugin host produced no result",
1947
+ plannedDispatchIds.length,
1948
+ state.started,
1949
+ state.verified,
1950
+ );
1951
+ }
1952
+ return executionResult;
1953
+ }
1954
+
1955
+ export async function runPortableProviderPluginHost(
1956
+ invocation: PortableProviderPluginHostInvocation,
1957
+ ): Promise<PortableProviderPluginHostResult> {
1958
+ const cleanupBarrier = registerPortableProviderPluginCleanupBarrier();
1959
+ try {
1960
+ return await runPortableProviderPluginHostCore(
1961
+ invocation,
1962
+ cleanupBarrier,
1963
+ );
1964
+ } catch (error) {
1965
+ if (!cleanupBarrier.settled) {
1966
+ cleanupBarrier.unsafe(new Error(
1967
+ "portable provider plugin host exited without cleanup proof",
1968
+ ));
1969
+ }
1970
+ throw error;
1971
+ }
1972
+ }