@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,2530 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import {
4
+ lstatSync,
5
+ type BigIntStats,
6
+ } from "node:fs";
7
+ import { isIP } from "node:net";
8
+ import { dirname, join, resolve } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import ts from "typescript";
11
+
12
+ import type {
13
+ ArrayInputField,
14
+ FileInputField,
15
+ IdempotencyKind,
16
+ InputField,
17
+ InputSchema,
18
+ OperationRisk,
19
+ ScalarInputField,
20
+ } from "./model";
21
+ import {
22
+ isPortableProviderPluginVersion,
23
+ } from "./provider-plugin-identifiers";
24
+
25
+ export {
26
+ isPortableProviderPluginVersion,
27
+ } from "./provider-plugin-identifiers";
28
+
29
+ export const PORTABLE_PROVIDER_PLUGIN_MANIFEST_SCHEMA_VERSION = 1 as const;
30
+ export const PORTABLE_PROVIDER_PLUGIN_HOST_API_VERSION = 1 as const;
31
+ export const PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME = "ghostget-plugin.json";
32
+ // Historical verified bundles retain their exact manifest name and bytes.
33
+ export const WRENCH_PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME = "wrench-plugin.json";
34
+ export const LEGACY_PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME = "oh-plugin.json";
35
+ const portableProviderPluginManifestNames = new Set([
36
+ PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME,
37
+ WRENCH_PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME,
38
+ LEGACY_PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME,
39
+ ]);
40
+
41
+ export const portableProviderPluginTransports = [
42
+ "provider-api",
43
+ "web-session-api",
44
+ "linked-device",
45
+ ] as const;
46
+
47
+ export const portableProviderPluginAuthKinds = [
48
+ "cookie-source",
49
+ "cookies-file",
50
+ "browser-profile",
51
+ "oauth-token-file",
52
+ "linked-device-store",
53
+ ] as const;
54
+
55
+ /** Session materials implemented by the host API v1 credential kernel. */
56
+ export const portableProviderPluginSessionMaterialNames = [
57
+ "cookie-jar",
58
+ "oauth-access-token",
59
+ ] as const;
60
+
61
+ export type PortableProviderPluginTransport =
62
+ (typeof portableProviderPluginTransports)[number];
63
+ export type PortableProviderPluginAuthKind =
64
+ (typeof portableProviderPluginAuthKinds)[number];
65
+ export type PortableProviderPluginSessionMaterialName =
66
+ (typeof portableProviderPluginSessionMaterialNames)[number];
67
+
68
+ type PortableProviderPluginOperationBaseV1 = {
69
+ readonly name: string;
70
+ readonly contractVersion: number;
71
+ readonly timeoutMs: number;
72
+ readonly maxOutputBytes: number;
73
+ readonly state: "observed" | "capture-required";
74
+ readonly risk: OperationRisk;
75
+ readonly dispatch: "none" | "single";
76
+ readonly sideEffect: string;
77
+ readonly idempotency: IdempotencyKind;
78
+ readonly dedupeWindowMs: number;
79
+ readonly input: InputSchema;
80
+ readonly implementation: string;
81
+ };
82
+
83
+ export type PortableProviderApiPluginOperationV1 =
84
+ PortableProviderPluginOperationBaseV1 & {
85
+ readonly requiredScopeSets: readonly (readonly string[])[];
86
+ readonly coverage: readonly string[];
87
+ };
88
+
89
+ export type PortableWebSessionPluginOperationV1 =
90
+ PortableProviderPluginOperationBaseV1 & {
91
+ readonly requiredScopeSets?: never;
92
+ readonly coverage?: never;
93
+ };
94
+
95
+ export type PortableProviderPluginOperationV1 =
96
+ | PortableProviderApiPluginOperationV1
97
+ | PortableWebSessionPluginOperationV1;
98
+
99
+ type PortableProviderPluginBindingBaseV1 = {
100
+ /** Stable short-form CLI address owned by this exact plugin binding. */
101
+ readonly adapterId: string;
102
+ readonly surfaceId: string;
103
+ readonly origin: `https://${string}`;
104
+ readonly authKinds: readonly PortableProviderPluginAuthKind[];
105
+ readonly subject: {
106
+ readonly format: string;
107
+ readonly kind: "opaque-token" | "decimal" | "did" | "uuid" | "e164";
108
+ readonly probe: {
109
+ readonly operation: string;
110
+ readonly contractVersion: number;
111
+ } | null;
112
+ };
113
+ };
114
+
115
+ export type PortableProviderApiPluginBindingV1 =
116
+ PortableProviderPluginBindingBaseV1 & {
117
+ readonly transport: "provider-api";
118
+ readonly operations: readonly PortableProviderApiPluginOperationV1[];
119
+ };
120
+
121
+ export type PortableWebSessionApiPluginBindingV1 =
122
+ PortableProviderPluginBindingBaseV1 & {
123
+ readonly transport: "web-session-api";
124
+ readonly operations: readonly PortableWebSessionPluginOperationV1[];
125
+ };
126
+
127
+ export type PortableLinkedDevicePluginBindingV1 =
128
+ PortableProviderPluginBindingBaseV1 & {
129
+ readonly transport: "linked-device";
130
+ readonly operations: readonly PortableWebSessionPluginOperationV1[];
131
+ };
132
+
133
+ export type PortableProviderPluginBindingV1 =
134
+ | PortableProviderApiPluginBindingV1
135
+ | PortableWebSessionApiPluginBindingV1
136
+ | PortableLinkedDevicePluginBindingV1;
137
+
138
+ export type PortableProviderPluginCapabilitiesV1 = {
139
+ readonly networkOrigins: readonly `https://${string}`[];
140
+ readonly planFiles: "none" | "read";
141
+ readonly state: "none" | "namespaced";
142
+ readonly sessionMaterial: readonly PortableProviderPluginSessionMaterialName[];
143
+ };
144
+
145
+ export type PortableProviderPluginFileV1 = {
146
+ readonly path: string;
147
+ readonly kind: "runtime" | "data";
148
+ readonly bytes: number;
149
+ readonly sha256: string;
150
+ };
151
+
152
+ export type PortableProviderPluginProvenanceV1 =
153
+ | {
154
+ readonly kind: "local";
155
+ }
156
+ | {
157
+ readonly kind: "git";
158
+ readonly repository: `https://${string}`;
159
+ readonly revision: string;
160
+ };
161
+
162
+ export type PortableProviderPluginManifestV1 = {
163
+ readonly schemaVersion:
164
+ typeof PORTABLE_PROVIDER_PLUGIN_MANIFEST_SCHEMA_VERSION;
165
+ readonly hostApiVersion: typeof PORTABLE_PROVIDER_PLUGIN_HOST_API_VERSION;
166
+ readonly id: string;
167
+ readonly version: string;
168
+ readonly displayName: string;
169
+ readonly runtime: {
170
+ readonly kind: "bun-js";
171
+ readonly entrypoint: string;
172
+ };
173
+ readonly provenance: PortableProviderPluginProvenanceV1;
174
+ readonly capabilities: PortableProviderPluginCapabilitiesV1;
175
+ readonly bindings: readonly PortableProviderPluginBindingV1[];
176
+ readonly files: readonly PortableProviderPluginFileV1[];
177
+ };
178
+
179
+ export type PortableProviderPluginManifestResult =
180
+ | {
181
+ readonly ok: true;
182
+ readonly value: PortableProviderPluginManifestV1;
183
+ }
184
+ | {
185
+ readonly ok: false;
186
+ readonly issues: readonly string[];
187
+ };
188
+
189
+ export type VerifiedPortableProviderPluginFile = {
190
+ readonly path: string;
191
+ readonly kind: PortableProviderPluginFileV1["kind"];
192
+ readonly bytes: Buffer;
193
+ readonly sha256: string;
194
+ };
195
+
196
+ export type VerifiedPortableProviderPluginPackage = {
197
+ readonly root: string;
198
+ readonly manifest: PortableProviderPluginManifestV1;
199
+ readonly manifestBytes: Buffer;
200
+ readonly manifestSha256: string;
201
+ readonly bundleSha256: string;
202
+ readonly payloadBytes: number;
203
+ readonly files: readonly VerifiedPortableProviderPluginFile[];
204
+ };
205
+
206
+ const verifiedPortableProviderPluginPackages = new WeakSet<object>();
207
+
208
+ /** Kernel evidence that a package object came from the exact-byte verifier. */
209
+ export function isVerifiedPortableProviderPluginPackage(
210
+ value: unknown,
211
+ ): value is VerifiedPortableProviderPluginPackage {
212
+ return typeof value === "object"
213
+ && value !== null
214
+ && verifiedPortableProviderPluginPackages.has(value);
215
+ }
216
+
217
+ const MAX_MANIFEST_BYTES = 256 * 1024;
218
+ const MAX_PLUGIN_FILES = 256;
219
+ const MAX_PLUGIN_FILE_BYTES = 8 * 1024 * 1024;
220
+ const MAX_PLUGIN_PAYLOAD_BYTES = 32 * 1024 * 1024;
221
+ const MAX_BINDINGS = 64;
222
+ const MAX_OPERATIONS_PER_BINDING = 256;
223
+ const MAX_NETWORK_ORIGINS = 64;
224
+ const MAX_SESSION_MATERIAL_NAMES = 64;
225
+ const MAX_PLUGIN_DIRECTORIES = MAX_PLUGIN_FILES * 15;
226
+ const MAX_PLUGIN_PACKAGE_ENTRIES =
227
+ 1 + MAX_PLUGIN_FILES + MAX_PLUGIN_DIRECTORIES;
228
+ const allowedPortableRuntimeImports = new Set([
229
+ "node:readline",
230
+ ]);
231
+ const portableRuntimeImportScanner = new Bun.Transpiler({ loader: "js" });
232
+ const pathHelperPath = join(
233
+ dirname(fileURLToPath(import.meta.url)),
234
+ "path-helper.ts",
235
+ );
236
+ const pathHelperConfigPath = join(
237
+ dirname(fileURLToPath(import.meta.url)),
238
+ "state-helper.bunfig.toml",
239
+ );
240
+ let pathHelperSpawnObserverForTest: (() => void) | undefined;
241
+
242
+ export function observePortableProviderPluginPathHelperSpawnsForTest(
243
+ observer: () => void,
244
+ ): () => void {
245
+ if (process.env.NODE_ENV !== "test") {
246
+ throw new Error("path helper spawn observation is available only in tests");
247
+ }
248
+ if (pathHelperSpawnObserverForTest !== undefined) {
249
+ throw new Error("path helper spawn observation is already active");
250
+ }
251
+ pathHelperSpawnObserverForTest = observer;
252
+ return () => {
253
+ if (pathHelperSpawnObserverForTest === observer) {
254
+ pathHelperSpawnObserverForTest = undefined;
255
+ }
256
+ };
257
+ }
258
+
259
+ const pluginIdPattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u;
260
+ const operationNamePattern =
261
+ /^[a-z][a-z0-9-]{0,39}(?:\.[a-z][a-z0-9-]{0,39}){1,3}$/u;
262
+ const materialNamePattern =
263
+ /^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/u;
264
+ const packagePathSegmentPattern =
265
+ /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
266
+ const sha256Pattern = /^[a-f0-9]{64}$/u;
267
+ const gitRevisionPattern = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u;
268
+
269
+ const forbiddenPackageBasenames = new Set([
270
+ ".env",
271
+ ".npmrc",
272
+ ".yarnrc",
273
+ "bun.lock",
274
+ "bun.lockb",
275
+ "bunfig.toml",
276
+ "deno.json",
277
+ "deno.jsonc",
278
+ "package-lock.json",
279
+ "package.json",
280
+ "pnpm-lock.yaml",
281
+ "yarn.lock",
282
+ ]);
283
+
284
+ const forbiddenPackageExtensions = new Set([
285
+ ".cmd",
286
+ ".dll",
287
+ ".dylib",
288
+ ".exe",
289
+ ".node",
290
+ ".ps1",
291
+ ".sh",
292
+ ".so",
293
+ ]);
294
+ const forbiddenPortablePathStems =
295
+ /^(?:aux|con|nul|prn|com[1-9]|lpt[1-9])$/iu;
296
+
297
+ function isRecord(value: unknown): value is Record<string, unknown> {
298
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
299
+ return false;
300
+ }
301
+ const prototype = Object.getPrototypeOf(value) as unknown;
302
+ if (prototype !== Object.prototype && prototype !== null) return false;
303
+ return Reflect.ownKeys(value).every((key) => {
304
+ if (typeof key !== "string") return false;
305
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
306
+ return descriptor !== undefined
307
+ && descriptor.enumerable
308
+ && "value" in descriptor;
309
+ });
310
+ }
311
+
312
+ function exactKeys(
313
+ value: Readonly<Record<string, unknown>>,
314
+ expected: readonly string[],
315
+ label: string,
316
+ ): void {
317
+ const actual = Object.keys(value).sort();
318
+ const wanted = [...expected].sort();
319
+ if (
320
+ actual.length !== wanted.length
321
+ || actual.some((key, index) => key !== wanted[index])
322
+ ) {
323
+ throw new Error(
324
+ `${label} must contain exactly: ${wanted.join(", ")}`,
325
+ );
326
+ }
327
+ }
328
+
329
+ function allowedKeys(
330
+ value: Readonly<Record<string, unknown>>,
331
+ allowed: readonly string[],
332
+ label: string,
333
+ ): void {
334
+ const accepted = new Set(allowed);
335
+ const unexpected = Object.keys(value)
336
+ .filter((key) => !accepted.has(key))
337
+ .sort();
338
+ if (unexpected.length > 0) {
339
+ throw new Error(`${label} contains unsupported keys: ${unexpected.join(", ")}`);
340
+ }
341
+ }
342
+
343
+ function compareCanonicalText(left: string, right: string): number {
344
+ if (left < right) return -1;
345
+ if (left > right) return 1;
346
+ return 0;
347
+ }
348
+
349
+ function record(value: unknown, label: string): Record<string, unknown> {
350
+ if (!isRecord(value)) throw new Error(`${label} must be an object`);
351
+ return value;
352
+ }
353
+
354
+ function boundedString(
355
+ value: unknown,
356
+ label: string,
357
+ maximum: number,
358
+ ): string {
359
+ if (
360
+ typeof value !== "string"
361
+ || value.length < 1
362
+ || value.length > maximum
363
+ || /[\0\r\n]/u.test(value)
364
+ ) {
365
+ throw new Error(`${label} must be bounded single-line text`);
366
+ }
367
+ return value;
368
+ }
369
+
370
+ function boundedSemanticText(
371
+ value: unknown,
372
+ label: string,
373
+ maximumBytes: number,
374
+ ): string {
375
+ if (
376
+ typeof value !== "string"
377
+ || value.length < 1
378
+ || Buffer.byteLength(value, "utf8") > maximumBytes
379
+ ) {
380
+ throw new Error(`${label} must be bounded text`);
381
+ }
382
+ for (let index = 0; index < value.length; index += 1) {
383
+ const code = value.charCodeAt(index);
384
+ if (code <= 0x1f || code === 0x7f) {
385
+ throw new Error(`${label} must not contain control characters`);
386
+ }
387
+ }
388
+ return value;
389
+ }
390
+
391
+ function boundedArray(
392
+ value: unknown,
393
+ label: string,
394
+ maximum: number,
395
+ ): readonly unknown[] {
396
+ if (!Array.isArray(value) || value.length < 1 || value.length > maximum) {
397
+ throw new Error(
398
+ `${label} must contain between 1 and ${maximum} items`,
399
+ );
400
+ }
401
+ return value;
402
+ }
403
+
404
+ function safePluginId(value: unknown, label: string): string {
405
+ const id = boundedString(value, label, 63);
406
+ if (!pluginIdPattern.test(id)) {
407
+ throw new Error(`${label} must be strict lowercase kebab-case`);
408
+ }
409
+ return id;
410
+ }
411
+
412
+ function safeVersion(value: unknown): string {
413
+ const version = boundedString(value, "plugin version", 128);
414
+ if (!isPortableProviderPluginVersion(version)) {
415
+ throw new Error("plugin version must be strict semantic version text");
416
+ }
417
+ return version;
418
+ }
419
+
420
+ function safeOperationName(value: unknown): string {
421
+ const name = boundedString(value, "plugin operation name", 163);
422
+ if (!operationNamePattern.test(name)) {
423
+ throw new Error(
424
+ "plugin operation name must be a bounded dotted semantic outcome",
425
+ );
426
+ }
427
+ return name;
428
+ }
429
+
430
+ function safeSha256(value: unknown, label: string): string {
431
+ const sha256 = boundedString(value, label, 64);
432
+ if (!sha256Pattern.test(sha256)) {
433
+ throw new Error(`${label} must be a lowercase SHA-256 digest`);
434
+ }
435
+ return sha256;
436
+ }
437
+
438
+ function exactPublicHttpsOrigin(value: unknown, label: string): `https://${string}` {
439
+ const text = boundedString(value, label, 2_048);
440
+ let url: URL;
441
+ try {
442
+ url = new URL(text);
443
+ } catch {
444
+ throw new Error(`${label} must be an exact public HTTPS origin`);
445
+ }
446
+ const hostname = url.hostname.toLowerCase();
447
+ if (
448
+ url.protocol !== "https:"
449
+ || url.username !== ""
450
+ || url.password !== ""
451
+ || url.pathname !== "/"
452
+ || url.search !== ""
453
+ || url.hash !== ""
454
+ || url.origin !== text
455
+ || hostname.endsWith(".")
456
+ || hostname === "localhost"
457
+ || hostname.endsWith(".localhost")
458
+ || hostname.endsWith(".local")
459
+ || hostname.endsWith(".internal")
460
+ || !hostname.includes(".")
461
+ || isIP(hostname) !== 0
462
+ ) {
463
+ throw new Error(`${label} must be an exact public HTTPS origin`);
464
+ }
465
+ return text as `https://${string}`;
466
+ }
467
+
468
+ function exactHttpsRepository(value: unknown): `https://${string}` {
469
+ const text = boundedString(value, "plugin provenance repository", 2_048);
470
+ let url: URL;
471
+ try {
472
+ url = new URL(text);
473
+ } catch {
474
+ throw new Error(
475
+ "plugin git provenance repository must identify an HTTPS repository",
476
+ );
477
+ }
478
+ const hostname = url.hostname.toLowerCase();
479
+ if (
480
+ url.protocol !== "https:"
481
+ || url.username !== ""
482
+ || url.password !== ""
483
+ || url.search !== ""
484
+ || url.hash !== ""
485
+ || url.pathname === "/"
486
+ || url.pathname.endsWith("/")
487
+ || hostname.endsWith(".")
488
+ || hostname === "localhost"
489
+ || hostname.endsWith(".localhost")
490
+ || hostname.endsWith(".local")
491
+ || hostname.endsWith(".internal")
492
+ || !hostname.includes(".")
493
+ || isIP(hostname) !== 0
494
+ || url.toString() !== text
495
+ ) {
496
+ throw new Error(
497
+ "plugin git provenance repository must identify an exact credential-free HTTPS repository",
498
+ );
499
+ }
500
+ return text as `https://${string}`;
501
+ }
502
+
503
+ function safePackagePath(value: unknown, label: string): string {
504
+ const path = boundedString(value, label, 512);
505
+ if (
506
+ path.startsWith("/")
507
+ || path.endsWith("/")
508
+ || path.includes("\\")
509
+ || path.includes("//")
510
+ ) {
511
+ throw new Error(`${label} must be a normalized relative package path`);
512
+ }
513
+ const segments = path.split("/");
514
+ if (
515
+ segments.length > 16
516
+ || segments.some(
517
+ (segment) =>
518
+ segment === "."
519
+ || segment === ".."
520
+ || segment.endsWith(".")
521
+ || !packagePathSegmentPattern.test(segment)
522
+ || forbiddenPortablePathStems.test(segment.split(".")[0] ?? ""),
523
+ )
524
+ ) {
525
+ throw new Error(`${label} contains an unsafe path segment`);
526
+ }
527
+ if (segments.some((segment) => segment.toLowerCase() === "node_modules")) {
528
+ throw new Error(`${label} cannot contain node_modules`);
529
+ }
530
+ const basename = segments.at(-1)?.toLowerCase();
531
+ if (basename === undefined || forbiddenPackageBasenames.has(basename)) {
532
+ throw new Error(`${label} names a forbidden package-manager or environment file`);
533
+ }
534
+ const dot = basename.lastIndexOf(".");
535
+ const extension = dot < 0 ? "" : basename.slice(dot);
536
+ if (forbiddenPackageExtensions.has(extension)) {
537
+ throw new Error(`${label} names a native or shell executable`);
538
+ }
539
+ if (portableProviderPluginManifestNames.has(path)) {
540
+ throw new Error(`${label} cannot redeclare the package manifest`);
541
+ }
542
+ return path;
543
+ }
544
+
545
+ const operationRiskRank = Object.freeze({
546
+ R1: 1,
547
+ R2: 2,
548
+ R3: 3,
549
+ R4: 4,
550
+ } satisfies Readonly<Record<OperationRisk, number>>);
551
+
552
+ const readOperationSuffixes = new Set([
553
+ "get",
554
+ "inspect",
555
+ "list",
556
+ "lookup",
557
+ "probe",
558
+ "read",
559
+ "search",
560
+ "status",
561
+ ]);
562
+ const reversibleMutationSuffixes = new Set([
563
+ "edit",
564
+ "follow",
565
+ "like",
566
+ "save",
567
+ "unfollow",
568
+ "unlike",
569
+ "unsave",
570
+ "update",
571
+ ]);
572
+ const outwardMutationSuffixes = new Set([
573
+ "comment",
574
+ "create",
575
+ "post",
576
+ "publish",
577
+ "reply",
578
+ "send",
579
+ "upload",
580
+ ]);
581
+ const highAuthorityOperationSegments = new Set([
582
+ "admin",
583
+ "administrator",
584
+ "ban",
585
+ "billing",
586
+ "charge",
587
+ "charges",
588
+ "delete",
589
+ "destroy",
590
+ "erase",
591
+ "financial",
592
+ "funds",
593
+ "invoice",
594
+ "invoices",
595
+ "money",
596
+ "payment",
597
+ "payments",
598
+ "permission",
599
+ "permissions",
600
+ "purchase",
601
+ "purge",
602
+ "refund",
603
+ "remove",
604
+ "role",
605
+ "roles",
606
+ "subscription",
607
+ "subscriptions",
608
+ "suspend",
609
+ "transfer",
610
+ "transfers",
611
+ "withdraw",
612
+ "withdrawal",
613
+ ]);
614
+
615
+ /**
616
+ * Derive the kernel's conservative minimum authority from one semantic
617
+ * outcome. Unknown, destructive, administrative, and financial outcomes are
618
+ * inert R4 reservations until the kernel learns an explicit policy.
619
+ */
620
+ export function derivePortableProviderPluginMinimumRisk(
621
+ operationName: string,
622
+ ): OperationRisk {
623
+ const name = safeOperationName(
624
+ operationName,
625
+ );
626
+ const segments = name.split(/[.-]/u);
627
+ if (segments.some((segment) => highAuthorityOperationSegments.has(segment))) {
628
+ return "R4";
629
+ }
630
+ const suffix = name.split(".").at(-1);
631
+ if (suffix !== undefined && readOperationSuffixes.has(suffix)) return "R1";
632
+ if (suffix !== undefined && reversibleMutationSuffixes.has(suffix)) return "R2";
633
+ if (suffix !== undefined && outwardMutationSuffixes.has(suffix)) return "R3";
634
+ return "R4";
635
+ }
636
+
637
+ function parseDescription(value: unknown, label: string): string {
638
+ return boundedSemanticText(value, label, 500);
639
+ }
640
+
641
+ function parseMediaTypes(value: unknown, label: string): readonly string[] {
642
+ if (!Array.isArray(value) || value.length < 1 || value.length > 32) {
643
+ throw new Error(`${label} must contain between 1 and 32 media types`);
644
+ }
645
+ const mediaTypes = value.map((candidate, index) => {
646
+ const mediaType = boundedString(candidate, `${label}[${index}]`, 256);
647
+ if (
648
+ !/^[a-z0-9!#$&^_.+-]+\/(?:[a-z0-9!#$&^_.+-]+|\*)$/u.test(mediaType)
649
+ ) {
650
+ throw new Error(`${label}[${index}] is malformed`);
651
+ }
652
+ return mediaType;
653
+ });
654
+ if (new Set(mediaTypes).size !== mediaTypes.length) {
655
+ throw new Error(`${label} must not contain duplicates`);
656
+ }
657
+ return Object.freeze([...mediaTypes].sort());
658
+ }
659
+
660
+ function parseScalarInputField(
661
+ field: Readonly<Record<string, unknown>>,
662
+ label: string,
663
+ ): ScalarInputField {
664
+ allowedKeys(
665
+ field,
666
+ [
667
+ "type",
668
+ "description",
669
+ "minLength",
670
+ "maxLength",
671
+ "minimum",
672
+ "maximum",
673
+ "enum",
674
+ "format",
675
+ "urlPathPrefixes",
676
+ ],
677
+ label,
678
+ );
679
+ if (
680
+ field.type !== "string"
681
+ && field.type !== "number"
682
+ && field.type !== "boolean"
683
+ ) {
684
+ throw new Error(`${label}.type is unsupported`);
685
+ }
686
+ const type = field.type;
687
+ const description = parseDescription(field.description, `${label}.description`);
688
+
689
+ let minLength: number | undefined;
690
+ let maxLength: number | undefined;
691
+ if (field.minLength !== undefined || field.maxLength !== undefined) {
692
+ if (type !== "string") {
693
+ throw new Error(`${label} length bounds require a string field`);
694
+ }
695
+ if (
696
+ field.minLength !== undefined
697
+ && (
698
+ !Number.isSafeInteger(field.minLength)
699
+ || (field.minLength as number) < 0
700
+ || (field.minLength as number) > 1_000_000
701
+ )
702
+ ) {
703
+ throw new Error(`${label}.minLength is invalid`);
704
+ }
705
+ if (
706
+ field.maxLength !== undefined
707
+ && (
708
+ !Number.isSafeInteger(field.maxLength)
709
+ || (field.maxLength as number) < 1
710
+ || (field.maxLength as number) > 1_000_000
711
+ )
712
+ ) {
713
+ throw new Error(`${label}.maxLength is invalid`);
714
+ }
715
+ minLength = field.minLength as number | undefined;
716
+ maxLength = field.maxLength as number | undefined;
717
+ if (
718
+ minLength !== undefined
719
+ && maxLength !== undefined
720
+ && minLength > maxLength
721
+ ) {
722
+ throw new Error(`${label}.minLength cannot exceed maxLength`);
723
+ }
724
+ }
725
+
726
+ let minimum: number | undefined;
727
+ let maximum: number | undefined;
728
+ if (field.minimum !== undefined || field.maximum !== undefined) {
729
+ if (type !== "number") {
730
+ throw new Error(`${label} numeric bounds require a number field`);
731
+ }
732
+ if (
733
+ field.minimum !== undefined
734
+ && (typeof field.minimum !== "number" || !Number.isFinite(field.minimum))
735
+ ) {
736
+ throw new Error(`${label}.minimum is invalid`);
737
+ }
738
+ if (
739
+ field.maximum !== undefined
740
+ && (typeof field.maximum !== "number" || !Number.isFinite(field.maximum))
741
+ ) {
742
+ throw new Error(`${label}.maximum is invalid`);
743
+ }
744
+ minimum = field.minimum;
745
+ maximum = field.maximum;
746
+ if (
747
+ minimum !== undefined
748
+ && maximum !== undefined
749
+ && minimum > maximum
750
+ ) {
751
+ throw new Error(`${label}.minimum cannot exceed maximum`);
752
+ }
753
+ }
754
+
755
+ let enumValues: readonly (string | number | boolean)[] | undefined;
756
+ if (field.enum !== undefined) {
757
+ if (
758
+ !Array.isArray(field.enum)
759
+ || field.enum.length < 1
760
+ || field.enum.length > 100
761
+ || field.enum.some((candidate) =>
762
+ typeof candidate !== type
763
+ || (typeof candidate === "number" && !Number.isFinite(candidate)))
764
+ ) {
765
+ throw new Error(`${label}.enum must contain bounded values matching its type`);
766
+ }
767
+ const values = field.enum as readonly (string | number | boolean)[];
768
+ if (
769
+ new Set(values.map((candidate) => JSON.stringify(candidate))).size
770
+ !== values.length
771
+ ) {
772
+ throw new Error(`${label}.enum must not contain duplicates`);
773
+ }
774
+ enumValues = Object.freeze([...values]);
775
+ }
776
+
777
+ let format: "url" | "path-segment" | undefined;
778
+ if (field.format !== undefined) {
779
+ if (
780
+ type !== "string"
781
+ || (field.format !== "url" && field.format !== "path-segment")
782
+ ) {
783
+ throw new Error(`${label}.format is invalid`);
784
+ }
785
+ format = field.format;
786
+ }
787
+
788
+ let urlPathPrefixes: readonly string[] | undefined;
789
+ if (field.urlPathPrefixes !== undefined) {
790
+ if (
791
+ type !== "string"
792
+ || format !== "url"
793
+ || !Array.isArray(field.urlPathPrefixes)
794
+ || field.urlPathPrefixes.length < 1
795
+ || field.urlPathPrefixes.length > 20
796
+ ) {
797
+ throw new Error(
798
+ `${label}.urlPathPrefixes requires a URL string field and 1-20 prefixes`,
799
+ );
800
+ }
801
+ const prefixes = field.urlPathPrefixes.map((candidate, index) => {
802
+ const prefix = boundedString(
803
+ candidate,
804
+ `${label}.urlPathPrefixes[${index}]`,
805
+ 2_048,
806
+ );
807
+ if (
808
+ !prefix.startsWith("/")
809
+ || prefix.startsWith("//")
810
+ || prefix.includes("\\")
811
+ || prefix.includes("?")
812
+ || prefix.includes("#")
813
+ || /%(?:25|2e|2f|5c)/iu.test(prefix)
814
+ || prefix.split("/").some((segment) =>
815
+ segment === "." || segment === "..")
816
+ ) {
817
+ throw new Error(`${label}.urlPathPrefixes[${index}] is ambiguous`);
818
+ }
819
+ return prefix;
820
+ });
821
+ if (new Set(prefixes).size !== prefixes.length) {
822
+ throw new Error(`${label}.urlPathPrefixes must not contain duplicates`);
823
+ }
824
+ urlPathPrefixes = Object.freeze([...prefixes].sort());
825
+ }
826
+
827
+ return Object.freeze({
828
+ type,
829
+ description,
830
+ ...(minLength === undefined ? {} : { minLength }),
831
+ ...(maxLength === undefined ? {} : { maxLength }),
832
+ ...(minimum === undefined ? {} : { minimum }),
833
+ ...(maximum === undefined ? {} : { maximum }),
834
+ ...(enumValues === undefined ? {} : { enum: enumValues }),
835
+ ...(format === undefined ? {} : { format }),
836
+ ...(urlPathPrefixes === undefined ? {} : { urlPathPrefixes }),
837
+ });
838
+ }
839
+
840
+ function parseInputField(
841
+ value: unknown,
842
+ label: string,
843
+ maximumArrayItems: 25 | 100,
844
+ nested = false,
845
+ ): InputField {
846
+ const field = record(value, label);
847
+ if (field.type === "file") {
848
+ allowedKeys(
849
+ field,
850
+ ["type", "description", "maxBytes", "mediaTypes"],
851
+ label,
852
+ );
853
+ const description = parseDescription(field.description, `${label}.description`);
854
+ if (
855
+ !Number.isSafeInteger(field.maxBytes)
856
+ || (field.maxBytes as number) < 1
857
+ || (field.maxBytes as number) > 1024 * 1024 * 1024
858
+ ) {
859
+ throw new Error(`${label}.maxBytes must be a bounded positive integer`);
860
+ }
861
+ const result: FileInputField = {
862
+ type: "file",
863
+ description,
864
+ maxBytes: field.maxBytes as number,
865
+ ...(field.mediaTypes === undefined
866
+ ? {}
867
+ : { mediaTypes: parseMediaTypes(field.mediaTypes, `${label}.mediaTypes`) }),
868
+ };
869
+ return Object.freeze(result);
870
+ }
871
+ if (field.type === "array") {
872
+ allowedKeys(
873
+ field,
874
+ ["type", "description", "items", "minItems", "maxItems"],
875
+ label,
876
+ );
877
+ if (nested) throw new Error(`${label} cannot contain a nested array`);
878
+ if (
879
+ !Number.isSafeInteger(field.minItems)
880
+ || !Number.isSafeInteger(field.maxItems)
881
+ || (field.minItems as number) < 0
882
+ || (field.maxItems as number) < 1
883
+ || (field.maxItems as number) < (field.minItems as number)
884
+ || (field.maxItems as number) > maximumArrayItems
885
+ ) {
886
+ throw new Error(`${label} must declare valid bounded item counts`);
887
+ }
888
+ const result: ArrayInputField = {
889
+ type: "array",
890
+ description: parseDescription(field.description, `${label}.description`),
891
+ items: parseInputField(
892
+ field.items,
893
+ `${label}.items`,
894
+ maximumArrayItems,
895
+ true,
896
+ ) as ScalarInputField | FileInputField,
897
+ minItems: field.minItems as number,
898
+ maxItems: field.maxItems as number,
899
+ };
900
+ return Object.freeze(result);
901
+ }
902
+ return parseScalarInputField(field, label);
903
+ }
904
+
905
+ function parseInputSchema(
906
+ value: unknown,
907
+ label: string,
908
+ maximumArrayItems: 25 | 100,
909
+ ): InputSchema {
910
+ const schema = record(value, label);
911
+ exactKeys(schema, ["properties", "required"], label);
912
+ const rawProperties = record(schema.properties, `${label}.properties`);
913
+ const propertyEntries = Object.entries(rawProperties)
914
+ .sort(([left], [right]) => compareCanonicalText(left, right));
915
+ if (propertyEntries.length > 100) {
916
+ throw new Error(`${label}.properties may contain at most 100 fields`);
917
+ }
918
+ const properties: Record<string, InputField> = Object.create(null) as
919
+ Record<string, InputField>;
920
+ for (const [name, field] of propertyEntries) {
921
+ if (!/^[a-z][a-z0-9_]{0,63}$/u.test(name)) {
922
+ throw new Error(`${label}.properties contains invalid field ${name}`);
923
+ }
924
+ properties[name] = parseInputField(
925
+ field,
926
+ `${label}.properties.${name}`,
927
+ maximumArrayItems,
928
+ );
929
+ }
930
+ if (
931
+ !Array.isArray(schema.required)
932
+ || schema.required.length > 100
933
+ || schema.required.some((name) => typeof name !== "string")
934
+ ) {
935
+ throw new Error(`${label}.required must contain at most 100 field names`);
936
+ }
937
+ const required = schema.required as readonly string[];
938
+ if (new Set(required).size !== required.length) {
939
+ throw new Error(`${label}.required must not contain duplicates`);
940
+ }
941
+ for (const name of required) {
942
+ if (properties[name] === undefined) {
943
+ throw new Error(`${label}.required references unknown field ${name}`);
944
+ }
945
+ }
946
+ return Object.freeze({
947
+ properties: Object.freeze(properties),
948
+ required: Object.freeze([...required].sort()),
949
+ });
950
+ }
951
+
952
+ function containsFileInput(field: InputField): boolean {
953
+ return field.type === "file"
954
+ || (field.type === "array" && field.items.type === "file");
955
+ }
956
+
957
+ function parseScope(value: unknown, label: string): string {
958
+ const scope = boundedString(value, label, 256);
959
+ if (!/^[\x21-\x7e]+$/u.test(scope)) {
960
+ throw new Error(`${label} must be printable non-whitespace ASCII`);
961
+ }
962
+ return scope;
963
+ }
964
+
965
+ function parseRequiredScopeSets(
966
+ value: unknown,
967
+ label: string,
968
+ ): readonly (readonly string[])[] {
969
+ const rawSets = boundedArray(value, label, 32);
970
+ const scopeSets = rawSets.map((rawSet, setIndex) => {
971
+ const rawScopes = boundedArray(
972
+ rawSet,
973
+ `${label}[${setIndex}]`,
974
+ 32,
975
+ );
976
+ const scopes = rawScopes.map((scope, scopeIndex) =>
977
+ parseScope(scope, `${label}[${setIndex}][${scopeIndex}]`));
978
+ if (new Set(scopes).size !== scopes.length) {
979
+ throw new Error(`${label}[${setIndex}] must not repeat a scope`);
980
+ }
981
+ return Object.freeze([...scopes].sort());
982
+ });
983
+ const identities = scopeSets.map((scopes) => scopes.join("\0"));
984
+ if (new Set(identities).size !== identities.length) {
985
+ throw new Error(`${label} must not repeat an equivalent scope set`);
986
+ }
987
+ return Object.freeze(
988
+ [...scopeSets].sort((left, right) =>
989
+ compareCanonicalText(left.join("\0"), right.join("\0"))),
990
+ );
991
+ }
992
+
993
+ function parseCoverage(value: unknown, label: string): readonly string[] {
994
+ const rawCoverage = boundedArray(value, label, 64);
995
+ const coverage = rawCoverage.map((candidate, index) => {
996
+ const item = boundedString(candidate, `${label}[${index}]`, 128);
997
+ if (!/^[a-z][a-z0-9]*(?:[-.][a-z0-9]+)*$/u.test(item)) {
998
+ throw new Error(`${label}[${index}] is malformed`);
999
+ }
1000
+ return item;
1001
+ });
1002
+ if (new Set(coverage).size !== coverage.length) {
1003
+ throw new Error(`${label} must not contain duplicates`);
1004
+ }
1005
+ return Object.freeze([...coverage].sort());
1006
+ }
1007
+
1008
+ function parseOperation(
1009
+ value: unknown,
1010
+ transport: PortableProviderPluginTransport,
1011
+ ): PortableProviderPluginOperationV1 {
1012
+ const operation = record(value, "plugin operation");
1013
+ const providerApi = transport === "provider-api";
1014
+ exactKeys(
1015
+ operation,
1016
+ [
1017
+ "name",
1018
+ "contractVersion",
1019
+ "timeoutMs",
1020
+ "maxOutputBytes",
1021
+ "state",
1022
+ "risk",
1023
+ "dispatch",
1024
+ "sideEffect",
1025
+ "idempotency",
1026
+ "dedupeWindowMs",
1027
+ "input",
1028
+ "implementation",
1029
+ ...(providerApi ? ["requiredScopeSets", "coverage"] : []),
1030
+ ],
1031
+ "plugin operation",
1032
+ );
1033
+ const name = safeOperationName(operation.name);
1034
+ if (
1035
+ !Number.isSafeInteger(operation.contractVersion)
1036
+ || (operation.contractVersion as number) < 1
1037
+ || (operation.contractVersion as number) > 1_000_000
1038
+ ) {
1039
+ throw new Error(`plugin operation ${name} contractVersion is invalid`);
1040
+ }
1041
+ if (
1042
+ !Number.isSafeInteger(operation.timeoutMs)
1043
+ || (operation.timeoutMs as number) < 1_000
1044
+ || (operation.timeoutMs as number) > 120_000
1045
+ ) {
1046
+ throw new Error(`plugin operation ${name} timeoutMs is invalid`);
1047
+ }
1048
+ if (
1049
+ !Number.isSafeInteger(operation.maxOutputBytes)
1050
+ || (operation.maxOutputBytes as number) < 1_024
1051
+ || (operation.maxOutputBytes as number) > 512 * 1024
1052
+ ) {
1053
+ throw new Error(`plugin operation ${name} maxOutputBytes is invalid`);
1054
+ }
1055
+ if (operation.state !== "observed" && operation.state !== "capture-required") {
1056
+ throw new Error(`plugin operation ${name} state is invalid`);
1057
+ }
1058
+ if (
1059
+ operation.risk !== "R1"
1060
+ && operation.risk !== "R2"
1061
+ && operation.risk !== "R3"
1062
+ && operation.risk !== "R4"
1063
+ ) {
1064
+ throw new Error(`plugin operation ${name} risk is invalid`);
1065
+ }
1066
+ const risk = operation.risk;
1067
+ const minimumRisk = derivePortableProviderPluginMinimumRisk(name);
1068
+ if (operationRiskRank[risk] < operationRiskRank[minimumRisk]) {
1069
+ throw new Error(
1070
+ `plugin operation ${name} requires at least ${minimumRisk} authority, not ${risk}`,
1071
+ );
1072
+ }
1073
+ if (operation.dispatch !== "none" && operation.dispatch !== "single") {
1074
+ throw new Error(`plugin operation ${name} dispatch is invalid`);
1075
+ }
1076
+ if (
1077
+ operation.idempotency !== "none"
1078
+ && operation.idempotency !== "local-at-most-once"
1079
+ ) {
1080
+ throw new Error(`plugin operation ${name} idempotency is invalid`);
1081
+ }
1082
+ if (
1083
+ !Number.isSafeInteger(operation.dedupeWindowMs)
1084
+ || (operation.dedupeWindowMs as number) < 0
1085
+ || (operation.dedupeWindowMs as number) > 30 * 24 * 60 * 60_000
1086
+ ) {
1087
+ throw new Error(`plugin operation ${name} dedupeWindowMs is invalid`);
1088
+ }
1089
+ const sideEffect = boundedSemanticText(
1090
+ operation.sideEffect,
1091
+ `plugin operation ${name} sideEffect`,
1092
+ 500,
1093
+ );
1094
+ const implementation = boundedSemanticText(
1095
+ operation.implementation,
1096
+ `plugin operation ${name} implementation`,
1097
+ 500,
1098
+ );
1099
+ const input = parseInputSchema(
1100
+ operation.input,
1101
+ `plugin operation ${name} input`,
1102
+ providerApi ? 100 : 25,
1103
+ );
1104
+ if (
1105
+ risk === "R1"
1106
+ && (
1107
+ sideEffect !== "none"
1108
+ || operation.dispatch !== "none"
1109
+ || operation.idempotency !== "none"
1110
+ || operation.dedupeWindowMs !== 0
1111
+ || Object.values(input.properties).some(containsFileInput)
1112
+ )
1113
+ ) {
1114
+ throw new Error(
1115
+ `plugin operation ${name} must keep R1 semantics side-effect-free, file-free, and dispatch-free`,
1116
+ );
1117
+ }
1118
+ if (
1119
+ (risk === "R2" || risk === "R3")
1120
+ && (
1121
+ sideEffect === "none"
1122
+ || operation.dispatch !== "single"
1123
+ || operation.idempotency !== "local-at-most-once"
1124
+ || (operation.dedupeWindowMs as number) < 60_000
1125
+ )
1126
+ ) {
1127
+ throw new Error(
1128
+ `plugin operation ${name} must bind R2/R3 authority to one at-most-once dispatch and a 60-second minimum dedupe window`,
1129
+ );
1130
+ }
1131
+ if (risk === "R4" && operation.state !== "capture-required") {
1132
+ throw new Error(
1133
+ `plugin operation ${name} must keep R4 authority capture-required`,
1134
+ );
1135
+ }
1136
+ const common = {
1137
+ name,
1138
+ contractVersion: operation.contractVersion as number,
1139
+ timeoutMs: operation.timeoutMs as number,
1140
+ maxOutputBytes: operation.maxOutputBytes as number,
1141
+ state: operation.state,
1142
+ risk,
1143
+ dispatch: operation.dispatch,
1144
+ sideEffect,
1145
+ idempotency: operation.idempotency,
1146
+ dedupeWindowMs: operation.dedupeWindowMs as number,
1147
+ input,
1148
+ implementation,
1149
+ };
1150
+ if (!providerApi) {
1151
+ return Object.freeze(common) as PortableWebSessionPluginOperationV1;
1152
+ }
1153
+ return Object.freeze({
1154
+ ...common,
1155
+ requiredScopeSets: parseRequiredScopeSets(
1156
+ operation.requiredScopeSets,
1157
+ `plugin operation ${name} requiredScopeSets`,
1158
+ ),
1159
+ coverage: parseCoverage(
1160
+ operation.coverage,
1161
+ `plugin operation ${name} coverage`,
1162
+ ),
1163
+ }) as PortableProviderApiPluginOperationV1;
1164
+ }
1165
+
1166
+ function parseSubject(
1167
+ value: unknown,
1168
+ operations: readonly PortableProviderPluginOperationV1[],
1169
+ transport: PortableProviderPluginTransport,
1170
+ surfaceId: string,
1171
+ ): PortableProviderPluginBindingV1["subject"] {
1172
+ const subject = record(value, `plugin binding ${surfaceId} subject`);
1173
+ exactKeys(
1174
+ subject,
1175
+ ["format", "kind", "probe"],
1176
+ `plugin binding ${surfaceId} subject`,
1177
+ );
1178
+ const format = boundedSemanticText(
1179
+ subject.format,
1180
+ `plugin binding ${surfaceId} subject format`,
1181
+ 200,
1182
+ );
1183
+ if (
1184
+ subject.kind !== "opaque-token"
1185
+ && subject.kind !== "decimal"
1186
+ && subject.kind !== "did"
1187
+ && subject.kind !== "uuid"
1188
+ && subject.kind !== "e164"
1189
+ ) {
1190
+ throw new Error(`plugin binding ${surfaceId} subject kind is unsupported`);
1191
+ }
1192
+ if (subject.probe === null) {
1193
+ if (
1194
+ transport !== "provider-api"
1195
+ && operations.some((operation) => operation.state === "observed")
1196
+ ) {
1197
+ throw new Error(
1198
+ `plugin binding ${surfaceId} must declare an observed R1 subject probe before session operations become executable`,
1199
+ );
1200
+ }
1201
+ return Object.freeze({ format, kind: subject.kind, probe: null });
1202
+ }
1203
+ const probe = record(
1204
+ subject.probe,
1205
+ `plugin binding ${surfaceId} subject probe`,
1206
+ );
1207
+ exactKeys(
1208
+ probe,
1209
+ ["operation", "contractVersion"],
1210
+ `plugin binding ${surfaceId} subject probe`,
1211
+ );
1212
+ const operationName = safeOperationName(probe.operation);
1213
+ if (
1214
+ !Number.isSafeInteger(probe.contractVersion)
1215
+ || (probe.contractVersion as number) < 1
1216
+ || (probe.contractVersion as number) > 1_000_000
1217
+ ) {
1218
+ throw new Error(
1219
+ `plugin binding ${surfaceId} subject probe contractVersion is invalid`,
1220
+ );
1221
+ }
1222
+ const operation = operations.find((candidate) =>
1223
+ candidate.name === operationName
1224
+ && candidate.contractVersion === probe.contractVersion);
1225
+ if (
1226
+ operation === undefined
1227
+ || operation.state !== "observed"
1228
+ || operation.risk !== "R1"
1229
+ || operation.dispatch !== "none"
1230
+ || Object.keys(operation.input.properties).length !== 0
1231
+ || operation.input.required.length !== 0
1232
+ ) {
1233
+ throw new Error(
1234
+ `plugin binding ${surfaceId} subject probe must reference one input-free observed dispatch-free R1 operation`,
1235
+ );
1236
+ }
1237
+ return Object.freeze({
1238
+ format,
1239
+ kind: subject.kind,
1240
+ probe: Object.freeze({
1241
+ operation: operationName,
1242
+ contractVersion: probe.contractVersion as number,
1243
+ }),
1244
+ });
1245
+ }
1246
+
1247
+ function parseBinding(value: unknown): PortableProviderPluginBindingV1 {
1248
+ const binding = record(value, "plugin binding");
1249
+ exactKeys(
1250
+ binding,
1251
+ [
1252
+ "transport",
1253
+ "adapterId",
1254
+ "surfaceId",
1255
+ "origin",
1256
+ "authKinds",
1257
+ "subject",
1258
+ "operations",
1259
+ ],
1260
+ "plugin binding",
1261
+ );
1262
+ if (
1263
+ typeof binding.transport !== "string"
1264
+ || !portableProviderPluginTransports.includes(
1265
+ binding.transport as PortableProviderPluginTransport,
1266
+ )
1267
+ ) {
1268
+ throw new Error("plugin binding transport is unsupported");
1269
+ }
1270
+ const transport = binding.transport as PortableProviderPluginTransport;
1271
+ const adapterId = safePluginId(binding.adapterId, "plugin binding adapterId");
1272
+ const surfaceId = safePluginId(binding.surfaceId, "plugin binding surfaceId");
1273
+ const origin = exactPublicHttpsOrigin(
1274
+ binding.origin,
1275
+ `plugin binding ${surfaceId} origin`,
1276
+ );
1277
+ const authKinds = boundedArray(
1278
+ binding.authKinds,
1279
+ `plugin binding ${surfaceId} authKinds`,
1280
+ portableProviderPluginAuthKinds.length,
1281
+ ).map((kind) => {
1282
+ if (
1283
+ typeof kind !== "string"
1284
+ || !portableProviderPluginAuthKinds.includes(
1285
+ kind as PortableProviderPluginAuthKind,
1286
+ )
1287
+ ) {
1288
+ throw new Error(
1289
+ `plugin binding ${surfaceId} accepts an unsupported auth kind`,
1290
+ );
1291
+ }
1292
+ return kind as PortableProviderPluginAuthKind;
1293
+ });
1294
+ if (new Set(authKinds).size !== authKinds.length) {
1295
+ throw new Error(`plugin binding ${surfaceId} repeats an auth kind`);
1296
+ }
1297
+ const sortedAuthKinds = [...authKinds].sort();
1298
+ if (sortedAuthKinds.some((kind, index) => kind !== authKinds[index])) {
1299
+ throw new Error(`plugin binding ${surfaceId} auth kinds must be sorted`);
1300
+ }
1301
+ if (
1302
+ transport === "provider-api"
1303
+ && (
1304
+ sortedAuthKinds.length !== 1
1305
+ || sortedAuthKinds[0] !== "oauth-token-file"
1306
+ )
1307
+ ) {
1308
+ throw new Error(
1309
+ `plugin binding ${surfaceId} provider-api requires only oauth-token-file auth`,
1310
+ );
1311
+ }
1312
+ if (
1313
+ transport === "linked-device"
1314
+ && (
1315
+ sortedAuthKinds.length !== 1
1316
+ || sortedAuthKinds[0] !== "linked-device-store"
1317
+ )
1318
+ ) {
1319
+ throw new Error(
1320
+ `plugin binding ${surfaceId} linked-device requires only linked-device-store auth`,
1321
+ );
1322
+ }
1323
+ if (
1324
+ transport === "web-session-api"
1325
+ && sortedAuthKinds.some(
1326
+ (kind) =>
1327
+ kind === "oauth-token-file"
1328
+ || kind === "linked-device-store",
1329
+ )
1330
+ ) {
1331
+ throw new Error(
1332
+ `plugin binding ${surfaceId} web-session-api requires browser-session auth`,
1333
+ );
1334
+ }
1335
+ const operations = boundedArray(
1336
+ binding.operations,
1337
+ `plugin binding ${surfaceId} operations`,
1338
+ MAX_OPERATIONS_PER_BINDING,
1339
+ ).map((operation) => parseOperation(operation, transport));
1340
+ if (
1341
+ transport === "linked-device"
1342
+ && operations.some((operation) => operation.state !== "capture-required")
1343
+ ) {
1344
+ throw new Error(
1345
+ `plugin binding ${surfaceId} linked-device operations must remain capture-required until a portable lifecycle protocol is available`,
1346
+ );
1347
+ }
1348
+ const operationKeys = operations.map(
1349
+ (operation) => `${operation.name}@${operation.contractVersion}`,
1350
+ );
1351
+ if (new Set(operationKeys).size !== operationKeys.length) {
1352
+ throw new Error(
1353
+ `plugin binding ${surfaceId} repeats an operation contract`,
1354
+ );
1355
+ }
1356
+ if (
1357
+ new Set(operations.map((operation) => operation.name)).size
1358
+ !== operations.length
1359
+ ) {
1360
+ throw new Error(
1361
+ `plugin binding ${surfaceId} v1 permits only one current contract version per operation name`,
1362
+ );
1363
+ }
1364
+ const sortedOperations = [...operations].sort(
1365
+ (left, right) => {
1366
+ const nameOrder = compareCanonicalText(left.name, right.name);
1367
+ return nameOrder === 0
1368
+ ? left.contractVersion - right.contractVersion
1369
+ : nameOrder;
1370
+ },
1371
+ );
1372
+ if (
1373
+ sortedOperations.some(
1374
+ (operation, index) => operation !== operations[index],
1375
+ )
1376
+ ) {
1377
+ throw new Error(
1378
+ `plugin binding ${surfaceId} operations must be sorted by name and contract version`,
1379
+ );
1380
+ }
1381
+ const common = {
1382
+ adapterId,
1383
+ surfaceId,
1384
+ origin,
1385
+ authKinds: Object.freeze(sortedAuthKinds),
1386
+ subject: parseSubject(
1387
+ binding.subject,
1388
+ sortedOperations,
1389
+ transport,
1390
+ surfaceId,
1391
+ ),
1392
+ };
1393
+ if (transport === "provider-api") {
1394
+ return Object.freeze({
1395
+ ...common,
1396
+ transport,
1397
+ operations: Object.freeze(
1398
+ sortedOperations as readonly PortableProviderApiPluginOperationV1[],
1399
+ ),
1400
+ });
1401
+ }
1402
+ if (transport === "web-session-api") {
1403
+ return Object.freeze({
1404
+ ...common,
1405
+ transport,
1406
+ operations: Object.freeze(
1407
+ sortedOperations as readonly PortableWebSessionPluginOperationV1[],
1408
+ ),
1409
+ });
1410
+ }
1411
+ return Object.freeze({
1412
+ ...common,
1413
+ transport,
1414
+ operations: Object.freeze(
1415
+ sortedOperations as readonly PortableWebSessionPluginOperationV1[],
1416
+ ),
1417
+ });
1418
+ }
1419
+
1420
+ function parseCapabilities(
1421
+ value: unknown,
1422
+ bindingOrigins: ReadonlySet<string>,
1423
+ bindings: readonly PortableProviderPluginBindingV1[],
1424
+ ): PortableProviderPluginCapabilitiesV1 {
1425
+ const capabilities = record(value, "plugin capabilities");
1426
+ exactKeys(
1427
+ capabilities,
1428
+ ["networkOrigins", "planFiles", "state", "sessionMaterial"],
1429
+ "plugin capabilities",
1430
+ );
1431
+ if (
1432
+ capabilities.planFiles !== "none"
1433
+ && capabilities.planFiles !== "read"
1434
+ ) {
1435
+ throw new Error("plugin capabilities planFiles must be none or read");
1436
+ }
1437
+ if (
1438
+ capabilities.state !== "none"
1439
+ && capabilities.state !== "namespaced"
1440
+ ) {
1441
+ throw new Error("plugin capabilities state must be none or namespaced");
1442
+ }
1443
+ if (
1444
+ !Array.isArray(capabilities.networkOrigins)
1445
+ || capabilities.networkOrigins.length > MAX_NETWORK_ORIGINS
1446
+ ) {
1447
+ throw new Error(
1448
+ `plugin capabilities networkOrigins must contain at most ${MAX_NETWORK_ORIGINS} entries`,
1449
+ );
1450
+ }
1451
+ const networkOrigins = capabilities.networkOrigins.map((origin, index) =>
1452
+ exactPublicHttpsOrigin(
1453
+ origin,
1454
+ `plugin capabilities networkOrigins[${index}]`,
1455
+ ),
1456
+ );
1457
+ if (new Set(networkOrigins).size !== networkOrigins.length) {
1458
+ throw new Error("plugin capabilities repeat a network origin");
1459
+ }
1460
+ const sortedOrigins = [...networkOrigins].sort();
1461
+ if (sortedOrigins.some((origin, index) => origin !== networkOrigins[index])) {
1462
+ throw new Error("plugin capability network origins must be sorted");
1463
+ }
1464
+ for (const origin of bindingOrigins) {
1465
+ if (!networkOrigins.includes(origin as `https://${string}`)) {
1466
+ throw new Error(
1467
+ `plugin capability network origins omit binding origin ${origin}`,
1468
+ );
1469
+ }
1470
+ }
1471
+ if (
1472
+ !Array.isArray(capabilities.sessionMaterial)
1473
+ || capabilities.sessionMaterial.length > MAX_SESSION_MATERIAL_NAMES
1474
+ ) {
1475
+ throw new Error(
1476
+ `plugin capabilities sessionMaterial must contain at most ${MAX_SESSION_MATERIAL_NAMES} entries`,
1477
+ );
1478
+ }
1479
+ const sessionMaterial = capabilities.sessionMaterial.map((name, index) => {
1480
+ const normalized = boundedString(
1481
+ name,
1482
+ `plugin capabilities sessionMaterial[${index}]`,
1483
+ 128,
1484
+ );
1485
+ if (!materialNamePattern.test(normalized)) {
1486
+ throw new Error(
1487
+ "plugin session material names must use lowercase dotted or dashed identifiers",
1488
+ );
1489
+ }
1490
+ if (
1491
+ !portableProviderPluginSessionMaterialNames.includes(
1492
+ normalized as PortableProviderPluginSessionMaterialName,
1493
+ )
1494
+ ) {
1495
+ throw new Error(
1496
+ `plugin session material ${normalized} is unsupported by host API v1`,
1497
+ );
1498
+ }
1499
+ return normalized as PortableProviderPluginSessionMaterialName;
1500
+ });
1501
+ if (new Set(sessionMaterial).size !== sessionMaterial.length) {
1502
+ throw new Error("plugin capabilities repeat a session material name");
1503
+ }
1504
+ const sortedMaterial = [...sessionMaterial].sort();
1505
+ if (
1506
+ sortedMaterial.some((name, index) => name !== sessionMaterial[index])
1507
+ ) {
1508
+ throw new Error("plugin session material names must be sorted");
1509
+ }
1510
+ const executableSessionMaterial = new Set<
1511
+ PortableProviderPluginSessionMaterialName
1512
+ >();
1513
+ for (const binding of bindings) {
1514
+ if (
1515
+ binding.transport === "provider-api"
1516
+ && binding.authKinds.includes("oauth-token-file")
1517
+ ) {
1518
+ executableSessionMaterial.add("oauth-access-token");
1519
+ }
1520
+ if (
1521
+ binding.transport === "web-session-api"
1522
+ && binding.authKinds.some((kind) =>
1523
+ kind === "cookie-source"
1524
+ || kind === "cookies-file"
1525
+ || kind === "browser-profile"
1526
+ )
1527
+ ) {
1528
+ executableSessionMaterial.add("cookie-jar");
1529
+ }
1530
+ }
1531
+ for (const name of sortedMaterial) {
1532
+ if (!executableSessionMaterial.has(name)) {
1533
+ throw new Error(
1534
+ `plugin session material ${name} is not executable by any declared binding`,
1535
+ );
1536
+ }
1537
+ }
1538
+ return Object.freeze({
1539
+ networkOrigins: Object.freeze(sortedOrigins),
1540
+ planFiles: capabilities.planFiles,
1541
+ state: capabilities.state,
1542
+ sessionMaterial: Object.freeze(sortedMaterial),
1543
+ });
1544
+ }
1545
+
1546
+ function parseFile(value: unknown): PortableProviderPluginFileV1 {
1547
+ const file = record(value, "plugin file");
1548
+ exactKeys(file, ["path", "kind", "bytes", "sha256"], "plugin file");
1549
+ const path = safePackagePath(file.path, "plugin file path");
1550
+ if (file.kind !== "runtime" && file.kind !== "data") {
1551
+ throw new Error(`plugin file ${path} kind must be runtime or data`);
1552
+ }
1553
+ if (
1554
+ !Number.isSafeInteger(file.bytes)
1555
+ || (file.bytes as number) < 1
1556
+ || (file.bytes as number) > MAX_PLUGIN_FILE_BYTES
1557
+ ) {
1558
+ throw new Error(
1559
+ `plugin file ${path} bytes must be between 1 and ${MAX_PLUGIN_FILE_BYTES}`,
1560
+ );
1561
+ }
1562
+ const sha256 = safeSha256(file.sha256, `plugin file ${path} sha256`);
1563
+ return Object.freeze({
1564
+ path,
1565
+ kind: file.kind,
1566
+ bytes: file.bytes as number,
1567
+ sha256,
1568
+ });
1569
+ }
1570
+
1571
+ function runtimeRequireReference(node: ts.Node): boolean {
1572
+ if (ts.isIdentifier(node)) return node.text === "require";
1573
+ if (ts.isPropertyAccessExpression(node)) {
1574
+ return node.name.text === "require";
1575
+ }
1576
+ return (
1577
+ ts.isElementAccessExpression(node)
1578
+ && node.argumentExpression !== undefined
1579
+ && ts.isStringLiteralLike(node.argumentExpression)
1580
+ && node.argumentExpression.text === "require"
1581
+ );
1582
+ }
1583
+
1584
+ function assertSelfContainedPortableRuntime(
1585
+ path: string,
1586
+ bytes: Uint8Array,
1587
+ ): void {
1588
+ let source: string;
1589
+ try {
1590
+ source = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1591
+ } catch {
1592
+ throw new Error(`portable plugin runtime ${path} must be valid UTF-8`);
1593
+ }
1594
+
1595
+ let imports: readonly {
1596
+ readonly kind: string;
1597
+ readonly path: string;
1598
+ }[];
1599
+ try {
1600
+ imports = portableRuntimeImportScanner.scanImports(source);
1601
+ } catch {
1602
+ throw new Error(
1603
+ `portable plugin runtime ${path} must contain valid JavaScript`,
1604
+ );
1605
+ }
1606
+ for (const imported of imports) {
1607
+ if (imported.kind === "dynamic-import") {
1608
+ throw new Error(
1609
+ `portable plugin runtime ${path} must not use dynamic import`,
1610
+ );
1611
+ }
1612
+ if (imported.kind === "require-call") {
1613
+ throw new Error(
1614
+ `portable plugin runtime ${path} must not use require`,
1615
+ );
1616
+ }
1617
+ if (
1618
+ imported.kind !== "import-statement"
1619
+ || !allowedPortableRuntimeImports.has(imported.path)
1620
+ ) {
1621
+ throw new Error(
1622
+ `portable plugin runtime ${path} imports unsupported module ${imported.path}`,
1623
+ );
1624
+ }
1625
+ }
1626
+
1627
+ const sourceFile = ts.createSourceFile(
1628
+ path,
1629
+ source,
1630
+ ts.ScriptTarget.ESNext,
1631
+ true,
1632
+ ts.ScriptKind.JS,
1633
+ );
1634
+ let nonLiteralImport = false;
1635
+ let requireReference = false;
1636
+ const visit = (node: ts.Node): void => {
1637
+ if (nonLiteralImport || requireReference) return;
1638
+ if (
1639
+ ts.isCallExpression(node)
1640
+ && node.expression.kind === ts.SyntaxKind.ImportKeyword
1641
+ ) {
1642
+ nonLiteralImport = true;
1643
+ return;
1644
+ }
1645
+ if (runtimeRequireReference(node)) {
1646
+ requireReference = true;
1647
+ return;
1648
+ }
1649
+ ts.forEachChild(node, visit);
1650
+ };
1651
+ visit(sourceFile);
1652
+ if (nonLiteralImport) {
1653
+ throw new Error(
1654
+ `portable plugin runtime ${path} must not use dynamic import`,
1655
+ );
1656
+ }
1657
+ if (requireReference) {
1658
+ throw new Error(
1659
+ `portable plugin runtime ${path} must not use require`,
1660
+ );
1661
+ }
1662
+ }
1663
+
1664
+ function parseProvenance(
1665
+ value: unknown,
1666
+ ): PortableProviderPluginProvenanceV1 {
1667
+ const provenance = record(value, "plugin provenance");
1668
+ if (provenance.kind === "local") {
1669
+ exactKeys(provenance, ["kind"], "local plugin provenance");
1670
+ return Object.freeze({ kind: "local" });
1671
+ }
1672
+ if (provenance.kind !== "git") {
1673
+ throw new Error("plugin provenance kind must be local or git");
1674
+ }
1675
+ exactKeys(
1676
+ provenance,
1677
+ ["kind", "repository", "revision"],
1678
+ "git plugin provenance",
1679
+ );
1680
+ const repository = exactHttpsRepository(provenance.repository);
1681
+ const repositoryHostname = new URL(repository).hostname.toLowerCase();
1682
+ if (
1683
+ !repository.endsWith(".git")
1684
+ && repositoryHostname !== "github.com"
1685
+ && repositoryHostname !== "gitlab.com"
1686
+ ) {
1687
+ throw new Error(
1688
+ "plugin git provenance repository must identify an HTTPS repository",
1689
+ );
1690
+ }
1691
+ const revision = boundedString(
1692
+ provenance.revision,
1693
+ "plugin provenance revision",
1694
+ 64,
1695
+ );
1696
+ if (!gitRevisionPattern.test(revision)) {
1697
+ throw new Error(
1698
+ "plugin git provenance revision must be a full lowercase commit digest",
1699
+ );
1700
+ }
1701
+ return Object.freeze({
1702
+ kind: "git",
1703
+ repository,
1704
+ revision,
1705
+ });
1706
+ }
1707
+
1708
+ function parseManifestOrThrow(
1709
+ value: unknown,
1710
+ ): PortableProviderPluginManifestV1 {
1711
+ const manifest = record(value, "portable provider plugin manifest");
1712
+ exactKeys(
1713
+ manifest,
1714
+ [
1715
+ "schemaVersion",
1716
+ "hostApiVersion",
1717
+ "id",
1718
+ "version",
1719
+ "displayName",
1720
+ "runtime",
1721
+ "provenance",
1722
+ "capabilities",
1723
+ "bindings",
1724
+ "files",
1725
+ ],
1726
+ "portable provider plugin manifest",
1727
+ );
1728
+ if (
1729
+ manifest.schemaVersion
1730
+ !== PORTABLE_PROVIDER_PLUGIN_MANIFEST_SCHEMA_VERSION
1731
+ ) {
1732
+ throw new Error(
1733
+ `portable provider plugin schemaVersion must be ${PORTABLE_PROVIDER_PLUGIN_MANIFEST_SCHEMA_VERSION}`,
1734
+ );
1735
+ }
1736
+ if (manifest.hostApiVersion !== PORTABLE_PROVIDER_PLUGIN_HOST_API_VERSION) {
1737
+ throw new Error(
1738
+ `portable provider plugin hostApiVersion must be ${PORTABLE_PROVIDER_PLUGIN_HOST_API_VERSION}`,
1739
+ );
1740
+ }
1741
+ const id = safePluginId(manifest.id, "plugin ID");
1742
+ const version = safeVersion(manifest.version);
1743
+ const displayName = boundedString(manifest.displayName, "plugin displayName", 160);
1744
+ const runtime = record(manifest.runtime, "plugin runtime");
1745
+ exactKeys(runtime, ["kind", "entrypoint"], "plugin runtime");
1746
+ if (runtime.kind !== "bun-js") {
1747
+ throw new Error("plugin runtime kind must be bun-js");
1748
+ }
1749
+ const entrypoint = safePackagePath(
1750
+ runtime.entrypoint,
1751
+ "plugin runtime entrypoint",
1752
+ );
1753
+ if (!entrypoint.endsWith(".mjs")) {
1754
+ throw new Error("plugin runtime entrypoint must be a bundled .mjs file");
1755
+ }
1756
+ const provenance = parseProvenance(manifest.provenance);
1757
+ const bindings = boundedArray(
1758
+ manifest.bindings,
1759
+ "plugin bindings",
1760
+ MAX_BINDINGS,
1761
+ ).map(parseBinding);
1762
+ const routeKeys = bindings.map(
1763
+ (binding) => `${binding.transport}/${binding.surfaceId}`,
1764
+ );
1765
+ if (new Set(routeKeys).size !== routeKeys.length) {
1766
+ throw new Error("portable provider plugin repeats a transport route");
1767
+ }
1768
+ const adapterIds = bindings.map((binding) => binding.adapterId);
1769
+ if (new Set(adapterIds).size !== adapterIds.length) {
1770
+ throw new Error("portable provider plugin repeats an adapter ID");
1771
+ }
1772
+ const sortedBindings = [...bindings].sort((left, right) =>
1773
+ compareCanonicalText(
1774
+ `${left.transport}/${left.surfaceId}`,
1775
+ `${right.transport}/${right.surfaceId}`,
1776
+ ),
1777
+ );
1778
+ if (
1779
+ sortedBindings.some((binding, index) => binding !== bindings[index])
1780
+ ) {
1781
+ throw new Error("plugin bindings must be sorted by transport and surface");
1782
+ }
1783
+ const bindingOrigins = new Set(bindings.map((binding) => binding.origin));
1784
+ const capabilities = parseCapabilities(
1785
+ manifest.capabilities,
1786
+ bindingOrigins,
1787
+ bindings,
1788
+ );
1789
+ const files = boundedArray(
1790
+ manifest.files,
1791
+ "plugin files",
1792
+ MAX_PLUGIN_FILES,
1793
+ ).map(parseFile);
1794
+ const pathKeys = files.map((file) => file.path);
1795
+ const caseFoldedPathKeys = pathKeys.map((path) => path.toLowerCase());
1796
+ if (
1797
+ new Set(pathKeys).size !== pathKeys.length
1798
+ || new Set(caseFoldedPathKeys).size !== caseFoldedPathKeys.length
1799
+ ) {
1800
+ throw new Error("portable provider plugin repeats a file path");
1801
+ }
1802
+ const sortedFiles = [...files].sort((left, right) =>
1803
+ compareCanonicalText(left.path, right.path),
1804
+ );
1805
+ if (sortedFiles.some((file, index) => file !== files[index])) {
1806
+ throw new Error("plugin files must be sorted by path");
1807
+ }
1808
+ const payloadBytes = files.reduce((total, file) => total + file.bytes, 0);
1809
+ if (payloadBytes > MAX_PLUGIN_PAYLOAD_BYTES) {
1810
+ throw new Error(
1811
+ `plugin payload exceeds ${MAX_PLUGIN_PAYLOAD_BYTES} bytes`,
1812
+ );
1813
+ }
1814
+ const runtimeFiles = files.filter((file) => file.kind === "runtime");
1815
+ if (
1816
+ runtimeFiles.length !== 1
1817
+ || runtimeFiles[0]?.path !== entrypoint
1818
+ ) {
1819
+ throw new Error(
1820
+ "plugin package must declare exactly one runtime file, its entrypoint",
1821
+ );
1822
+ }
1823
+ return Object.freeze({
1824
+ schemaVersion: PORTABLE_PROVIDER_PLUGIN_MANIFEST_SCHEMA_VERSION,
1825
+ hostApiVersion: PORTABLE_PROVIDER_PLUGIN_HOST_API_VERSION,
1826
+ id,
1827
+ version,
1828
+ displayName,
1829
+ runtime: Object.freeze({ kind: "bun-js", entrypoint }),
1830
+ provenance,
1831
+ capabilities,
1832
+ bindings: Object.freeze(sortedBindings),
1833
+ files: Object.freeze(sortedFiles),
1834
+ });
1835
+ }
1836
+
1837
+ export function parsePortableProviderPluginManifest(
1838
+ value: unknown,
1839
+ ): PortableProviderPluginManifestResult {
1840
+ try {
1841
+ return { ok: true, value: parseManifestOrThrow(value) };
1842
+ } catch (error) {
1843
+ return {
1844
+ ok: false,
1845
+ issues: Object.freeze([
1846
+ error instanceof Error ? error.message : "invalid portable provider plugin manifest",
1847
+ ]),
1848
+ };
1849
+ }
1850
+ }
1851
+
1852
+ export function renderPortableProviderPluginManifest(
1853
+ manifest: PortableProviderPluginManifestV1,
1854
+ ): string {
1855
+ const parsed = parsePortableProviderPluginManifest(manifest);
1856
+ if (!parsed.ok) {
1857
+ throw new Error(`invalid portable provider plugin manifest: ${parsed.issues.join("; ")}`);
1858
+ }
1859
+ return `${JSON.stringify(parsed.value, null, 2)}\n`;
1860
+ }
1861
+
1862
+ function sha256(bytes: Uint8Array | string): string {
1863
+ return createHash("sha256").update(bytes).digest("hex");
1864
+ }
1865
+
1866
+ function sameRootStats(
1867
+ left: BigIntStats,
1868
+ right: BigIntStats,
1869
+ ): boolean {
1870
+ return (
1871
+ left.dev === right.dev
1872
+ && left.ino === right.ino
1873
+ && left.size === right.size
1874
+ && left.mtimeNs === right.mtimeNs
1875
+ && left.ctimeNs === right.ctimeNs
1876
+ );
1877
+ }
1878
+
1879
+ type BoundPathIdentity = {
1880
+ readonly device: string;
1881
+ readonly inode: string;
1882
+ };
1883
+
1884
+ type BoundPathDirectoryEntry = {
1885
+ readonly name: string;
1886
+ readonly kind: "file" | "directory" | "symbolic-link" | "other";
1887
+ readonly identity: BoundPathIdentity;
1888
+ readonly size: string;
1889
+ readonly mtimeNs: string;
1890
+ readonly ctimeNs: string;
1891
+ };
1892
+
1893
+ type BoundPathTreeEntry = Omit<BoundPathDirectoryEntry, "name"> & {
1894
+ readonly path: string;
1895
+ };
1896
+
1897
+ type BoundPathFileExpectation = Pick<
1898
+ BoundPathDirectoryEntry,
1899
+ "identity" | "size" | "mtimeNs" | "ctimeNs"
1900
+ >;
1901
+
1902
+ type BoundPathHelperOperation =
1903
+ | {
1904
+ readonly kind: "list-directory";
1905
+ readonly segments: readonly string[];
1906
+ readonly directoryExpectations: readonly BoundPathIdentity[];
1907
+ readonly maximumEntries: number;
1908
+ }
1909
+ | {
1910
+ readonly kind: "read-file";
1911
+ readonly segments: readonly string[];
1912
+ readonly directoryExpectations: readonly BoundPathIdentity[];
1913
+ readonly maximumBytes: number;
1914
+ readonly fileExpectation: BoundPathFileExpectation;
1915
+ }
1916
+ | {
1917
+ readonly kind: "snapshot-tree";
1918
+ readonly maximumEntries: number;
1919
+ readonly maximumDirectories: number;
1920
+ readonly maximumDepth: number;
1921
+ readonly maximumPathBytes: number;
1922
+ }
1923
+ | {
1924
+ readonly kind: "batch-read-files";
1925
+ readonly files: readonly {
1926
+ readonly segments: readonly string[];
1927
+ readonly directoryExpectations: readonly BoundPathIdentity[];
1928
+ readonly maximumBytes: number;
1929
+ readonly fileExpectation: BoundPathFileExpectation;
1930
+ }[];
1931
+ readonly maximumTotalBytes: number;
1932
+ };
1933
+
1934
+ function rootIdentity(
1935
+ stats: Pick<BigIntStats, "dev" | "ino">,
1936
+ ): BoundPathIdentity {
1937
+ return {
1938
+ device: stats.dev.toString(),
1939
+ inode: stats.ino.toString(),
1940
+ };
1941
+ }
1942
+
1943
+ function sameBoundPathIdentity(
1944
+ left: BoundPathIdentity,
1945
+ right: BoundPathIdentity,
1946
+ ): boolean {
1947
+ return left.device === right.device && left.inode === right.inode;
1948
+ }
1949
+
1950
+ function parseBoundPathIdentity(
1951
+ value: unknown,
1952
+ label: string,
1953
+ ): BoundPathIdentity {
1954
+ if (!isRecord(value)) throw new Error(`${label} must be an object`);
1955
+ exactKeys(value, ["device", "inode"], label);
1956
+ if (
1957
+ typeof value.device !== "string"
1958
+ || !/^[0-9]{1,40}$/u.test(value.device)
1959
+ || typeof value.inode !== "string"
1960
+ || !/^[0-9]{1,40}$/u.test(value.inode)
1961
+ ) throw new Error(`${label} is invalid`);
1962
+ return {
1963
+ device: value.device,
1964
+ inode: value.inode,
1965
+ };
1966
+ }
1967
+
1968
+ function parseBoundPathTreeEntry(value: unknown): BoundPathTreeEntry {
1969
+ if (!isRecord(value)) throw new Error("path helper tree entry must be an object");
1970
+ exactKeys(
1971
+ value,
1972
+ ["path", "kind", "identity", "size", "mtimeNs", "ctimeNs"],
1973
+ "path helper tree entry",
1974
+ );
1975
+ if (
1976
+ typeof value.path !== "string"
1977
+ || value.path === ""
1978
+ || value.path.startsWith("/")
1979
+ || value.path.endsWith("/")
1980
+ || value.path.includes("\\")
1981
+ || value.path.includes("\u0000")
1982
+ || Buffer.byteLength(value.path, "utf8") > 512
1983
+ || value.path.split("/").some((segment) =>
1984
+ segment === "" || segment === "." || segment === "..")
1985
+ || (
1986
+ value.kind !== "file"
1987
+ && value.kind !== "directory"
1988
+ && value.kind !== "symbolic-link"
1989
+ && value.kind !== "other"
1990
+ )
1991
+ || typeof value.size !== "string"
1992
+ || !/^[0-9]{1,40}$/u.test(value.size)
1993
+ || typeof value.mtimeNs !== "string"
1994
+ || !/^-?[0-9]{1,40}$/u.test(value.mtimeNs)
1995
+ || typeof value.ctimeNs !== "string"
1996
+ || !/^-?[0-9]{1,40}$/u.test(value.ctimeNs)
1997
+ ) throw new Error("path helper returned an invalid tree entry");
1998
+ return {
1999
+ path: value.path,
2000
+ kind: value.kind,
2001
+ identity: parseBoundPathIdentity(value.identity, "path helper tree entry identity"),
2002
+ size: value.size,
2003
+ mtimeNs: value.mtimeNs,
2004
+ ctimeNs: value.ctimeNs,
2005
+ };
2006
+ }
2007
+
2008
+ function runBoundPathHelper(
2009
+ root: string,
2010
+ expectedRoot: BoundPathIdentity,
2011
+ operation: BoundPathHelperOperation,
2012
+ ): unknown {
2013
+ pathHelperSpawnObserverForTest?.();
2014
+ const child = spawnSync(process.execPath, [
2015
+ "--no-env-file",
2016
+ "--no-install",
2017
+ "--no-macros",
2018
+ "--no-addons",
2019
+ `--config=${pathHelperConfigPath}`,
2020
+ pathHelperPath,
2021
+ ], {
2022
+ cwd: root,
2023
+ encoding: "utf8",
2024
+ env: { NODE_ENV: "production" },
2025
+ input: JSON.stringify({
2026
+ schemaVersion: 1,
2027
+ requestId: randomUUID(),
2028
+ expected: expectedRoot,
2029
+ operation,
2030
+ }),
2031
+ maxBuffer: operation.kind === "read-file"
2032
+ ? Math.max(1024 * 1024, Math.ceil(operation.maximumBytes / 3) * 4 + 64 * 1024)
2033
+ : operation.kind === "batch-read-files"
2034
+ ? Math.ceil(operation.maximumTotalBytes / 3) * 4 + 1024 * 1024
2035
+ : 8 * 1024 * 1024,
2036
+ shell: false,
2037
+ timeout: 30_000,
2038
+ windowsHide: true,
2039
+ });
2040
+ let current: BigIntStats;
2041
+ try {
2042
+ current = lstatSync(root, { bigint: true });
2043
+ } catch {
2044
+ throw new Error("portable provider plugin package root disappeared during verification");
2045
+ }
2046
+ if (
2047
+ current.isSymbolicLink()
2048
+ || !current.isDirectory()
2049
+ || !sameBoundPathIdentity(rootIdentity(current), expectedRoot)
2050
+ ) {
2051
+ throw new Error("portable provider plugin package root changed identity during verification");
2052
+ }
2053
+ if (child.error !== undefined) {
2054
+ throw new Error("bound package path helper failed to start", { cause: child.error });
2055
+ }
2056
+ if (child.status !== 0) {
2057
+ const detail = child.stderr.trim().slice(0, 512);
2058
+ throw new Error(
2059
+ detail === ""
2060
+ ? "bound package path helper rejected the operation"
2061
+ : detail,
2062
+ );
2063
+ }
2064
+ try {
2065
+ return JSON.parse(child.stdout) as unknown;
2066
+ } catch (error) {
2067
+ throw new Error("bound package path helper returned invalid JSON", { cause: error });
2068
+ }
2069
+ }
2070
+
2071
+ function snapshotBoundPackageTree(
2072
+ root: string,
2073
+ expectedRoot: BoundPathIdentity,
2074
+ ): readonly BoundPathTreeEntry[] {
2075
+ const response = runBoundPathHelper(root, expectedRoot, {
2076
+ kind: "snapshot-tree",
2077
+ maximumEntries: MAX_PLUGIN_PACKAGE_ENTRIES,
2078
+ maximumDirectories: MAX_PLUGIN_DIRECTORIES,
2079
+ maximumDepth: 15,
2080
+ maximumPathBytes: 512,
2081
+ });
2082
+ if (!isRecord(response)) throw new Error("path helper tree response must be an object");
2083
+ exactKeys(
2084
+ response,
2085
+ ["ok", "identity", "treeEntries"],
2086
+ "path helper tree response",
2087
+ );
2088
+ if (response.ok !== true || !Array.isArray(response.treeEntries)) {
2089
+ throw new Error("path helper returned a malformed tree response");
2090
+ }
2091
+ const identity = parseBoundPathIdentity(response.identity, "path helper root identity");
2092
+ if (!sameBoundPathIdentity(identity, expectedRoot)) {
2093
+ throw new Error("path helper tree response came from the wrong package root");
2094
+ }
2095
+ if (response.treeEntries.length > MAX_PLUGIN_PACKAGE_ENTRIES) {
2096
+ throw new Error("path helper tree response exceeds its entry bound");
2097
+ }
2098
+ const entries = response.treeEntries.map(parseBoundPathTreeEntry);
2099
+ if (
2100
+ entries.some((entry, index) =>
2101
+ index > 0
2102
+ && compareCanonicalText(entries[index - 1]!.path, entry.path) >= 0)
2103
+ ) {
2104
+ throw new Error("path helper tree response is not strictly sorted");
2105
+ }
2106
+ return Object.freeze(entries);
2107
+ }
2108
+
2109
+ function readBoundPackageFile(
2110
+ root: string,
2111
+ expectedRoot: BoundPathIdentity,
2112
+ relativePath: string,
2113
+ directoryExpectations: readonly BoundPathIdentity[],
2114
+ fileExpectation: BoundPathFileExpectation,
2115
+ maximumBytes: number,
2116
+ label: string,
2117
+ ): Buffer {
2118
+ let response: unknown;
2119
+ try {
2120
+ response = runBoundPathHelper(root, expectedRoot, {
2121
+ kind: "read-file",
2122
+ segments: relativePath.split("/"),
2123
+ directoryExpectations,
2124
+ fileExpectation: {
2125
+ identity: fileExpectation.identity,
2126
+ size: fileExpectation.size,
2127
+ mtimeNs: fileExpectation.mtimeNs,
2128
+ ctimeNs: fileExpectation.ctimeNs,
2129
+ },
2130
+ maximumBytes,
2131
+ });
2132
+ } catch (error) {
2133
+ throw new Error(`${label} could not be safely read`, { cause: error });
2134
+ }
2135
+ if (!isRecord(response)) throw new Error("path helper read response must be an object");
2136
+ exactKeys(
2137
+ response,
2138
+ ["ok", "identity", "contentBase64"],
2139
+ "path helper read response",
2140
+ );
2141
+ if (response.ok !== true || typeof response.contentBase64 !== "string") {
2142
+ throw new Error("path helper returned a malformed read response");
2143
+ }
2144
+ const identity = parseBoundPathIdentity(response.identity, "path helper root identity");
2145
+ if (!sameBoundPathIdentity(identity, expectedRoot)) {
2146
+ throw new Error("path helper read response came from the wrong package root");
2147
+ }
2148
+ const maximumEncodedBytes = Math.ceil(maximumBytes / 3) * 4;
2149
+ if (
2150
+ response.contentBase64.length > maximumEncodedBytes
2151
+ || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(
2152
+ response.contentBase64,
2153
+ )
2154
+ ) throw new Error("path helper returned malformed bounded file content");
2155
+ const bytes = Buffer.from(response.contentBase64, "base64");
2156
+ if (
2157
+ bytes.byteLength < 1
2158
+ || bytes.byteLength > maximumBytes
2159
+ || bytes.toString("base64") !== response.contentBase64
2160
+ ) {
2161
+ throw new Error(
2162
+ `${label} must be a regular file between 1 and ${maximumBytes} bytes`,
2163
+ );
2164
+ }
2165
+ return bytes;
2166
+ }
2167
+
2168
+ function readBoundPackageFiles(
2169
+ root: string,
2170
+ expectedRoot: BoundPathIdentity,
2171
+ files: readonly {
2172
+ readonly path: string;
2173
+ readonly directoryExpectations: readonly BoundPathIdentity[];
2174
+ readonly fileExpectation: BoundPathFileExpectation;
2175
+ readonly maximumBytes: number;
2176
+ }[],
2177
+ maximumTotalBytes: number,
2178
+ ): readonly Buffer[] {
2179
+ const response = runBoundPathHelper(root, expectedRoot, {
2180
+ kind: "batch-read-files",
2181
+ files: files.map((file) => ({
2182
+ segments: file.path.split("/"),
2183
+ directoryExpectations: file.directoryExpectations,
2184
+ maximumBytes: file.maximumBytes,
2185
+ fileExpectation: {
2186
+ identity: file.fileExpectation.identity,
2187
+ size: file.fileExpectation.size,
2188
+ mtimeNs: file.fileExpectation.mtimeNs,
2189
+ ctimeNs: file.fileExpectation.ctimeNs,
2190
+ },
2191
+ })),
2192
+ maximumTotalBytes,
2193
+ });
2194
+ if (!isRecord(response)) throw new Error("path helper batch read response must be an object");
2195
+ exactKeys(
2196
+ response,
2197
+ ["ok", "identity", "fileContentsBase64"],
2198
+ "path helper batch read response",
2199
+ );
2200
+ if (
2201
+ response.ok !== true
2202
+ || !Array.isArray(response.fileContentsBase64)
2203
+ || response.fileContentsBase64.length !== files.length
2204
+ ) throw new Error("path helper returned a malformed batch read response");
2205
+ const identity = parseBoundPathIdentity(response.identity, "path helper root identity");
2206
+ if (!sameBoundPathIdentity(identity, expectedRoot)) {
2207
+ throw new Error("path helper batch read response came from the wrong package root");
2208
+ }
2209
+ let totalBytes = 0;
2210
+ return Object.freeze(response.fileContentsBase64.map((encoded, index) => {
2211
+ const file = files[index];
2212
+ if (file === undefined || typeof encoded !== "string") {
2213
+ throw new Error("path helper returned malformed batch file content");
2214
+ }
2215
+ const maximumEncodedBytes = Math.ceil(file.maximumBytes / 3) * 4;
2216
+ if (
2217
+ encoded.length > maximumEncodedBytes
2218
+ || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(
2219
+ encoded,
2220
+ )
2221
+ ) throw new Error("path helper returned malformed bounded batch file content");
2222
+ const bytes = Buffer.from(encoded, "base64");
2223
+ totalBytes += bytes.byteLength;
2224
+ if (
2225
+ bytes.byteLength < 1
2226
+ || bytes.byteLength > file.maximumBytes
2227
+ || totalBytes > maximumTotalBytes
2228
+ || bytes.toString("base64") !== encoded
2229
+ ) throw new Error("path helper batch file content violates its byte bound");
2230
+ return bytes;
2231
+ }));
2232
+ }
2233
+
2234
+ type WalkedPackageEntry = BoundPathTreeEntry & {
2235
+ readonly directoryExpectations: readonly BoundPathIdentity[];
2236
+ };
2237
+
2238
+ type WalkedPackage = {
2239
+ readonly files: readonly string[];
2240
+ readonly directories: readonly string[];
2241
+ readonly entries: readonly WalkedPackageEntry[];
2242
+ readonly entryByPath: ReadonlyMap<string, WalkedPackageEntry>;
2243
+ };
2244
+
2245
+ function walkPackage(
2246
+ root: string,
2247
+ expectedRoot: BoundPathIdentity,
2248
+ ): WalkedPackage {
2249
+ const files: string[] = [];
2250
+ const directories: string[] = [];
2251
+ const entries: WalkedPackageEntry[] = [];
2252
+ const entryByPath = new Map<string, WalkedPackageEntry>();
2253
+ const treeEntries = snapshotBoundPackageTree(root, expectedRoot);
2254
+ for (const entry of treeEntries) {
2255
+ if (entryByPath.has(entry.path)) {
2256
+ throw new Error(
2257
+ `portable provider plugin package contains duplicate entry ${entry.path}`,
2258
+ );
2259
+ }
2260
+ const segments = entry.path.split("/");
2261
+ const directoryExpectations: BoundPathIdentity[] = [];
2262
+ for (let index = 1; index < segments.length; index += 1) {
2263
+ const parentPath = segments.slice(0, index).join("/");
2264
+ const parent = entryByPath.get(parentPath);
2265
+ if (parent === undefined || parent.kind !== "directory") {
2266
+ throw new Error(
2267
+ `portable provider plugin tree omits directory ${parentPath}`,
2268
+ );
2269
+ }
2270
+ directoryExpectations.push(parent.identity);
2271
+ }
2272
+ if (entry.kind === "symbolic-link") {
2273
+ throw new Error(
2274
+ `portable provider plugin package contains symlink ${entry.path}`,
2275
+ );
2276
+ }
2277
+ const walkedEntry = Object.freeze({
2278
+ ...entry,
2279
+ directoryExpectations: Object.freeze(directoryExpectations),
2280
+ });
2281
+ entries.push(walkedEntry);
2282
+ entryByPath.set(entry.path, walkedEntry);
2283
+ if (entry.kind === "directory") {
2284
+ safePackagePath(`${entry.path}/placeholder`, "plugin directory path");
2285
+ directories.push(entry.path);
2286
+ if (directories.length > MAX_PLUGIN_DIRECTORIES) {
2287
+ throw new Error(
2288
+ `portable provider plugin package exceeds ${MAX_PLUGIN_DIRECTORIES} directories`,
2289
+ );
2290
+ }
2291
+ continue;
2292
+ }
2293
+ if (entry.kind !== "file") {
2294
+ throw new Error(
2295
+ `portable provider plugin package contains unsupported entry ${entry.path}`,
2296
+ );
2297
+ }
2298
+ if (!portableProviderPluginManifestNames.has(entry.path)) {
2299
+ safePackagePath(entry.path, "plugin package file path");
2300
+ }
2301
+ files.push(entry.path);
2302
+ if (files.length > MAX_PLUGIN_FILES + 1) {
2303
+ throw new Error(
2304
+ `portable provider plugin package exceeds ${MAX_PLUGIN_FILES} declared files`,
2305
+ );
2306
+ }
2307
+ }
2308
+ return {
2309
+ files: Object.freeze(files.sort(compareCanonicalText)),
2310
+ directories: Object.freeze(directories.sort(compareCanonicalText)),
2311
+ entries: Object.freeze(entries),
2312
+ entryByPath,
2313
+ };
2314
+ }
2315
+
2316
+ function sameWalkedPackage(
2317
+ left: WalkedPackage,
2318
+ right: WalkedPackage,
2319
+ ): boolean {
2320
+ return (
2321
+ left.entries.length === right.entries.length
2322
+ && left.entries.every((entry, index) => {
2323
+ const candidate = right.entries[index];
2324
+ return candidate !== undefined
2325
+ && entry.path === candidate.path
2326
+ && entry.kind === candidate.kind
2327
+ && sameBoundPathIdentity(entry.identity, candidate.identity)
2328
+ && entry.size === candidate.size
2329
+ && entry.mtimeNs === candidate.mtimeNs
2330
+ && entry.ctimeNs === candidate.ctimeNs;
2331
+ })
2332
+ );
2333
+ }
2334
+
2335
+ function expectedDirectories(paths: readonly string[]): readonly string[] {
2336
+ const expected = new Set<string>();
2337
+ for (const path of paths) {
2338
+ const segments = path.split("/");
2339
+ for (let index = 1; index < segments.length; index += 1) {
2340
+ expected.add(segments.slice(0, index).join("/"));
2341
+ }
2342
+ }
2343
+ return [...expected].sort();
2344
+ }
2345
+
2346
+ export function verifyPortableProviderPluginPackageDirectory(
2347
+ path: string,
2348
+ ): VerifiedPortableProviderPluginPackage {
2349
+ const root = resolve(path);
2350
+ let rootStats: BigIntStats;
2351
+ try {
2352
+ rootStats = lstatSync(root, { bigint: true });
2353
+ } catch {
2354
+ throw new Error("portable provider plugin package directory does not exist");
2355
+ }
2356
+ if (!rootStats.isDirectory() || rootStats.isSymbolicLink()) {
2357
+ throw new Error(
2358
+ "portable provider plugin package root must be a regular non-symlink directory",
2359
+ );
2360
+ }
2361
+ const expectedRoot = rootIdentity(rootStats);
2362
+ const walked = walkPackage(root, expectedRoot);
2363
+ const manifestNames = walked.files.filter((file) =>
2364
+ portableProviderPluginManifestNames.has(file)
2365
+ );
2366
+ if (manifestNames.length === 0) {
2367
+ throw new Error(
2368
+ `portable provider plugin package is missing ${PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME}`,
2369
+ );
2370
+ }
2371
+ if (manifestNames.length > 1) {
2372
+ throw new Error(
2373
+ `portable provider plugin package must contain exactly one of ${[...portableProviderPluginManifestNames].join(" or ")}`,
2374
+ );
2375
+ }
2376
+ const manifestName = manifestNames[0];
2377
+ if (manifestName === undefined) {
2378
+ throw new Error("portable provider plugin package manifest selection failed");
2379
+ }
2380
+ const manifestEntry = walked.entryByPath.get(
2381
+ manifestName,
2382
+ );
2383
+ if (manifestEntry === undefined || manifestEntry.kind !== "file") {
2384
+ throw new Error(
2385
+ `portable provider plugin package is missing ${manifestName}`,
2386
+ );
2387
+ }
2388
+ const manifestBytes = readBoundPackageFile(
2389
+ root,
2390
+ expectedRoot,
2391
+ manifestName,
2392
+ manifestEntry.directoryExpectations,
2393
+ manifestEntry,
2394
+ MAX_MANIFEST_BYTES,
2395
+ "portable provider plugin manifest",
2396
+ );
2397
+ let manifestValue: unknown;
2398
+ try {
2399
+ manifestValue = JSON.parse(manifestBytes.toString("utf8")) as unknown;
2400
+ } catch {
2401
+ throw new Error("portable provider plugin manifest must contain valid UTF-8 JSON");
2402
+ }
2403
+ const parsed = parsePortableProviderPluginManifest(manifestValue);
2404
+ if (!parsed.ok) {
2405
+ throw new Error(
2406
+ `invalid portable provider plugin manifest: ${parsed.issues.join("; ")}`,
2407
+ );
2408
+ }
2409
+ const canonicalManifest = renderPortableProviderPluginManifest(parsed.value);
2410
+ if (!manifestBytes.equals(Buffer.from(canonicalManifest, "utf8"))) {
2411
+ throw new Error(
2412
+ "portable provider plugin manifest must use canonical rendered JSON",
2413
+ );
2414
+ }
2415
+ const expectedFiles = [
2416
+ manifestName,
2417
+ ...parsed.value.files.map((file) => file.path),
2418
+ ].sort();
2419
+ const actualFiles = [...walked.files].sort();
2420
+ if (
2421
+ expectedFiles.length !== actualFiles.length
2422
+ || expectedFiles.some((file, index) => file !== actualFiles[index])
2423
+ ) {
2424
+ throw new Error(
2425
+ "portable provider plugin package files differ from its exact manifest",
2426
+ );
2427
+ }
2428
+ const declaredDirectories = expectedDirectories(
2429
+ parsed.value.files.map((file) => file.path),
2430
+ );
2431
+ const actualDirectories = [...walked.directories].sort();
2432
+ if (
2433
+ declaredDirectories.length !== actualDirectories.length
2434
+ || declaredDirectories.some(
2435
+ (directory, index) => directory !== actualDirectories[index],
2436
+ )
2437
+ ) {
2438
+ throw new Error(
2439
+ "portable provider plugin package contains an undeclared or empty directory",
2440
+ );
2441
+ }
2442
+ const walkedFiles = parsed.value.files.map((file) => {
2443
+ const walkedEntry = walked.entryByPath.get(file.path);
2444
+ if (walkedEntry === undefined || walkedEntry.kind !== "file") {
2445
+ throw new Error(
2446
+ `portable provider plugin package no longer contains file ${file.path}`,
2447
+ );
2448
+ }
2449
+ return walkedEntry;
2450
+ });
2451
+ const payloadFileBytes = readBoundPackageFiles(
2452
+ root,
2453
+ expectedRoot,
2454
+ walkedFiles.map((walkedEntry) => ({
2455
+ path: walkedEntry.path,
2456
+ directoryExpectations: walkedEntry.directoryExpectations,
2457
+ fileExpectation: walkedEntry,
2458
+ maximumBytes: MAX_PLUGIN_FILE_BYTES,
2459
+ })),
2460
+ MAX_PLUGIN_PAYLOAD_BYTES,
2461
+ );
2462
+ let payloadBytes = 0;
2463
+ const files = parsed.value.files.map((file, index) => {
2464
+ const bytes = payloadFileBytes[index];
2465
+ if (bytes === undefined) {
2466
+ throw new Error(`portable provider plugin file ${file.path} was not read`);
2467
+ }
2468
+ payloadBytes += bytes.byteLength;
2469
+ if (bytes.byteLength !== file.bytes) {
2470
+ throw new Error(
2471
+ `portable provider plugin file ${file.path} byte length does not match its manifest`,
2472
+ );
2473
+ }
2474
+ const digest = sha256(bytes);
2475
+ if (digest !== file.sha256) {
2476
+ throw new Error(
2477
+ `portable provider plugin file ${file.path} digest does not match its manifest`,
2478
+ );
2479
+ }
2480
+ return Object.freeze({
2481
+ path: file.path,
2482
+ kind: file.kind,
2483
+ bytes,
2484
+ sha256: digest,
2485
+ });
2486
+ });
2487
+ const runtimeFile = files.find((file) => file.kind === "runtime");
2488
+ if (runtimeFile === undefined) {
2489
+ throw new Error("portable provider plugin runtime file was not read");
2490
+ }
2491
+ assertSelfContainedPortableRuntime(runtimeFile.path, runtimeFile.bytes);
2492
+ if (payloadBytes > MAX_PLUGIN_PAYLOAD_BYTES) {
2493
+ throw new Error(
2494
+ `portable provider plugin payload exceeds ${MAX_PLUGIN_PAYLOAD_BYTES} bytes`,
2495
+ );
2496
+ }
2497
+ const rootAfter = lstatSync(root, { bigint: true });
2498
+ const walkedAfter = walkPackage(root, expectedRoot);
2499
+ if (
2500
+ !sameRootStats(rootStats, rootAfter)
2501
+ || !sameWalkedPackage(walked, walkedAfter)
2502
+ ) {
2503
+ throw new Error(
2504
+ "portable provider plugin package changed while it was being verified",
2505
+ );
2506
+ }
2507
+ const bundleHash = createHash("sha256")
2508
+ .update("io-portable-provider-plugin-v1\0")
2509
+ .update(canonicalManifest);
2510
+ for (const file of files) {
2511
+ bundleHash
2512
+ .update("\0")
2513
+ .update(file.path)
2514
+ .update("\0")
2515
+ .update(file.kind)
2516
+ .update("\0")
2517
+ .update(file.sha256);
2518
+ }
2519
+ const verified = Object.freeze({
2520
+ root,
2521
+ manifest: parsed.value,
2522
+ manifestBytes: Buffer.from(manifestBytes),
2523
+ manifestSha256: sha256(manifestBytes),
2524
+ bundleSha256: bundleHash.digest("hex"),
2525
+ payloadBytes,
2526
+ files: Object.freeze(files),
2527
+ });
2528
+ verifiedPortableProviderPluginPackages.add(verified);
2529
+ return verified;
2530
+ }