@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,2057 @@
1
+ import { acquireCookieRecords, type CookieSelection } from "@hraness/kb/clip/acquire";
2
+ import { filterCookies, renderCookieHeader } from "@hraness/kb/clip/cookies";
3
+ import { createHash, randomUUID } from "node:crypto";
4
+ import {
5
+ chmodSync,
6
+ closeSync,
7
+ constants,
8
+ fstatSync,
9
+ fsyncSync,
10
+ mkdtempSync,
11
+ openSync,
12
+ readSync,
13
+ rmSync,
14
+ unlinkSync,
15
+ writeSync,
16
+ } from "node:fs";
17
+ import { tmpdir } from "node:os";
18
+ import { join } from "node:path";
19
+
20
+ import type { GhostgetAuth } from "./auth";
21
+ import type { BrowserFileResolver } from "./browser";
22
+ import {
23
+ type FileInputValue,
24
+ type GhostgetManifest,
25
+ type OperationInput,
26
+ canonicalJson,
27
+ } from "./model";
28
+ import { OperationDeadline } from "./operation-deadline";
29
+ import {
30
+ createPinnedHttpsFetchScope,
31
+ type PinnedHttpsFetch,
32
+ type PinnedHttpsFetchScope,
33
+ } from "./pinned-https";
34
+ import {
35
+ type ProviderPluginBindingDefinitionV1,
36
+ type ProviderPluginOperationDefinitionV1,
37
+ type ProviderApiPluginOperationDefinitionV1,
38
+ type WebSessionPluginOperationDefinitionV1,
39
+ type ProviderPluginSubjectDefinitionV1,
40
+ type ProviderPluginSubjectProbeOptionsV1,
41
+ } from "./provider-plugin";
42
+ import {
43
+ runPortableProviderPluginHost,
44
+ type PortableProviderPluginCapabilityHost,
45
+ type PortableProviderPluginHostResult,
46
+ } from "./provider-plugin-host";
47
+ import {
48
+ registerPortableProviderPluginCleanupBarrier,
49
+ settlePortableProviderPluginCleanup,
50
+ trackPortableProviderPluginHostCompletion,
51
+ type PortableProviderPluginCleanupBarrier,
52
+ } from "./provider-plugin-cleanup-barrier";
53
+ import {
54
+ acquirePortableProviderPluginInvocationLease,
55
+ createPortableProviderPluginInvocationLeaseContainmentController,
56
+ releasePortableProviderPluginInvocationLease,
57
+ } from "./provider-plugin-invocation-lease";
58
+ import type {
59
+ PortableProviderPluginBindingV1,
60
+ PortableProviderPluginOperationV1,
61
+ VerifiedPortableProviderPluginPackage,
62
+ } from "./provider-plugin-package";
63
+ import {
64
+ createPortableOperationIdentityV1,
65
+ } from "./provider-plugin-portable-identity";
66
+ import type {
67
+ PortablePluginCapabilityRequest,
68
+ PortablePluginCapabilityResult,
69
+ PortablePluginHttpBody,
70
+ PortablePluginInvocationFile,
71
+ PortablePluginJsonObject,
72
+ PortablePluginJsonValue,
73
+ PortablePluginVersionedStateResult,
74
+ } from "./provider-plugin-protocol";
75
+ import {
76
+ normalizePortablePluginJsonObject,
77
+ normalizePortablePluginJsonValue,
78
+ } from "./provider-plugin-protocol";
79
+ import {
80
+ loadOAuthToken,
81
+ } from "./provider-http";
82
+ import { summarizePlanFile } from "./plan-assets";
83
+ import {
84
+ createPrivateJsonIfAbsent,
85
+ ghostgetStateHome,
86
+ readPrivateStateFileIfPresent,
87
+ removePrivateStateFile,
88
+ removePrivateStateFileIfUnchanged,
89
+ writePrivateJson,
90
+ writePrivateJsonIfUnchanged,
91
+ } from "./storage";
92
+ import type {
93
+ ProviderActionContext,
94
+ } from "./provider";
95
+ import type {
96
+ WebSessionDispatchEvent,
97
+ WebSessionExecution,
98
+ WebSessionOperationDeadline,
99
+ WebSessionOperationExecutor,
100
+ } from "./web-session-execution";
101
+
102
+ type Environment = Readonly<Record<string, string | undefined>>;
103
+
104
+ const PORTABLE_HOST_VERSION = "io-portable-host-v1";
105
+ const WEB_SESSION_OPERATION_LABEL = "authenticated web operation deadline";
106
+ const MAX_PORTABLE_STATE_BYTES = 512 * 1024;
107
+ const MAX_PORTABLE_RESPONSE_HEADERS = 64;
108
+ const RESPONSE_HEADER_ALLOWLIST = new Set([
109
+ "cache-control",
110
+ "content-language",
111
+ "content-length",
112
+ "content-type",
113
+ "etag",
114
+ "expires",
115
+ "last-modified",
116
+ "location",
117
+ "retry-after",
118
+ "vary",
119
+ "x-ratelimit-limit",
120
+ "x-ratelimit-remaining",
121
+ "x-ratelimit-reset",
122
+ ]);
123
+ const FORBIDDEN_REQUEST_HEADERS = new Set([
124
+ "authorization",
125
+ "connection",
126
+ "content-length",
127
+ "cookie",
128
+ "host",
129
+ "proxy-authorization",
130
+ "proxy-connection",
131
+ "set-cookie",
132
+ "te",
133
+ "trailer",
134
+ "transfer-encoding",
135
+ "upgrade",
136
+ "x-api-key",
137
+ ]);
138
+
139
+ export type PortableProviderRuntimeDependencies = {
140
+ readonly runHost: typeof runPortableProviderPluginHost;
141
+ readonly createFetchScope: typeof createPinnedHttpsFetchScope;
142
+ readonly acquireCookies: typeof acquireCookieRecords;
143
+ readonly loadToken: typeof loadOAuthToken;
144
+ readonly closeBoundFile: (descriptor: number) => void;
145
+ /** Internal deterministic seam for pre-transfer descriptor cleanup. */
146
+ readonly closePlanFileBindingDescriptor: (descriptor: number) => void;
147
+ /** Internal deterministic seam for the private snapshot staging directory. */
148
+ readonly removePlanFileBindingDirectory: (
149
+ path: string,
150
+ force: boolean,
151
+ ) => void;
152
+ };
153
+
154
+ const defaultDependencies: PortableProviderRuntimeDependencies = {
155
+ runHost: runPortableProviderPluginHost,
156
+ createFetchScope: createPinnedHttpsFetchScope,
157
+ acquireCookies: acquireCookieRecords,
158
+ loadToken: loadOAuthToken,
159
+ closeBoundFile: closeSync,
160
+ closePlanFileBindingDescriptor: closeSync,
161
+ removePlanFileBindingDirectory: (path, force) =>
162
+ rmSync(path, { recursive: true, force }),
163
+ };
164
+
165
+ const resolvedPortableProviderRuntimeDependencies = new WeakSet<object>();
166
+ const portableRuntimeDependencyKeys = Object.freeze([
167
+ "runHost",
168
+ "createFetchScope",
169
+ "acquireCookies",
170
+ "loadToken",
171
+ "closeBoundFile",
172
+ "closePlanFileBindingDescriptor",
173
+ "removePlanFileBindingDirectory",
174
+ ] as const);
175
+
176
+ export function resolvePortableProviderRuntimeDependencies(
177
+ overrides: Partial<PortableProviderRuntimeDependencies>,
178
+ ): PortableProviderRuntimeDependencies {
179
+ if (
180
+ typeof overrides !== "object"
181
+ || overrides === null
182
+ || Array.isArray(overrides)
183
+ ) {
184
+ throw new Error(
185
+ "portable provider runtime dependency overrides must be a plain data object",
186
+ );
187
+ }
188
+ const actualKeys = Reflect.ownKeys(overrides);
189
+ const allowedKeys = new Set<PropertyKey>(portableRuntimeDependencyKeys);
190
+ if (actualKeys.some((key) => !allowedKeys.has(key))) {
191
+ throw new Error(
192
+ "portable provider runtime dependency overrides contain unsupported keys",
193
+ );
194
+ }
195
+ if (
196
+ actualKeys.length > 0
197
+ && process.env.NODE_ENV !== "test"
198
+ ) {
199
+ throw new Error(
200
+ "portable provider runtime dependency overrides are available only in tests",
201
+ );
202
+ }
203
+ const snapshot: Partial<PortableProviderRuntimeDependencies> = {};
204
+ for (const key of actualKeys) {
205
+ if (typeof key !== "string") {
206
+ throw new Error(
207
+ "portable provider runtime dependency overrides contain unsupported keys",
208
+ );
209
+ }
210
+ const descriptor = Object.getOwnPropertyDescriptor(overrides, key);
211
+ if (
212
+ descriptor === undefined
213
+ || !descriptor.enumerable
214
+ || !("value" in descriptor)
215
+ || typeof descriptor.value !== "function"
216
+ ) {
217
+ throw new Error(
218
+ `portable provider runtime dependency override ${key} must be an enumerable function data property`,
219
+ );
220
+ }
221
+ Object.defineProperty(snapshot, key, {
222
+ value: descriptor.value,
223
+ enumerable: true,
224
+ configurable: false,
225
+ writable: false,
226
+ });
227
+ }
228
+ const dependencies = Object.freeze({
229
+ ...defaultDependencies,
230
+ ...snapshot,
231
+ });
232
+ resolvedPortableProviderRuntimeDependencies.add(dependencies);
233
+ return dependencies;
234
+ }
235
+
236
+ export function isResolvedPortableProviderRuntimeDependencies(
237
+ value: unknown,
238
+ ): value is PortableProviderRuntimeDependencies {
239
+ return (
240
+ typeof value === "object"
241
+ && value !== null
242
+ && resolvedPortableProviderRuntimeDependencies.has(value)
243
+ );
244
+ }
245
+
246
+ export type KernelPortableProviderPluginBindingProjection = {
247
+ readonly adapterId: string;
248
+ readonly manifest: GhostgetManifest;
249
+ readonly portableBinding: PortableProviderPluginBindingV1;
250
+ readonly binding: ProviderPluginBindingDefinitionV1;
251
+ };
252
+
253
+ type BoundInvocationFile = {
254
+ readonly descriptor: number;
255
+ readonly dispose: () => void;
256
+ readonly protocol: PortablePluginInvocationFile;
257
+ };
258
+
259
+ class UnverifiedPlanFileBindingCleanupError extends Error {
260
+ constructor(resource: string, cause: unknown) {
261
+ super(
262
+ `portable plugin plan-file ${resource} cleanup could not be verified`,
263
+ { cause },
264
+ );
265
+ this.name = "UnverifiedPlanFileBindingCleanupError";
266
+ }
267
+ }
268
+
269
+ function disposePortableRuntimeResources(
270
+ cleanupBarrier: PortableProviderPluginCleanupBarrier,
271
+ fetchScope: PinnedHttpsFetchScope | undefined,
272
+ files: readonly BoundInvocationFile[],
273
+ ): void {
274
+ let cleanupError: unknown = null;
275
+ try {
276
+ fetchScope?.close();
277
+ } catch (error) {
278
+ cleanupError = error;
279
+ }
280
+ for (const file of files) {
281
+ try {
282
+ file.dispose();
283
+ } catch (error) {
284
+ if (cleanupError === null) cleanupError = error;
285
+ }
286
+ }
287
+ if (cleanupError === null) {
288
+ cleanupBarrier.verified();
289
+ return;
290
+ }
291
+ cleanupBarrier.unsafe(cleanupError);
292
+ throw new Error(
293
+ "portable provider plugin runtime resource cleanup failed",
294
+ { cause: cleanupError },
295
+ );
296
+ }
297
+
298
+ type DispatchBoundary = {
299
+ readonly verify: () => Promise<void>;
300
+ };
301
+
302
+ type PortableCapabilityActivity = {
303
+ readonly begin: () => () => void;
304
+ readonly whenIdle: () => Promise<void>;
305
+ };
306
+
307
+ const PORTABLE_CAPABILITY_ABORT_JOIN_MS = 500;
308
+
309
+ async function settlesWithin<T>(
310
+ operation: Promise<T>,
311
+ maximumMs: number,
312
+ ): Promise<PromiseSettledResult<T> | null> {
313
+ let timer: ReturnType<typeof setTimeout> | undefined;
314
+ try {
315
+ return await Promise.race([
316
+ operation.then(
317
+ (value): PromiseFulfilledResult<T> => ({
318
+ status: "fulfilled",
319
+ value,
320
+ }),
321
+ (reason: unknown): PromiseRejectedResult => ({
322
+ status: "rejected",
323
+ reason,
324
+ }),
325
+ ),
326
+ new Promise<null>((resolve) => {
327
+ timer = setTimeout(() => resolve(null), maximumMs);
328
+ }),
329
+ ]);
330
+ } finally {
331
+ if (timer !== undefined) clearTimeout(timer);
332
+ }
333
+ }
334
+
335
+ function createPortableCapabilityActivity(): PortableCapabilityActivity {
336
+ let active = 0;
337
+ const waiters = new Set<() => void>();
338
+ return Object.freeze({
339
+ begin: () => {
340
+ active += 1;
341
+ let ended = false;
342
+ return () => {
343
+ if (ended) return;
344
+ ended = true;
345
+ active -= 1;
346
+ if (active !== 0) return;
347
+ for (const resolve of waiters) resolve();
348
+ waiters.clear();
349
+ };
350
+ },
351
+ whenIdle: () => active === 0
352
+ ? Promise.resolve()
353
+ : new Promise<void>((resolve) => {
354
+ waiters.add(resolve);
355
+ }),
356
+ });
357
+ }
358
+
359
+ type CapabilityMaterial =
360
+ | { readonly kind: "oauth-access-token"; readonly value: string }
361
+ | { readonly kind: "cookie-jar"; readonly auth: GhostgetAuth };
362
+
363
+ function sha256(value: string): string {
364
+ return createHash("sha256").update(value).digest("hex");
365
+ }
366
+
367
+ function portableSubject(
368
+ value: PortableProviderPluginBindingV1["subject"],
369
+ ): ProviderPluginSubjectDefinitionV1 {
370
+ const matches = (candidate: string): boolean => {
371
+ if (
372
+ typeof candidate !== "string"
373
+ || candidate.length < 1
374
+ || Buffer.byteLength(candidate, "utf8") > 256
375
+ || /[\0-\x20\x7f]/u.test(candidate)
376
+ ) return false;
377
+ if (value.kind === "opaque-token") {
378
+ return /^[A-Za-z0-9][A-Za-z0-9._:@/+~-]{0,255}$/u.test(candidate);
379
+ }
380
+ if (value.kind === "decimal") return /^(?:0|[1-9][0-9]{0,127})$/u.test(candidate);
381
+ if (value.kind === "did") {
382
+ return /^did:[a-z0-9]+:[A-Za-z0-9._:%-]{1,220}$/u.test(candidate);
383
+ }
384
+ if (value.kind === "uuid") {
385
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test(candidate);
386
+ }
387
+ return /^\+[1-9][0-9]{6,14}$/u.test(candidate);
388
+ };
389
+ return Object.freeze({ format: value.format, matches });
390
+ }
391
+
392
+ function plannedDispatches(
393
+ operation: PortableProviderPluginOperationV1,
394
+ ): ProviderPluginOperationDefinitionV1["planDispatches"] {
395
+ if (operation.dispatch === "none") return () => Object.freeze([]);
396
+ const schedule = Object.freeze([Object.freeze({
397
+ id: operation.name,
398
+ description: operation.sideEffect,
399
+ })]);
400
+ return () => schedule;
401
+ }
402
+
403
+ function portableOperation(
404
+ operation: PortableProviderPluginOperationV1,
405
+ ): ProviderPluginOperationDefinitionV1 {
406
+ const common = {
407
+ name: operation.name,
408
+ contractVersion: operation.contractVersion,
409
+ risk: operation.risk,
410
+ input: operation.input,
411
+ sideEffect: operation.sideEffect,
412
+ idempotency: operation.idempotency,
413
+ dedupeWindowMs: operation.dedupeWindowMs,
414
+ state: operation.state,
415
+ dispatch: operation.dispatch,
416
+ implementation: operation.implementation,
417
+ planDispatches: plannedDispatches(operation),
418
+ validateInput: () => Object.freeze([]),
419
+ ...(
420
+ operation.name === "messaging.list"
421
+ || operation.name === "messaging.read"
422
+ ? {
423
+ omni: Object.freeze({
424
+ state: "unsupported" as const,
425
+ reason: "portable provider plugins do not yet attest a versioned output materializer",
426
+ }),
427
+ }
428
+ : {}
429
+ ),
430
+ } as const;
431
+ return (
432
+ operation.requiredScopeSets !== undefined
433
+ && operation.coverage !== undefined
434
+ )
435
+ ? {
436
+ ...common,
437
+ requiredScopeSets: operation.requiredScopeSets,
438
+ coverage: operation.coverage,
439
+ }
440
+ : common;
441
+ }
442
+
443
+ function portableProviderOperation(
444
+ operation: Extract<
445
+ PortableProviderPluginOperationV1,
446
+ { readonly requiredScopeSets: readonly (readonly string[])[] }
447
+ >,
448
+ ): ProviderApiPluginOperationDefinitionV1 {
449
+ return portableOperation(operation) as ProviderApiPluginOperationDefinitionV1;
450
+ }
451
+
452
+ function portableSessionOperation(
453
+ operation: Exclude<
454
+ PortableProviderPluginOperationV1,
455
+ { readonly requiredScopeSets: readonly (readonly string[])[] }
456
+ >,
457
+ ): WebSessionPluginOperationDefinitionV1 {
458
+ return portableOperation(operation) as WebSessionPluginOperationDefinitionV1;
459
+ }
460
+
461
+ function freezePortableProjectionValue<T>(value: T): T {
462
+ if (typeof value !== "object" || value === null || Object.isFrozen(value)) {
463
+ return value;
464
+ }
465
+ for (const entry of Object.values(value as Record<string, unknown>)) {
466
+ freezePortableProjectionValue(entry);
467
+ }
468
+ return Object.freeze(value);
469
+ }
470
+
471
+ function operationManifest(
472
+ binding: PortableProviderPluginBindingV1,
473
+ operation: PortableProviderPluginOperationV1,
474
+ ): GhostgetManifest["operations"][string] {
475
+ const common = {
476
+ description: operation.implementation,
477
+ risk: operation.risk,
478
+ sideEffect: operation.sideEffect,
479
+ idempotency: operation.idempotency,
480
+ dedupeWindowMs: operation.dedupeWindowMs,
481
+ input: operation.input,
482
+ };
483
+ return binding.transport === "provider-api"
484
+ ? {
485
+ ...common,
486
+ provider: {
487
+ provider: binding.surfaceId,
488
+ action: operation.name,
489
+ contractVersion: operation.contractVersion,
490
+ timeoutMs: operation.timeoutMs,
491
+ maxOutputBytes: operation.maxOutputBytes,
492
+ },
493
+ }
494
+ : {
495
+ ...common,
496
+ webSession: {
497
+ site: binding.surfaceId,
498
+ action: operation.name,
499
+ contractVersion: operation.contractVersion,
500
+ timeoutMs: operation.timeoutMs,
501
+ maxOutputBytes: operation.maxOutputBytes,
502
+ },
503
+ };
504
+ }
505
+
506
+ function virtualManifest(
507
+ packageValue: VerifiedPortableProviderPluginPackage,
508
+ binding: PortableProviderPluginBindingV1,
509
+ ): GhostgetManifest {
510
+ const operations = Object.fromEntries(
511
+ binding.operations.map((operation) => [
512
+ operation.name,
513
+ operationManifest(binding, operation),
514
+ ]),
515
+ );
516
+ return freezePortableProjectionValue({
517
+ schemaVersion: binding.transport === "provider-api" ? 3 : 4,
518
+ id: binding.adapterId,
519
+ version: packageValue.manifest.version,
520
+ displayName: packageValue.manifest.displayName,
521
+ surfaceId: binding.surfaceId,
522
+ origins: Object.freeze([binding.origin]),
523
+ browserDomains: Object.freeze([new URL(binding.origin).hostname]),
524
+ operations,
525
+ });
526
+ }
527
+
528
+ function cookieSelection(auth: GhostgetAuth, timeoutMs: number): CookieSelection {
529
+ if (auth.kind === "cookie-source") {
530
+ return {
531
+ cookieSources: [auth.source],
532
+ cookiesFile: undefined,
533
+ cookieProfile: auth.profile,
534
+ timeoutMs,
535
+ requireExplicitCookieScope: true,
536
+ };
537
+ }
538
+ if (auth.kind === "cookies-file") {
539
+ return {
540
+ cookieSources: [],
541
+ cookiesFile: auth.path,
542
+ cookieProfile: undefined,
543
+ timeoutMs,
544
+ requireExplicitCookieScope: true,
545
+ };
546
+ }
547
+ if (auth.kind === "browser-profile" && auth.cookieSource !== undefined) {
548
+ return {
549
+ cookieSources: [auth.cookieSource],
550
+ cookiesFile: undefined,
551
+ cookieProfile: auth.cookieProfile,
552
+ timeoutMs,
553
+ requireExplicitCookieScope: true,
554
+ };
555
+ }
556
+ throw new Error("portable cookie material requires an explicit cookie source");
557
+ }
558
+
559
+ function bodyBytes(body: PortablePluginHttpBody): ArrayBuffer | undefined {
560
+ if (body.kind === "none") return undefined;
561
+ const source = body.kind === "utf8"
562
+ ? new TextEncoder().encode(body.text)
563
+ : Buffer.from(body.data, "base64");
564
+ const owned = new Uint8Array(source.byteLength);
565
+ owned.set(source);
566
+ return owned.buffer;
567
+ }
568
+
569
+ async function boundedResponseBytes(
570
+ response: Response,
571
+ maximumBytes: number,
572
+ cleanupBarrier: PortableProviderPluginCleanupBarrier,
573
+ operationDeadline?: WebSessionOperationDeadline,
574
+ ): Promise<Uint8Array> {
575
+ if (response.body === null) {
576
+ operationDeadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
577
+ return new Uint8Array();
578
+ }
579
+ const reader = response.body.getReader();
580
+ const chunks: Uint8Array[] = [];
581
+ let total = 0;
582
+ let pendingRead: ReturnType<typeof reader.read> | null = null;
583
+ let failure: unknown;
584
+ let failed = false;
585
+ try {
586
+ operationDeadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
587
+ for (;;) {
588
+ pendingRead = reader.read();
589
+ const next = operationDeadline === undefined
590
+ ? await pendingRead
591
+ : await operationDeadline.run(
592
+ () => pendingRead as ReturnType<typeof reader.read>,
593
+ WEB_SESSION_OPERATION_LABEL,
594
+ );
595
+ pendingRead = null;
596
+ if (next.done) break;
597
+ const chunk: unknown = next.value;
598
+ if (!(chunk instanceof Uint8Array)) {
599
+ throw new Error("portable plugin HTTP response returned non-byte data");
600
+ }
601
+ total += chunk.byteLength;
602
+ if (total > maximumBytes) {
603
+ throw new Error("portable plugin HTTP response exceeded its declared bound");
604
+ }
605
+ chunks.push(chunk);
606
+ }
607
+ } catch (error) {
608
+ failed = true;
609
+ failure = error;
610
+ const cancellation = Promise.resolve().then(() =>
611
+ reader.cancel("portable plugin response processing stopped")
612
+ );
613
+ const quiescence = Promise.allSettled([
614
+ ...(pendingRead === null ? [] : [pendingRead]),
615
+ cancellation,
616
+ ]).then((results) => {
617
+ const cancellationResult = results.at(-1);
618
+ if (cancellationResult?.status === "rejected") {
619
+ throw cancellationResult.reason;
620
+ }
621
+ });
622
+ const settled = await settlesWithin(
623
+ quiescence,
624
+ PORTABLE_CAPABILITY_ABORT_JOIN_MS,
625
+ );
626
+ if (settled === null || settled.status === "rejected") {
627
+ const cleanupError = new Error(
628
+ "portable plugin HTTP response cleanup did not settle after cancellation",
629
+ settled?.status === "rejected"
630
+ ? { cause: settled.reason }
631
+ : undefined,
632
+ );
633
+ cleanupBarrier.unsafe(cleanupError);
634
+ failure = new AggregateError(
635
+ [error, cleanupError],
636
+ "portable plugin HTTP response processing and cleanup failed",
637
+ );
638
+ }
639
+ } finally {
640
+ try {
641
+ reader.releaseLock();
642
+ } catch (error) {
643
+ const cleanupError = new Error(
644
+ "portable plugin HTTP response reader lock could not be released",
645
+ { cause: error },
646
+ );
647
+ cleanupBarrier.unsafe(cleanupError);
648
+ failure = failed
649
+ ? new AggregateError(
650
+ [failure, cleanupError],
651
+ "portable plugin HTTP response processing and cleanup failed",
652
+ )
653
+ : cleanupError;
654
+ failed = true;
655
+ }
656
+ }
657
+ if (failed) throw failure;
658
+ operationDeadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
659
+ const output = new Uint8Array(total);
660
+ let offset = 0;
661
+ for (const chunk of chunks) {
662
+ output.set(chunk, offset);
663
+ offset += chunk.byteLength;
664
+ }
665
+ return output;
666
+ }
667
+
668
+ function responseHeaders(response: Response): readonly {
669
+ readonly name: string;
670
+ readonly value: string;
671
+ }[] {
672
+ const values: { readonly name: string; readonly value: string }[] = [];
673
+ response.headers.forEach((value, name) => {
674
+ const normalized = name.toLowerCase();
675
+ if (
676
+ values.length < MAX_PORTABLE_RESPONSE_HEADERS
677
+ && RESPONSE_HEADER_ALLOWLIST.has(normalized)
678
+ && !/[\0\r\n]/u.test(value)
679
+ && Buffer.byteLength(value, "utf8") <= 8_192
680
+ ) {
681
+ values.push(Object.freeze({ name: normalized, value }));
682
+ }
683
+ });
684
+ return Object.freeze(values);
685
+ }
686
+
687
+ function responseBody(bytes: Uint8Array): {
688
+ readonly kind: "utf8";
689
+ readonly text: string;
690
+ } | {
691
+ readonly kind: "base64";
692
+ readonly data: string;
693
+ } {
694
+ try {
695
+ return Object.freeze({
696
+ kind: "utf8" as const,
697
+ text: new TextDecoder("utf-8", { fatal: true }).decode(bytes),
698
+ });
699
+ } catch {
700
+ return Object.freeze({
701
+ kind: "base64" as const,
702
+ data: Buffer.from(bytes).toString("base64"),
703
+ });
704
+ }
705
+ }
706
+
707
+ function statePath(
708
+ packageValue: VerifiedPortableProviderPluginPackage,
709
+ binding: PortableProviderPluginBindingV1,
710
+ auth: GhostgetAuth,
711
+ key: string,
712
+ environment: Environment,
713
+ ): string {
714
+ return join(
715
+ ghostgetStateHome(environment),
716
+ "provider-plugin-state",
717
+ packageValue.manifest.id,
718
+ packageValue.bundleSha256,
719
+ binding.adapterId,
720
+ sha256(canonicalJson(auth)),
721
+ `${sha256(key)}.json`,
722
+ );
723
+ }
724
+
725
+ function parseStoredState(
726
+ content: string,
727
+ key: string,
728
+ includeVersion: boolean,
729
+ ): PortablePluginCapabilityResult {
730
+ let value: unknown;
731
+ try {
732
+ value = JSON.parse(content) as unknown;
733
+ } catch {
734
+ throw new Error("portable plugin namespaced state is malformed");
735
+ }
736
+ if (
737
+ typeof value !== "object"
738
+ || value === null
739
+ || Array.isArray(value)
740
+ ) {
741
+ throw new Error("portable plugin namespaced state is malformed");
742
+ }
743
+ const record = value as Record<string, unknown>;
744
+ const legacy = record.schemaVersion === 1
745
+ && Object.keys(record).sort().join(",") === "key,schemaVersion,value";
746
+ const current = record.schemaVersion === 2
747
+ && Object.keys(record).sort().join(",")
748
+ === "key,revision,schemaVersion,value"
749
+ && typeof record.revision === "string"
750
+ && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu
751
+ .test(record.revision);
752
+ if ((!legacy && !current) || record.key !== key) {
753
+ throw new Error("portable plugin namespaced state identity is malformed");
754
+ }
755
+ const result = {
756
+ kind: "state.read",
757
+ found: true,
758
+ value: normalizePortablePluginJsonValue(
759
+ record.value,
760
+ "portable plugin namespaced state value",
761
+ ),
762
+ } as const;
763
+ if (!includeVersion) return result;
764
+ const versioned: PortablePluginVersionedStateResult = {
765
+ ...result,
766
+ version: sha256(content),
767
+ };
768
+ return versioned;
769
+ }
770
+
771
+ function portableStateRecord(
772
+ key: string,
773
+ value: PortablePluginJsonValue,
774
+ ): Readonly<{
775
+ readonly schemaVersion: 2;
776
+ readonly key: string;
777
+ readonly revision: string;
778
+ readonly value: PortablePluginJsonValue;
779
+ }> {
780
+ return Object.freeze({
781
+ schemaVersion: 2 as const,
782
+ key,
783
+ revision: randomUUID(),
784
+ value: normalizePortablePluginJsonValue(
785
+ value,
786
+ "portable plugin namespaced state value",
787
+ ),
788
+ });
789
+ }
790
+
791
+ function requestHeaders(
792
+ request: Extract<PortablePluginCapabilityRequest, { readonly kind: "http.request" }>,
793
+ materials: ReadonlyMap<string, CapabilityMaterial>,
794
+ ): Headers {
795
+ const headers = new Headers();
796
+ for (const header of request.headers) {
797
+ const name = header.name.toLowerCase();
798
+ if (FORBIDDEN_REQUEST_HEADERS.has(name)) {
799
+ throw new Error("portable plugin requested a forbidden HTTP header");
800
+ }
801
+ headers.append(name, header.value);
802
+ }
803
+ for (const credential of request.credentials) {
804
+ const material = materials.get(credential.handle);
805
+ if (material === undefined) {
806
+ throw new Error("portable plugin requested an unknown material handle");
807
+ }
808
+ if (
809
+ material.kind === "oauth-access-token"
810
+ && credential.sink.kind === "header"
811
+ && credential.sink.name.toLowerCase() === "authorization"
812
+ ) {
813
+ if (headers.has("authorization")) {
814
+ throw new Error("portable plugin repeated the authorization sink");
815
+ }
816
+ headers.set("authorization", `Bearer ${material.value}`);
817
+ continue;
818
+ }
819
+ if (
820
+ material.kind === "cookie-jar"
821
+ && credential.sink.kind === "cookie-jar"
822
+ ) {
823
+ continue;
824
+ }
825
+ throw new Error("portable plugin requested an incompatible credential sink");
826
+ }
827
+ if (request.body.kind !== "none") {
828
+ if (headers.has("content-type")) {
829
+ throw new Error("portable plugin body media type owns content-type");
830
+ }
831
+ headers.set("content-type", request.body.mediaType);
832
+ }
833
+ return headers;
834
+ }
835
+
836
+ async function injectCookieMaterials(
837
+ request: Extract<PortablePluginCapabilityRequest, { readonly kind: "http.request" }>,
838
+ materials: ReadonlyMap<string, CapabilityMaterial>,
839
+ headers: Headers,
840
+ dependencies: PortableProviderRuntimeDependencies,
841
+ signal: AbortSignal,
842
+ cleanupBarrier: PortableProviderPluginCleanupBarrier,
843
+ operationDeadline?: WebSessionOperationDeadline,
844
+ ): Promise<void> {
845
+ for (const credential of request.credentials) {
846
+ const material = materials.get(credential.handle);
847
+ if (
848
+ material?.kind !== "cookie-jar"
849
+ || credential.sink.kind !== "cookie-jar"
850
+ ) continue;
851
+ if (headers.has("cookie")) {
852
+ throw new Error("portable plugin repeated the cookie sink");
853
+ }
854
+ operationDeadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
855
+ if (signal.aborted) throw new Error("portable plugin invocation was cancelled");
856
+ const target = new URL(request.url);
857
+ const timeoutMs = operationDeadline === undefined
858
+ ? request.timeoutMs
859
+ : Math.min(request.timeoutMs, operationDeadline.remainingTimeMs());
860
+ if (timeoutMs < 1) {
861
+ throw new Error("portable plugin operation deadline was exhausted");
862
+ }
863
+ const acquire = () => dependencies.acquireCookies(
864
+ cookieSelection(material.auth, timeoutMs),
865
+ target,
866
+ );
867
+ const acquisition = acquire();
868
+ let acquired: Awaited<ReturnType<typeof acquire>>;
869
+ try {
870
+ acquired = operationDeadline === undefined
871
+ ? await acquisition
872
+ : await operationDeadline.run(
873
+ () => acquisition,
874
+ WEB_SESSION_OPERATION_LABEL,
875
+ );
876
+ } catch (error) {
877
+ if (operationDeadline?.signal.aborted === true) {
878
+ await settlesWithin(
879
+ acquisition,
880
+ PORTABLE_CAPABILITY_ABORT_JOIN_MS,
881
+ );
882
+ // The pinned sweet-cookie dependency resolves its timeout before its
883
+ // spawned keyring helper emits close. Its Promise therefore cannot
884
+ // prove process quiescence after a deadline wins the race.
885
+ cleanupBarrier.unsafe(new Error(
886
+ "portable plugin cookie acquisition outlived its request deadline",
887
+ ));
888
+ }
889
+ throw error;
890
+ }
891
+ const filtered = filterCookies(acquired.cookies, target);
892
+ if (
893
+ filtered.rejected !== 0
894
+ || filtered.cookies.length !== acquired.cookies.length
895
+ ) {
896
+ throw new Error("portable plugin cookie source escaped its exact request scope");
897
+ }
898
+ headers.set("cookie", renderCookieHeader(filtered.cookies));
899
+ }
900
+ }
901
+
902
+ function capabilityHost(options: {
903
+ readonly package: VerifiedPortableProviderPluginPackage;
904
+ readonly binding: PortableProviderPluginBindingV1;
905
+ readonly auth: GhostgetAuth;
906
+ readonly environment: Environment;
907
+ readonly files: ReadonlyMap<string, BoundInvocationFile>;
908
+ readonly beginDispatch: (dispatchId: string) => Promise<DispatchBoundary>;
909
+ readonly dependencies: PortableProviderRuntimeDependencies;
910
+ readonly fetch: PinnedHttpsFetch;
911
+ readonly activity: PortableCapabilityActivity;
912
+ readonly cleanupBarrier: PortableProviderPluginCleanupBarrier;
913
+ readonly operationDeadline?: WebSessionOperationDeadline;
914
+ }): PortableProviderPluginCapabilityHost {
915
+ const materials = new Map<string, CapabilityMaterial>();
916
+ const dispatches = new Map<string, DispatchBoundary>();
917
+ return Object.freeze({
918
+ handle: async (
919
+ request,
920
+ context,
921
+ ): Promise<PortablePluginCapabilityResult> => {
922
+ const endActivity = options.activity.begin();
923
+ try {
924
+ options.operationDeadline?.throwIfUnavailable(
925
+ WEB_SESSION_OPERATION_LABEL,
926
+ );
927
+ if (request.kind === "dispatch.begin") {
928
+ const boundary = await options.beginDispatch(request.dispatchId);
929
+ const dispatchHandle = `dispatch-${randomUUID()}`;
930
+ dispatches.set(dispatchHandle, boundary);
931
+ return { kind: "dispatch.begin", dispatchHandle };
932
+ }
933
+ if (request.kind === "dispatch.verify") {
934
+ const boundary = dispatches.get(request.dispatchHandle);
935
+ if (boundary === undefined) {
936
+ throw new Error("portable plugin requested an unknown dispatch boundary");
937
+ }
938
+ await boundary.verify();
939
+ dispatches.delete(request.dispatchHandle);
940
+ return { kind: "dispatch.verify", verified: true };
941
+ }
942
+ if (request.kind === "session.acquire") {
943
+ let material: CapabilityMaterial;
944
+ if (
945
+ request.name === "oauth-access-token"
946
+ && options.binding.transport === "provider-api"
947
+ && options.auth.kind === "oauth-token-file"
948
+ ) {
949
+ const token = options.dependencies.loadToken(
950
+ options.auth,
951
+ new Date(),
952
+ 30_000,
953
+ );
954
+ material = { kind: "oauth-access-token", value: token.accessToken };
955
+ } else if (
956
+ request.name === "cookie-jar"
957
+ && options.binding.transport === "web-session-api"
958
+ && (
959
+ options.auth.kind === "cookie-source"
960
+ || options.auth.kind === "cookies-file"
961
+ || options.auth.kind === "browser-profile"
962
+ )
963
+ ) {
964
+ material = { kind: "cookie-jar", auth: options.auth };
965
+ } else {
966
+ throw new Error("portable plugin requested unsupported session material");
967
+ }
968
+ const materialHandle = `material-${randomUUID()}`;
969
+ materials.set(materialHandle, material);
970
+ return { kind: "session.acquire", materialHandle };
971
+ }
972
+ if (request.kind === "http.request") {
973
+ if (new URL(request.url).origin !== options.binding.origin) {
974
+ throw new Error(
975
+ "portable plugin HTTP request escaped its binding's exact origin",
976
+ );
977
+ }
978
+ const headers = requestHeaders(request, materials);
979
+ const timeoutMs = Math.min(
980
+ request.timeoutMs,
981
+ options.operationDeadline?.remainingTimeMs() ?? request.timeoutMs,
982
+ );
983
+ if (timeoutMs < 1) {
984
+ throw new Error("portable plugin operation deadline was exhausted");
985
+ }
986
+ const requestDeadline = new OperationDeadline(timeoutMs, {
987
+ signal: options.operationDeadline?.signal ?? context.signal,
988
+ });
989
+ let response: Response | undefined;
990
+ try {
991
+ await injectCookieMaterials(
992
+ request,
993
+ materials,
994
+ headers,
995
+ options.dependencies,
996
+ requestDeadline.signal,
997
+ options.cleanupBarrier,
998
+ requestDeadline,
999
+ );
1000
+ const fetchOperation = Promise.resolve().then(() =>
1001
+ options.fetch(
1002
+ new URL(request.url),
1003
+ {
1004
+ method: request.method,
1005
+ headers,
1006
+ ...(request.body.kind === "none"
1007
+ ? {}
1008
+ : { body: bodyBytes(request.body) }),
1009
+ redirect: "error",
1010
+ signal: requestDeadline.signal,
1011
+ },
1012
+ timeoutMs,
1013
+ )
1014
+ );
1015
+ try {
1016
+ response = await requestDeadline.run(
1017
+ () => fetchOperation,
1018
+ WEB_SESSION_OPERATION_LABEL,
1019
+ );
1020
+ } catch (error) {
1021
+ if (requestDeadline.signal.aborted) {
1022
+ const quiescence = fetchOperation.then(
1023
+ async (lateResponse) => {
1024
+ await lateResponse.body?.cancel(
1025
+ "portable plugin request deadline expired",
1026
+ );
1027
+ },
1028
+ () => undefined,
1029
+ );
1030
+ const settled = await settlesWithin(
1031
+ quiescence,
1032
+ PORTABLE_CAPABILITY_ABORT_JOIN_MS,
1033
+ );
1034
+ if (settled === null || settled.status === "rejected") {
1035
+ options.cleanupBarrier.unsafe(new Error(
1036
+ "portable plugin HTTP request cleanup did not settle after cancellation",
1037
+ settled?.status === "rejected"
1038
+ ? { cause: settled.reason }
1039
+ : undefined,
1040
+ ));
1041
+ }
1042
+ }
1043
+ throw error;
1044
+ }
1045
+ const bytes = await boundedResponseBytes(
1046
+ response,
1047
+ request.maxOutputBytes,
1048
+ options.cleanupBarrier,
1049
+ requestDeadline,
1050
+ );
1051
+ return {
1052
+ kind: "http.request",
1053
+ status: response.status,
1054
+ headers: responseHeaders(response),
1055
+ body: responseBody(bytes),
1056
+ finalUrl: request.url,
1057
+ };
1058
+ } finally {
1059
+ const requestAborted = requestDeadline.signal.aborted;
1060
+ requestDeadline.dispose();
1061
+ if (
1062
+ requestAborted
1063
+ && response?.body !== null
1064
+ && response?.body !== undefined
1065
+ && !response.bodyUsed
1066
+ ) {
1067
+ const settled = await settlesWithin(
1068
+ response.body.cancel(
1069
+ "portable plugin request deadline expired",
1070
+ ),
1071
+ PORTABLE_CAPABILITY_ABORT_JOIN_MS,
1072
+ );
1073
+ if (settled === null || settled.status === "rejected") {
1074
+ options.cleanupBarrier.unsafe(new Error(
1075
+ "portable plugin HTTP response cleanup did not settle after cancellation",
1076
+ settled?.status === "rejected"
1077
+ ? { cause: settled.reason }
1078
+ : undefined,
1079
+ ));
1080
+ }
1081
+ }
1082
+ }
1083
+ }
1084
+ if (request.kind === "file.read") {
1085
+ const file = options.files.get(request.handle);
1086
+ if (file === undefined) {
1087
+ throw new Error("portable plugin requested an unknown file handle");
1088
+ }
1089
+ const buffer = Buffer.alloc(request.length);
1090
+ const read = readSync(
1091
+ file.descriptor,
1092
+ buffer,
1093
+ 0,
1094
+ request.length,
1095
+ request.offset,
1096
+ );
1097
+ return {
1098
+ kind: "file.read",
1099
+ data: buffer.subarray(0, read).toString("base64"),
1100
+ eof: request.offset + read >= file.protocol.bytes,
1101
+ };
1102
+ }
1103
+ if (request.kind === "state.read") {
1104
+ const content = readPrivateStateFileIfPresent(
1105
+ statePath(
1106
+ options.package,
1107
+ options.binding,
1108
+ options.auth,
1109
+ request.key,
1110
+ options.environment,
1111
+ ),
1112
+ MAX_PORTABLE_STATE_BYTES,
1113
+ "portable plugin namespaced state",
1114
+ options.environment,
1115
+ );
1116
+ if (content !== null) {
1117
+ return parseStoredState(
1118
+ content,
1119
+ request.key,
1120
+ request.includeVersion === true,
1121
+ );
1122
+ }
1123
+ if (request.includeVersion !== true) {
1124
+ return { kind: "state.read", found: false };
1125
+ }
1126
+ const missing: PortablePluginVersionedStateResult = {
1127
+ kind: "state.read",
1128
+ found: false,
1129
+ version: null,
1130
+ };
1131
+ return missing;
1132
+ }
1133
+ if (request.kind === "state.write") {
1134
+ const path = statePath(
1135
+ options.package,
1136
+ options.binding,
1137
+ options.auth,
1138
+ request.key,
1139
+ options.environment,
1140
+ );
1141
+ const state = portableStateRecord(request.key, request.value);
1142
+ const serialized = `${canonicalJson(state)}\n`;
1143
+ if (
1144
+ Buffer.byteLength(serialized, "utf8")
1145
+ > MAX_PORTABLE_STATE_BYTES
1146
+ ) {
1147
+ throw new Error(
1148
+ "portable plugin namespaced state exceeds its persisted byte bound",
1149
+ );
1150
+ }
1151
+ let stored: boolean;
1152
+ if (request.expectedVersion === undefined) {
1153
+ writePrivateJson(path, state, { privateParent: true });
1154
+ stored = true;
1155
+ } else if (request.expectedVersion === null) {
1156
+ stored = createPrivateJsonIfAbsent(path, state, {
1157
+ environment: options.environment,
1158
+ privateParent: true,
1159
+ }).created;
1160
+ } else {
1161
+ stored = writePrivateJsonIfUnchanged(path, state, {
1162
+ expectedCurrentContentSha256: request.expectedVersion,
1163
+ });
1164
+ }
1165
+ if (!stored) {
1166
+ throw new Error("portable plugin state version conflict");
1167
+ }
1168
+ const result = {
1169
+ kind: "state.write",
1170
+ stored: true,
1171
+ } as const;
1172
+ if (request.expectedVersion === undefined) return result;
1173
+ const versioned: PortablePluginVersionedStateResult = {
1174
+ ...result,
1175
+ version: sha256(serialized),
1176
+ };
1177
+ return versioned;
1178
+ }
1179
+ if (request.kind === "state.delete") {
1180
+ const path = statePath(
1181
+ options.package,
1182
+ options.binding,
1183
+ options.auth,
1184
+ request.key,
1185
+ options.environment,
1186
+ );
1187
+ if (request.expectedVersion === undefined) {
1188
+ return {
1189
+ kind: "state.delete",
1190
+ removed: removePrivateStateFile(
1191
+ path,
1192
+ options.environment,
1193
+ ),
1194
+ };
1195
+ }
1196
+ return {
1197
+ kind: "state.delete",
1198
+ removed: removePrivateStateFileIfUnchanged(
1199
+ path,
1200
+ { expectedCurrentContentSha256: request.expectedVersion },
1201
+ options.environment,
1202
+ ),
1203
+ };
1204
+ }
1205
+ return { kind: "log.write", accepted: true };
1206
+ } finally {
1207
+ endActivity();
1208
+ }
1209
+ },
1210
+ });
1211
+ }
1212
+
1213
+ function fileValues(
1214
+ input: OperationInput,
1215
+ ): readonly { readonly input: string; readonly value: FileInputValue }[] {
1216
+ const isFile = (candidate: unknown): candidate is FileInputValue =>
1217
+ typeof candidate === "object"
1218
+ && candidate !== null
1219
+ && !Array.isArray(candidate)
1220
+ && "kind" in candidate
1221
+ && candidate.kind === "file"
1222
+ && "reference" in candidate
1223
+ && typeof candidate.reference === "string";
1224
+ return Object.entries(input).flatMap(([name, value]) => {
1225
+ const values = Array.isArray(value) ? value : [value];
1226
+ return values.flatMap((candidate) =>
1227
+ isFile(candidate)
1228
+ ? [{ input: name, value: candidate }]
1229
+ : []);
1230
+ });
1231
+ }
1232
+
1233
+ function bindFile(
1234
+ input: string,
1235
+ value: FileInputValue,
1236
+ path: string,
1237
+ cleanupBarrier: PortableProviderPluginCleanupBarrier,
1238
+ closeBoundFile: (descriptor: number) => void,
1239
+ closeBindingDescriptor: (descriptor: number) => void,
1240
+ removeBindingDirectory: (path: string, force: boolean) => void,
1241
+ ): BoundInvocationFile {
1242
+ const summary = summarizePlanFile(value);
1243
+ let sourceDescriptor: number | null = openSync(
1244
+ path,
1245
+ constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0),
1246
+ );
1247
+ let snapshotDirectory: string | null = null;
1248
+ let writer: number | null = null;
1249
+ let reader: number | null = null;
1250
+ const closeForTransfer = (
1251
+ descriptor: number,
1252
+ resource: string,
1253
+ ): void => {
1254
+ try {
1255
+ closeBindingDescriptor(descriptor);
1256
+ } catch (error) {
1257
+ throw new UnverifiedPlanFileBindingCleanupError(resource, error);
1258
+ }
1259
+ };
1260
+ const removeForTransfer = (
1261
+ directory: string,
1262
+ force: boolean,
1263
+ ): void => {
1264
+ try {
1265
+ removeBindingDirectory(directory, force);
1266
+ } catch (error) {
1267
+ throw new UnverifiedPlanFileBindingCleanupError(
1268
+ "snapshot-directory",
1269
+ error,
1270
+ );
1271
+ }
1272
+ };
1273
+ try {
1274
+ snapshotDirectory = mkdtempSync(
1275
+ join(tmpdir(), "wrench-portable-plan-file-"),
1276
+ );
1277
+ chmodSync(snapshotDirectory, 0o700);
1278
+ const snapshotPath = join(snapshotDirectory, "verified");
1279
+ const before = fstatSync(sourceDescriptor, { bigint: true });
1280
+ if (!before.isFile() || before.size !== BigInt(summary.bytes)) {
1281
+ throw new Error("portable plugin plan file changed before invocation");
1282
+ }
1283
+ writer = openSync(
1284
+ snapshotPath,
1285
+ constants.O_WRONLY
1286
+ | constants.O_CREAT
1287
+ | constants.O_EXCL
1288
+ | (constants.O_NOFOLLOW ?? 0),
1289
+ 0o600,
1290
+ );
1291
+ const hash = createHash("sha256");
1292
+ const buffer = Buffer.allocUnsafe(64 * 1024);
1293
+ let total = 0;
1294
+ for (;;) {
1295
+ const count = readSync(
1296
+ sourceDescriptor,
1297
+ buffer,
1298
+ 0,
1299
+ buffer.byteLength,
1300
+ total,
1301
+ );
1302
+ if (count === 0) break;
1303
+ total += count;
1304
+ if (total > summary.bytes) {
1305
+ throw new Error("portable plugin plan file grew before invocation");
1306
+ }
1307
+ hash.update(buffer.subarray(0, count));
1308
+ let written = 0;
1309
+ while (written < count) {
1310
+ const countWritten = writeSync(
1311
+ writer,
1312
+ buffer,
1313
+ written,
1314
+ count - written,
1315
+ total - count + written,
1316
+ );
1317
+ if (countWritten < 1) {
1318
+ throw new Error("portable plugin plan-file snapshot write stalled");
1319
+ }
1320
+ written += countWritten;
1321
+ }
1322
+ }
1323
+ const after = fstatSync(sourceDescriptor, { bigint: true });
1324
+ if (
1325
+ total !== summary.bytes
1326
+ || hash.digest("hex") !== summary.sha256
1327
+ || after.dev !== before.dev
1328
+ || after.ino !== before.ino
1329
+ || after.size !== before.size
1330
+ || after.mtimeNs !== before.mtimeNs
1331
+ || after.ctimeNs !== before.ctimeNs
1332
+ ) {
1333
+ throw new Error("portable plugin plan file failed its content hash");
1334
+ }
1335
+ fsyncSync(writer);
1336
+ const verifiedWriter = writer;
1337
+ writer = null;
1338
+ closeForTransfer(verifiedWriter, "snapshot-writer");
1339
+ chmodSync(snapshotPath, 0o400);
1340
+ reader = openSync(
1341
+ snapshotPath,
1342
+ constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0),
1343
+ );
1344
+ const snapshot = fstatSync(reader, { bigint: true });
1345
+ if (!snapshot.isFile() || snapshot.size !== BigInt(summary.bytes)) {
1346
+ throw new Error("portable plugin plan-file snapshot is invalid");
1347
+ }
1348
+ // An unlinked, read-only descriptor is an immutable invocation snapshot:
1349
+ // later source rewrites and pathname replacement cannot affect reads.
1350
+ unlinkSync(snapshotPath);
1351
+ const verifiedDirectory = snapshotDirectory;
1352
+ snapshotDirectory = null;
1353
+ removeForTransfer(verifiedDirectory, false);
1354
+ const verifiedSource = sourceDescriptor;
1355
+ sourceDescriptor = null;
1356
+ closeForTransfer(verifiedSource, "source-descriptor");
1357
+ const handle = `file-${randomUUID()}`;
1358
+ const descriptor = reader;
1359
+ reader = null;
1360
+ let disposed = false;
1361
+ return {
1362
+ descriptor,
1363
+ dispose: () => {
1364
+ if (disposed) return;
1365
+ closeBoundFile(descriptor);
1366
+ disposed = true;
1367
+ },
1368
+ protocol: Object.freeze({
1369
+ input,
1370
+ handle,
1371
+ bytes: summary.bytes,
1372
+ mediaType: summary.mediaType,
1373
+ sha256: summary.sha256,
1374
+ }),
1375
+ };
1376
+ } catch (error) {
1377
+ const cleanupFailures: unknown[] = [];
1378
+ if (error instanceof UnverifiedPlanFileBindingCleanupError) {
1379
+ cleanupFailures.push(error);
1380
+ }
1381
+ const cleanupDescriptor = (
1382
+ descriptor: number,
1383
+ resource: string,
1384
+ ): void => {
1385
+ try {
1386
+ closeBindingDescriptor(descriptor);
1387
+ } catch (cleanupError) {
1388
+ cleanupFailures.push(
1389
+ new UnverifiedPlanFileBindingCleanupError(
1390
+ resource,
1391
+ cleanupError,
1392
+ ),
1393
+ );
1394
+ }
1395
+ };
1396
+ if (writer !== null) {
1397
+ const pendingWriter = writer;
1398
+ writer = null;
1399
+ cleanupDescriptor(pendingWriter, "snapshot-writer");
1400
+ }
1401
+ if (reader !== null) {
1402
+ const pendingReader = reader;
1403
+ reader = null;
1404
+ cleanupDescriptor(pendingReader, "snapshot-reader");
1405
+ }
1406
+ if (snapshotDirectory !== null) {
1407
+ const pendingDirectory = snapshotDirectory;
1408
+ snapshotDirectory = null;
1409
+ try {
1410
+ removeBindingDirectory(pendingDirectory, true);
1411
+ } catch (cleanupError) {
1412
+ cleanupFailures.push(
1413
+ new UnverifiedPlanFileBindingCleanupError(
1414
+ "snapshot-directory",
1415
+ cleanupError,
1416
+ ),
1417
+ );
1418
+ }
1419
+ }
1420
+ if (sourceDescriptor !== null) {
1421
+ const pendingSource = sourceDescriptor;
1422
+ sourceDescriptor = null;
1423
+ cleanupDescriptor(pendingSource, "source-descriptor");
1424
+ }
1425
+ if (cleanupFailures.length > 0) {
1426
+ const cause = new AggregateError(
1427
+ [
1428
+ error,
1429
+ ...cleanupFailures.filter((failure) => failure !== error),
1430
+ ],
1431
+ "portable plugin plan-file binding cleanup failed",
1432
+ );
1433
+ cleanupBarrier.unsafe(cause);
1434
+ throw new Error(
1435
+ "portable provider plugin plan-file cleanup failed",
1436
+ { cause },
1437
+ );
1438
+ }
1439
+ throw error;
1440
+ }
1441
+ }
1442
+
1443
+ function portableInput(
1444
+ input: OperationInput,
1445
+ files: readonly BoundInvocationFile[],
1446
+ ): PortablePluginJsonObject {
1447
+ const handles = new Map<string, string[]>();
1448
+ for (const file of files) {
1449
+ const values = handles.get(file.protocol.input) ?? [];
1450
+ values.push(file.protocol.handle);
1451
+ handles.set(file.protocol.input, values);
1452
+ }
1453
+ const output: Record<string, unknown> = {};
1454
+ for (const [name, value] of Object.entries(input)) {
1455
+ const fileHandles = handles.get(name);
1456
+ if (fileHandles === undefined) {
1457
+ output[name] = value;
1458
+ continue;
1459
+ }
1460
+ output[name] = Array.isArray(value) ? fileHandles : fileHandles[0];
1461
+ }
1462
+ return normalizePortablePluginJsonObject(output, "portable plugin input");
1463
+ }
1464
+
1465
+ async function resolveWebFiles(
1466
+ input: OperationInput,
1467
+ resolver: BrowserFileResolver | undefined,
1468
+ cleanupBarrier: PortableProviderPluginCleanupBarrier,
1469
+ closeBoundFile: (descriptor: number) => void,
1470
+ closeBindingDescriptor: (descriptor: number) => void,
1471
+ removeBindingDirectory: (path: string, force: boolean) => void,
1472
+ operationDeadline?: WebSessionOperationDeadline,
1473
+ ): Promise<readonly BoundInvocationFile[]> {
1474
+ const values = fileValues(input);
1475
+ if (values.length === 0) return Object.freeze([]);
1476
+ if (resolver === undefined) {
1477
+ throw new Error("portable plugin plan-file resolver is unavailable");
1478
+ }
1479
+ const paths = await resolver(values.map((entry) => entry.value));
1480
+ operationDeadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
1481
+ if (paths.length !== values.length) {
1482
+ throw new Error("portable plugin plan-file resolver returned the wrong count");
1483
+ }
1484
+ const files: BoundInvocationFile[] = [];
1485
+ try {
1486
+ for (const [index, entry] of values.entries()) {
1487
+ operationDeadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
1488
+ const path = paths[index];
1489
+ if (path === undefined) {
1490
+ throw new Error("portable plugin plan-file resolver omitted a path");
1491
+ }
1492
+ files.push(bindFile(
1493
+ entry.input,
1494
+ entry.value,
1495
+ path,
1496
+ cleanupBarrier,
1497
+ closeBoundFile,
1498
+ closeBindingDescriptor,
1499
+ removeBindingDirectory,
1500
+ ));
1501
+ }
1502
+ return Object.freeze(files);
1503
+ } catch (error) {
1504
+ disposePortableRuntimeResources(cleanupBarrier, undefined, files);
1505
+ throw error;
1506
+ }
1507
+ }
1508
+
1509
+ async function runWebPortableHost(options: {
1510
+ readonly package: VerifiedPortableProviderPluginPackage;
1511
+ readonly binding: PortableProviderPluginBindingV1;
1512
+ readonly operation: PortableProviderPluginOperationV1;
1513
+ readonly input: OperationInput;
1514
+ readonly auth: GhostgetAuth;
1515
+ readonly environment: Environment;
1516
+ readonly fileResolver?: BrowserFileResolver;
1517
+ readonly signal?: AbortSignal;
1518
+ readonly operationDeadline?: WebSessionOperationDeadline;
1519
+ readonly beforeDispatch?: (event: WebSessionDispatchEvent) => Promise<void>;
1520
+ readonly afterDispatchVerified?: (event: WebSessionDispatchEvent) => Promise<void>;
1521
+ readonly dependencies: PortableProviderRuntimeDependencies;
1522
+ }): Promise<PortableProviderPluginHostResult> {
1523
+ const cleanupBarrier = registerPortableProviderPluginCleanupBarrier();
1524
+ const deadline = options.operationDeadline;
1525
+ const plannedIds = options.operation.dispatch === "single"
1526
+ ? [options.operation.name]
1527
+ : [];
1528
+ let started = 0;
1529
+ let verified = 0;
1530
+ let files: readonly BoundInvocationFile[] = Object.freeze([]);
1531
+ let fetchScope: PinnedHttpsFetchScope | undefined;
1532
+ let hostCompletion: Promise<void> | undefined;
1533
+ const capabilityActivity = createPortableCapabilityActivity();
1534
+ try {
1535
+ const resolveFiles = () =>
1536
+ resolveWebFiles(
1537
+ options.input,
1538
+ options.fileResolver,
1539
+ cleanupBarrier,
1540
+ options.dependencies.closeBoundFile,
1541
+ options.dependencies.closePlanFileBindingDescriptor,
1542
+ options.dependencies.removePlanFileBindingDirectory,
1543
+ options.operationDeadline,
1544
+ );
1545
+ deadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
1546
+ const fileResolution = resolveFiles();
1547
+ if (deadline === undefined) {
1548
+ files = await fileResolution;
1549
+ } else {
1550
+ try {
1551
+ files = await deadline.run(
1552
+ () => fileResolution,
1553
+ WEB_SESSION_OPERATION_LABEL,
1554
+ );
1555
+ } catch (error) {
1556
+ if (deadline.signal.aborted) {
1557
+ const settled = await settlesWithin(
1558
+ fileResolution,
1559
+ PORTABLE_CAPABILITY_ABORT_JOIN_MS,
1560
+ );
1561
+ if (settled?.status === "fulfilled") {
1562
+ // OperationDeadline performs a post-work check. Retain ownership
1563
+ // when binding completed just after the deadline, so the outer
1564
+ // resource boundary still closes every descriptor.
1565
+ files = settled.value;
1566
+ } else if (settled === null) {
1567
+ cleanupBarrier.unsafe(new Error(
1568
+ "portable plugin plan-file resolution outlived its operation deadline",
1569
+ ));
1570
+ // The unsafe durable lease remains the fail-closed proof, but keep
1571
+ // a handler attached so a very late successful bind is still
1572
+ // disposed rather than orphaned in this process.
1573
+ void fileResolution.then(
1574
+ (lateFiles) => {
1575
+ try {
1576
+ disposePortableRuntimeResources(
1577
+ cleanupBarrier,
1578
+ undefined,
1579
+ lateFiles,
1580
+ );
1581
+ } catch {
1582
+ // The already-unsafe barrier and retained lease own recovery.
1583
+ }
1584
+ },
1585
+ () => undefined,
1586
+ );
1587
+ }
1588
+ }
1589
+ throw error;
1590
+ }
1591
+ }
1592
+ deadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
1593
+ const byHandle = new Map(files.map((file) => [
1594
+ file.protocol.handle,
1595
+ file,
1596
+ ]));
1597
+ const scope = options.dependencies.createFetchScope(
1598
+ options.binding.origin,
1599
+ );
1600
+ fetchScope = scope;
1601
+ const timeoutMs = Math.min(
1602
+ options.operation.timeoutMs,
1603
+ deadline?.remainingTimeMs() ?? options.operation.timeoutMs,
1604
+ );
1605
+ if (timeoutMs < 100) {
1606
+ throw new Error("portable plugin operation deadline was exhausted");
1607
+ }
1608
+ const invokeHost = () => {
1609
+ const host = trackPortableProviderPluginHostCompletion(
1610
+ options.dependencies.runHost({
1611
+ package: options.package,
1612
+ route: {
1613
+ transport: options.binding.transport,
1614
+ surfaceId: options.binding.surfaceId,
1615
+ operation: options.operation.name,
1616
+ contractVersion: options.operation.contractVersion,
1617
+ },
1618
+ input: portableInput(options.input, files),
1619
+ auth: {
1620
+ kind: options.auth.kind,
1621
+ handle: `auth-${randomUUID()}`,
1622
+ ...(options.auth.subject === undefined
1623
+ ? {}
1624
+ : { subject: options.auth.subject }),
1625
+ },
1626
+ files: files.map((file) => file.protocol),
1627
+ timeoutMs,
1628
+ hostVersion: PORTABLE_HOST_VERSION,
1629
+ plannedDispatchIds: plannedIds,
1630
+ capabilityHost: capabilityHost({
1631
+ package: options.package,
1632
+ binding: options.binding,
1633
+ auth: options.auth,
1634
+ environment: options.environment,
1635
+ files: byHandle,
1636
+ dependencies: options.dependencies,
1637
+ fetch: scope.fetch,
1638
+ activity: capabilityActivity,
1639
+ cleanupBarrier,
1640
+ ...(deadline === undefined
1641
+ ? {}
1642
+ : { operationDeadline: deadline }),
1643
+ beginDispatch: async (dispatchId) => {
1644
+ deadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
1645
+ const expected = plannedIds[started];
1646
+ if (expected !== dispatchId) {
1647
+ throw new Error("portable plugin changed its confirmed dispatch");
1648
+ }
1649
+ const index = started + 1;
1650
+ await options.beforeDispatch?.({
1651
+ id: dispatchId,
1652
+ index,
1653
+ progress: {
1654
+ planned: plannedIds.length,
1655
+ started,
1656
+ verified,
1657
+ },
1658
+ });
1659
+ started += 1;
1660
+ deadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
1661
+ let completed = false;
1662
+ return Object.freeze({
1663
+ verify: async () => {
1664
+ deadline?.throwIfUnavailable(WEB_SESSION_OPERATION_LABEL);
1665
+ if (completed || verified + 1 !== index) {
1666
+ throw new Error("portable plugin verified dispatch out of order");
1667
+ }
1668
+ await options.afterDispatchVerified?.({
1669
+ id: dispatchId,
1670
+ index,
1671
+ progress: {
1672
+ planned: plannedIds.length,
1673
+ started,
1674
+ verified: verified + 1,
1675
+ },
1676
+ });
1677
+ verified += 1;
1678
+ completed = true;
1679
+ },
1680
+ });
1681
+ },
1682
+ }),
1683
+ ...(deadline === undefined
1684
+ ? (options.signal === undefined ? {} : { signal: options.signal })
1685
+ : { signal: deadline.signal }),
1686
+ }),
1687
+ );
1688
+ hostCompletion = host.then(
1689
+ () => undefined,
1690
+ () => undefined,
1691
+ );
1692
+ return host;
1693
+ };
1694
+ return deadline === undefined
1695
+ ? await invokeHost()
1696
+ : await deadline.run(invokeHost, WEB_SESSION_OPERATION_LABEL);
1697
+ } finally {
1698
+ // An operation deadline may win its race while the host is still using
1699
+ // file descriptors or the pinned fetch scope. Keep those resources alive
1700
+ // until the actual host boundary settles, then prove their disposal to the
1701
+ // lease scope.
1702
+ await hostCompletion;
1703
+ await capabilityActivity.whenIdle();
1704
+ disposePortableRuntimeResources(cleanupBarrier, fetchScope, files);
1705
+ }
1706
+ }
1707
+
1708
+ function webExecutor(
1709
+ packageValue: VerifiedPortableProviderPluginPackage,
1710
+ binding: PortableProviderPluginBindingV1,
1711
+ operations: ReadonlyMap<string, PortableProviderPluginOperationV1>,
1712
+ environment: Environment,
1713
+ dependencies: PortableProviderRuntimeDependencies,
1714
+ ): WebSessionOperationExecutor {
1715
+ return async (_manifest, recipe, input, auth, options): Promise<WebSessionExecution> => {
1716
+ const operation = operations.get(
1717
+ `${recipe.action}@${recipe.contractVersion}`,
1718
+ );
1719
+ if (operation === undefined) {
1720
+ throw new Error("portable plugin operation disappeared");
1721
+ }
1722
+ try {
1723
+ const result = await runWebPortableHost({
1724
+ package: packageValue,
1725
+ binding,
1726
+ operation,
1727
+ input,
1728
+ auth,
1729
+ environment: options.environment ?? environment,
1730
+ ...(options.fileResolver === undefined
1731
+ ? {}
1732
+ : { fileResolver: options.fileResolver }),
1733
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
1734
+ ...(options.operationDeadline === undefined
1735
+ ? {}
1736
+ : { operationDeadline: options.operationDeadline }),
1737
+ ...(options.beforeDispatch === undefined
1738
+ ? {}
1739
+ : { beforeDispatch: options.beforeDispatch }),
1740
+ ...(options.afterDispatchVerified === undefined
1741
+ ? {}
1742
+ : { afterDispatchVerified: options.afterDispatchVerified }),
1743
+ dependencies,
1744
+ });
1745
+ return {
1746
+ status: "succeeded",
1747
+ output: result.output,
1748
+ finalUrl: result.finalUrl,
1749
+ dispatchStarted: result.dispatch.started > 0,
1750
+ dispatch: result.dispatch,
1751
+ };
1752
+ } catch (error) {
1753
+ const dispatch = typeof error === "object"
1754
+ && error !== null
1755
+ && "dispatch" in error
1756
+ ? (error as { readonly dispatch: WebSessionExecution["dispatch"] }).dispatch
1757
+ : {
1758
+ planned: operation.dispatch === "single" ? 1 : 0,
1759
+ started: 0,
1760
+ verified: 0,
1761
+ };
1762
+ return {
1763
+ status: dispatch.started > dispatch.verified
1764
+ ? "indeterminate"
1765
+ : dispatch.verified > 0
1766
+ ? "partial"
1767
+ : "failed",
1768
+ output: null,
1769
+ finalUrl: null,
1770
+ dispatchStarted: dispatch.started > 0,
1771
+ dispatch,
1772
+ error: "portable provider plugin execution failed",
1773
+ };
1774
+ }
1775
+ };
1776
+ }
1777
+
1778
+ async function runProviderPortableHost(
1779
+ packageValue: VerifiedPortableProviderPluginPackage,
1780
+ binding: PortableProviderPluginBindingV1,
1781
+ operation: PortableProviderPluginOperationV1,
1782
+ context: ProviderActionContext,
1783
+ dependencies: PortableProviderRuntimeDependencies,
1784
+ ): Promise<void> {
1785
+ const cleanupBarrier = registerPortableProviderPluginCleanupBarrier();
1786
+ const values = fileValues(context.input);
1787
+ const files: BoundInvocationFile[] = [];
1788
+ try {
1789
+ for (const inputName of new Set(values.map((entry) => entry.input))) {
1790
+ const matching = values.filter((entry) => entry.input === inputName);
1791
+ const resolved = await context.resolveFiles(inputName);
1792
+ if (context.signal.aborted) {
1793
+ throw new Error("portable provider invocation was cancelled");
1794
+ }
1795
+ if (resolved.length !== matching.length) {
1796
+ throw new Error("portable provider file resolver returned the wrong count");
1797
+ }
1798
+ for (const [index, entry] of matching.entries()) {
1799
+ if (context.signal.aborted) {
1800
+ throw new Error("portable provider invocation was cancelled");
1801
+ }
1802
+ const file = resolved[index];
1803
+ if (file === undefined) {
1804
+ throw new Error("portable provider file resolver omitted a file");
1805
+ }
1806
+ files.push(bindFile(
1807
+ inputName,
1808
+ entry.value,
1809
+ file.path,
1810
+ cleanupBarrier,
1811
+ dependencies.closeBoundFile,
1812
+ dependencies.closePlanFileBindingDescriptor,
1813
+ dependencies.removePlanFileBindingDirectory,
1814
+ ));
1815
+ }
1816
+ }
1817
+ } catch (error) {
1818
+ disposePortableRuntimeResources(cleanupBarrier, undefined, files);
1819
+ throw error;
1820
+ }
1821
+ const byHandle = new Map(files.map((file) => [
1822
+ file.protocol.handle,
1823
+ file,
1824
+ ]));
1825
+ const plannedIds = operation.dispatch === "single" ? [operation.name] : [];
1826
+ let fetchScope: PinnedHttpsFetchScope | undefined;
1827
+ const capabilityActivity = createPortableCapabilityActivity();
1828
+ try {
1829
+ fetchScope = dependencies.createFetchScope(binding.origin);
1830
+ const timeoutMs = Math.min(
1831
+ operation.timeoutMs,
1832
+ context.remainingTimeMs(),
1833
+ );
1834
+ if (timeoutMs < 100) {
1835
+ throw new Error("portable provider operation deadline was exhausted");
1836
+ }
1837
+ const result = await trackPortableProviderPluginHostCompletion(
1838
+ dependencies.runHost({
1839
+ package: packageValue,
1840
+ route: {
1841
+ transport: binding.transport,
1842
+ surfaceId: binding.surfaceId,
1843
+ operation: operation.name,
1844
+ contractVersion: operation.contractVersion,
1845
+ },
1846
+ input: portableInput(context.input, files),
1847
+ auth: {
1848
+ kind: context.auth.kind,
1849
+ handle: `auth-${randomUUID()}`,
1850
+ ...(context.auth.subject === undefined
1851
+ ? {}
1852
+ : { subject: context.auth.subject }),
1853
+ },
1854
+ files: files.map((file) => file.protocol),
1855
+ timeoutMs,
1856
+ hostVersion: PORTABLE_HOST_VERSION,
1857
+ plannedDispatchIds: plannedIds,
1858
+ capabilityHost: capabilityHost({
1859
+ package: packageValue,
1860
+ binding,
1861
+ auth: context.auth,
1862
+ environment: context.environment,
1863
+ files: byHandle,
1864
+ dependencies,
1865
+ fetch: fetchScope.fetch,
1866
+ activity: capabilityActivity,
1867
+ cleanupBarrier,
1868
+ beginDispatch: async (dispatchId) => {
1869
+ if (dispatchId !== operation.name) {
1870
+ throw new Error("portable plugin changed its confirmed dispatch");
1871
+ }
1872
+ return context.beginDispatch();
1873
+ },
1874
+ }),
1875
+ signal: context.signal,
1876
+ }),
1877
+ );
1878
+ context.setOutput(result.output);
1879
+ if (result.finalUrl !== null) context.setFinalUrl(result.finalUrl);
1880
+ } finally {
1881
+ await capabilityActivity.whenIdle();
1882
+ disposePortableRuntimeResources(cleanupBarrier, fetchScope, files);
1883
+ }
1884
+ }
1885
+
1886
+ /**
1887
+ * Construct the only in-process hooks portable packages may receive. The
1888
+ * caller still cannot admit these raw values into a registry: the authority
1889
+ * module brands only the exact frozen objects returned here.
1890
+ */
1891
+ export function createKernelPortableProviderPluginBindingProjections(
1892
+ packageValue: VerifiedPortableProviderPluginPackage,
1893
+ environment: Environment,
1894
+ dependencies: PortableProviderRuntimeDependencies,
1895
+ ): readonly KernelPortableProviderPluginBindingProjection[] {
1896
+ if (!isResolvedPortableProviderRuntimeDependencies(dependencies)) {
1897
+ throw new Error(
1898
+ "portable provider plugin wrappers require resolved kernel runtime dependencies",
1899
+ );
1900
+ }
1901
+ const bindings = packageValue.manifest.bindings.map((binding) => {
1902
+ if (
1903
+ binding.transport === "linked-device"
1904
+ && binding.operations.some((operation) => operation.state === "observed")
1905
+ ) {
1906
+ throw new Error(
1907
+ `portable plugin ${packageValue.manifest.id} linked-device execution requires a future lifecycle protocol`,
1908
+ );
1909
+ }
1910
+ const operationByKey = new Map(binding.operations.map((operation) => [
1911
+ `${operation.name}@${operation.contractVersion}`,
1912
+ operation,
1913
+ ]));
1914
+ const subject = portableSubject(binding.subject);
1915
+ const manifest = virtualManifest(packageValue, binding);
1916
+ let projectedBinding: ProviderPluginBindingDefinitionV1;
1917
+ if (binding.transport === "provider-api") {
1918
+ const definitions = Object.freeze(
1919
+ binding.operations.map((operation) =>
1920
+ Object.freeze(portableProviderOperation(operation))),
1921
+ );
1922
+ projectedBinding = Object.freeze({
1923
+ transport: binding.transport,
1924
+ surfaceId: binding.surfaceId,
1925
+ origin: binding.origin,
1926
+ authKinds: binding.authKinds,
1927
+ subject,
1928
+ operations: definitions,
1929
+ runtime: Object.freeze({
1930
+ loadRuntime: () => Promise.resolve(Object.freeze({
1931
+ execute: async (context: ProviderActionContext) => {
1932
+ const operation = operationByKey.get(
1933
+ `${context.recipe.action}@${context.recipe.contractVersion}`,
1934
+ );
1935
+ if (operation === undefined) {
1936
+ throw new Error("portable provider operation disappeared");
1937
+ }
1938
+ await runProviderPortableHost(
1939
+ packageValue,
1940
+ binding,
1941
+ operation,
1942
+ context,
1943
+ dependencies,
1944
+ );
1945
+ },
1946
+ })),
1947
+ }),
1948
+ });
1949
+ } else {
1950
+ const definitions = Object.freeze(
1951
+ binding.operations.map((operation) =>
1952
+ Object.freeze(portableSessionOperation(operation))),
1953
+ );
1954
+ const execute = webExecutor(
1955
+ packageValue,
1956
+ binding,
1957
+ operationByKey,
1958
+ environment,
1959
+ dependencies,
1960
+ );
1961
+ projectedBinding = Object.freeze({
1962
+ transport: binding.transport,
1963
+ surfaceId: binding.surfaceId,
1964
+ origin: binding.origin,
1965
+ authKinds: binding.authKinds,
1966
+ subject,
1967
+ operations: definitions,
1968
+ runtime: Object.freeze({
1969
+ loadRuntime: () => Promise.resolve(Object.freeze({
1970
+ probe: async (
1971
+ auth: GhostgetAuth,
1972
+ options?: ProviderPluginSubjectProbeOptionsV1,
1973
+ ) => {
1974
+ const probe = binding.subject.probe;
1975
+ if (probe === null) {
1976
+ throw new Error("portable plugin has no current-account probe");
1977
+ }
1978
+ const operation = operationByKey.get(
1979
+ `${probe.operation}@${probe.contractVersion}`,
1980
+ );
1981
+ if (operation === undefined) {
1982
+ throw new Error("portable plugin subject probe disappeared");
1983
+ }
1984
+ const identity = createPortableOperationIdentityV1({
1985
+ package: {
1986
+ id: packageValue.manifest.id,
1987
+ version: packageValue.manifest.version,
1988
+ hostApiVersion: packageValue.manifest.hostApiVersion,
1989
+ bundleSha256: packageValue.bundleSha256,
1990
+ manifestSha256: packageValue.manifestSha256,
1991
+ capabilities: packageValue.manifest.capabilities,
1992
+ },
1993
+ binding,
1994
+ operation,
1995
+ });
1996
+ const lease = acquirePortableProviderPluginInvocationLease(
1997
+ identity,
1998
+ randomUUID(),
1999
+ environment,
2000
+ );
2001
+ const containment =
2002
+ createPortableProviderPluginInvocationLeaseContainmentController(
2003
+ lease,
2004
+ environment,
2005
+ );
2006
+ const outcome = await settlePortableProviderPluginCleanup(
2007
+ async () => {
2008
+ const result = await runWebPortableHost({
2009
+ package: packageValue,
2010
+ binding,
2011
+ operation,
2012
+ input: {},
2013
+ auth,
2014
+ environment,
2015
+ dependencies,
2016
+ ...(options?.signal === undefined
2017
+ ? {}
2018
+ : { signal: options.signal }),
2019
+ });
2020
+ if (
2021
+ typeof result.output !== "string"
2022
+ || !subject.matches(result.output)
2023
+ ) {
2024
+ throw new Error(
2025
+ "portable plugin returned an invalid account subject",
2026
+ );
2027
+ }
2028
+ return result.output;
2029
+ },
2030
+ {
2031
+ containment,
2032
+ cleanupComplete: containment.cleanupComplete,
2033
+ },
2034
+ );
2035
+ releasePortableProviderPluginInvocationLease(
2036
+ containment.current,
2037
+ environment,
2038
+ );
2039
+ if (outcome.status === "rejected") {
2040
+ throw outcome.reason;
2041
+ }
2042
+ return outcome.value;
2043
+ },
2044
+ execute,
2045
+ })),
2046
+ }),
2047
+ });
2048
+ }
2049
+ return Object.freeze({
2050
+ adapterId: binding.adapterId,
2051
+ manifest,
2052
+ portableBinding: binding,
2053
+ binding: projectedBinding,
2054
+ });
2055
+ });
2056
+ return Object.freeze(bindings);
2057
+ }