@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,2155 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ chmodSync,
4
+ closeSync,
5
+ constants,
6
+ existsSync,
7
+ fchmodSync,
8
+ fstatSync,
9
+ fsyncSync,
10
+ linkSync,
11
+ lstatSync,
12
+ mkdirSync,
13
+ openSync,
14
+ readFileSync,
15
+ readdirSync,
16
+ realpathSync,
17
+ renameSync,
18
+ rmSync,
19
+ unlinkSync,
20
+ writeFileSync,
21
+ } from "node:fs";
22
+ import { basename, dirname, join, resolve } from "node:path";
23
+
24
+ import { canonicalJson } from "./canonical-json";
25
+ import {
26
+ PORTABLE_PROVIDER_PLUGIN_HOST_API_VERSION,
27
+ PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME,
28
+ isPortableProviderPluginVersion,
29
+ verifyPortableProviderPluginPackageDirectory,
30
+ type PortableProviderPluginManifestV1,
31
+ type VerifiedPortableProviderPluginPackage,
32
+ } from "./provider-plugin-package";
33
+ import {
34
+ currentProcessStartIdentity,
35
+ processOwnerStatus,
36
+ type ProcessOwnerIdentity,
37
+ } from "./process-identity";
38
+ import { ensurePrivateDirectory } from "./storage";
39
+
40
+ export const PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION = 1 as const;
41
+
42
+ export type PortableProviderPluginTrustApprovalV1 = {
43
+ readonly decision: "trust-executable-code";
44
+ readonly pluginId: string;
45
+ readonly pluginVersion: string;
46
+ readonly bundleSha256: string;
47
+ };
48
+
49
+ export type PortableProviderPluginTrustRecordV1 = {
50
+ readonly schemaVersion:
51
+ typeof PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION;
52
+ readonly decision: "trust-executable-code";
53
+ readonly pluginId: string;
54
+ readonly pluginVersion: string;
55
+ readonly hostApiVersion: typeof PORTABLE_PROVIDER_PLUGIN_HOST_API_VERSION;
56
+ readonly bundleSha256: string;
57
+ readonly manifestSha256: string;
58
+ readonly provenance: PortableProviderPluginManifestV1["provenance"];
59
+ readonly trustedAt: string;
60
+ };
61
+
62
+ type PortableProviderPluginActivationBaseV1 = {
63
+ readonly schemaVersion:
64
+ typeof PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION;
65
+ readonly pluginId: string;
66
+ readonly pluginVersion: string;
67
+ readonly bundleSha256: string;
68
+ readonly activatedAt: string;
69
+ };
70
+
71
+ export type PortableProviderPluginActiveRecordV1 =
72
+ | PortableProviderPluginActivationBaseV1 & {
73
+ readonly status: "enabled";
74
+ }
75
+ | PortableProviderPluginActivationBaseV1 & {
76
+ readonly status: "disabled";
77
+ readonly disabledAt: string;
78
+ };
79
+
80
+ type PortableProviderPluginLockRecordV1 = ProcessOwnerIdentity & {
81
+ readonly schemaVersion:
82
+ typeof PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION;
83
+ readonly pluginId: string;
84
+ readonly acquiredAt: string;
85
+ };
86
+
87
+ type PortableProviderPluginCatalogLockRecordV1 = ProcessOwnerIdentity & {
88
+ readonly schemaVersion:
89
+ typeof PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION;
90
+ readonly scope: "catalog-mutation";
91
+ readonly acquiredAt: string;
92
+ };
93
+
94
+ type PortableProviderPluginLockClaimPhase =
95
+ | "waiting"
96
+ | "candidate"
97
+ | "held";
98
+
99
+ type PortableProviderPluginLockClaimV1 = ProcessOwnerIdentity & {
100
+ readonly schemaVersion:
101
+ typeof PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION;
102
+ readonly kind: "portable-provider-plugin-lock-claim";
103
+ readonly targetSha256: string;
104
+ readonly claimId: string;
105
+ };
106
+
107
+ export type PortableProviderPluginAssertActivatable = (
108
+ verifiedPackage: VerifiedPortableProviderPluginPackage,
109
+ ) => void;
110
+
111
+ export type PortableProviderPluginAssertQuiescent = (
112
+ bundleSha256: string,
113
+ artifactPath: string,
114
+ ) => void;
115
+
116
+ export type InstalledPortableProviderPlugin = {
117
+ readonly artifactPath: string;
118
+ readonly package: VerifiedPortableProviderPluginPackage;
119
+ readonly trust: PortableProviderPluginTrustRecordV1;
120
+ readonly active: PortableProviderPluginActiveRecordV1;
121
+ };
122
+
123
+ export type PortableProviderPluginStorePaths = {
124
+ readonly root: string;
125
+ readonly artifacts: string;
126
+ readonly trust: string;
127
+ readonly active: string;
128
+ readonly locks: string;
129
+ };
130
+
131
+ const MAX_STORE_RECORD_BYTES = 64 * 1024;
132
+ const MAX_LOCK_CLAIMS = 1_024;
133
+ const MAX_LOCK_PUBLICATIONS = 1_024;
134
+ const TEST_BARRIER_TIMEOUT_MS = 90_000;
135
+ const CATALOG_MUTATION_LOCK_NAME = ".catalog-mutation.lock";
136
+ const lockClaimIdPattern =
137
+ /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
138
+ const lockPublicationPrefix = ".lock-publication-";
139
+ const lockPublicationNamePattern =
140
+ /^\.lock-publication-([1-9][0-9]*)-([a-f0-9]{64})-([a-f0-9]{64})-([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\.tmp$/u;
141
+ const pluginIdPattern = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u;
142
+ const sha256Pattern = /^[a-f0-9]{64}$/u;
143
+ const activeCatalogMutationLocks = new Set<string>();
144
+ const activePluginMutations = new Set<string>();
145
+
146
+ function hasCode(error: unknown, code: string): boolean {
147
+ return (
148
+ typeof error === "object"
149
+ && error !== null
150
+ && "code" in error
151
+ && error.code === code
152
+ );
153
+ }
154
+
155
+ function isRecord(value: unknown): value is Record<string, unknown> {
156
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
157
+ return false;
158
+ }
159
+ const prototype = Object.getPrototypeOf(value) as unknown;
160
+ if (prototype !== Object.prototype && prototype !== null) return false;
161
+ return Reflect.ownKeys(value).every((key) => {
162
+ if (typeof key !== "string") return false;
163
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
164
+ return descriptor !== undefined
165
+ && descriptor.enumerable
166
+ && "value" in descriptor;
167
+ });
168
+ }
169
+
170
+ function record(value: unknown, label: string): Record<string, unknown> {
171
+ if (!isRecord(value)) throw new Error(`${label} must be an object`);
172
+ return value;
173
+ }
174
+
175
+ function exactKeys(
176
+ value: Readonly<Record<string, unknown>>,
177
+ expected: readonly string[],
178
+ label: string,
179
+ ): void {
180
+ const actual = Object.keys(value).sort();
181
+ const wanted = [...expected].sort();
182
+ if (
183
+ actual.length !== wanted.length
184
+ || actual.some((key, index) => key !== wanted[index])
185
+ ) {
186
+ throw new Error(`${label} must contain exactly: ${wanted.join(", ")}`);
187
+ }
188
+ }
189
+
190
+ function pluginId(value: unknown, label: string): string {
191
+ if (typeof value !== "string" || !pluginIdPattern.test(value)) {
192
+ throw new Error(`${label} must be strict lowercase kebab-case`);
193
+ }
194
+ return value;
195
+ }
196
+
197
+ function pluginVersion(value: unknown, label: string): string {
198
+ if (
199
+ typeof value !== "string"
200
+ || value.length > 128
201
+ || !isPortableProviderPluginVersion(value)
202
+ ) {
203
+ throw new Error(`${label} must be strict semantic version text`);
204
+ }
205
+ return value;
206
+ }
207
+
208
+ function sha256(value: unknown, label: string): string {
209
+ if (typeof value !== "string" || !sha256Pattern.test(value)) {
210
+ throw new Error(`${label} must be a lowercase SHA-256 digest`);
211
+ }
212
+ return value;
213
+ }
214
+
215
+ function timestamp(value: unknown, label: string): string {
216
+ if (
217
+ typeof value !== "string"
218
+ || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(value)
219
+ || new Date(value).toISOString() !== value
220
+ ) {
221
+ throw new Error(`${label} must be one canonical UTC timestamp`);
222
+ }
223
+ return value;
224
+ }
225
+
226
+ function parseProvenance(
227
+ value: unknown,
228
+ ): PortableProviderPluginManifestV1["provenance"] {
229
+ const parsed = record(value, "plugin trust provenance");
230
+ if (parsed.kind === "local") {
231
+ exactKeys(parsed, ["kind"], "local plugin trust provenance");
232
+ return Object.freeze({ kind: "local" });
233
+ }
234
+ if (parsed.kind === "git") {
235
+ exactKeys(
236
+ parsed,
237
+ ["kind", "repository", "revision"],
238
+ "git plugin trust provenance",
239
+ );
240
+ if (
241
+ typeof parsed.repository !== "string"
242
+ || !parsed.repository.startsWith("https://")
243
+ || typeof parsed.revision !== "string"
244
+ || !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(parsed.revision)
245
+ ) {
246
+ throw new Error("git plugin trust provenance is invalid");
247
+ }
248
+ return Object.freeze({
249
+ kind: "git",
250
+ repository: parsed.repository as `https://${string}`,
251
+ revision: parsed.revision,
252
+ });
253
+ }
254
+ throw new Error("plugin trust provenance kind is unsupported");
255
+ }
256
+
257
+ export function parsePortableProviderPluginTrustApproval(
258
+ value: unknown,
259
+ ): PortableProviderPluginTrustApprovalV1 {
260
+ const parsed = record(value, "portable plugin trust approval");
261
+ exactKeys(
262
+ parsed,
263
+ ["bundleSha256", "decision", "pluginId", "pluginVersion"],
264
+ "portable plugin trust approval",
265
+ );
266
+ if (parsed.decision !== "trust-executable-code") {
267
+ throw new Error(
268
+ "portable plugin trust approval must explicitly trust executable code",
269
+ );
270
+ }
271
+ return Object.freeze({
272
+ decision: parsed.decision,
273
+ pluginId: pluginId(parsed.pluginId, "approved plugin ID"),
274
+ pluginVersion: pluginVersion(
275
+ parsed.pluginVersion,
276
+ "approved plugin version",
277
+ ),
278
+ bundleSha256: sha256(
279
+ parsed.bundleSha256,
280
+ "approved plugin bundle digest",
281
+ ),
282
+ });
283
+ }
284
+
285
+ function parseTrustRecord(value: unknown): PortableProviderPluginTrustRecordV1 {
286
+ const parsed = record(value, "portable plugin trust record");
287
+ exactKeys(
288
+ parsed,
289
+ [
290
+ "bundleSha256",
291
+ "decision",
292
+ "hostApiVersion",
293
+ "manifestSha256",
294
+ "pluginId",
295
+ "pluginVersion",
296
+ "provenance",
297
+ "schemaVersion",
298
+ "trustedAt",
299
+ ],
300
+ "portable plugin trust record",
301
+ );
302
+ if (
303
+ parsed.schemaVersion !== PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION
304
+ || parsed.hostApiVersion !== PORTABLE_PROVIDER_PLUGIN_HOST_API_VERSION
305
+ || parsed.decision !== "trust-executable-code"
306
+ ) {
307
+ throw new Error("portable plugin trust record version or decision is invalid");
308
+ }
309
+ return Object.freeze({
310
+ schemaVersion: parsed.schemaVersion,
311
+ decision: parsed.decision,
312
+ pluginId: pluginId(parsed.pluginId, "trusted plugin ID"),
313
+ pluginVersion: pluginVersion(
314
+ parsed.pluginVersion,
315
+ "trusted plugin version",
316
+ ),
317
+ hostApiVersion: parsed.hostApiVersion,
318
+ bundleSha256: sha256(
319
+ parsed.bundleSha256,
320
+ "trusted plugin bundle digest",
321
+ ),
322
+ manifestSha256: sha256(
323
+ parsed.manifestSha256,
324
+ "trusted plugin manifest digest",
325
+ ),
326
+ provenance: parseProvenance(parsed.provenance),
327
+ trustedAt: timestamp(parsed.trustedAt, "plugin trust time"),
328
+ });
329
+ }
330
+
331
+ function parseActiveRecord(value: unknown): PortableProviderPluginActiveRecordV1 {
332
+ const parsed = record(value, "portable plugin active record");
333
+ const disabled = parsed.status === "disabled";
334
+ exactKeys(
335
+ parsed,
336
+ [
337
+ "activatedAt",
338
+ "bundleSha256",
339
+ ...(disabled ? ["disabledAt"] : []),
340
+ "pluginId",
341
+ "pluginVersion",
342
+ "schemaVersion",
343
+ "status",
344
+ ],
345
+ "portable plugin active record",
346
+ );
347
+ if (
348
+ parsed.schemaVersion !== PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION
349
+ || (parsed.status !== "enabled" && parsed.status !== "disabled")
350
+ ) {
351
+ throw new Error("portable plugin active record version is invalid");
352
+ }
353
+ const base = {
354
+ schemaVersion: parsed.schemaVersion,
355
+ pluginId: pluginId(parsed.pluginId, "active plugin ID"),
356
+ pluginVersion: pluginVersion(
357
+ parsed.pluginVersion,
358
+ "active plugin version",
359
+ ),
360
+ bundleSha256: sha256(
361
+ parsed.bundleSha256,
362
+ "active plugin bundle digest",
363
+ ),
364
+ activatedAt: timestamp(parsed.activatedAt, "plugin activation time"),
365
+ } as const;
366
+ return parsed.status === "enabled"
367
+ ? Object.freeze({ ...base, status: parsed.status })
368
+ : Object.freeze({
369
+ ...base,
370
+ status: parsed.status,
371
+ disabledAt: timestamp(
372
+ parsed.disabledAt,
373
+ "plugin disable time",
374
+ ),
375
+ });
376
+ }
377
+
378
+ function parseLockRecord(value: unknown): PortableProviderPluginLockRecordV1 {
379
+ const parsed = record(value, "portable plugin activation lock");
380
+ exactKeys(
381
+ parsed,
382
+ [
383
+ "acquiredAt",
384
+ "bootId",
385
+ "pid",
386
+ "pluginId",
387
+ "processStartId",
388
+ "schemaVersion",
389
+ ],
390
+ "portable plugin activation lock",
391
+ );
392
+ if (
393
+ parsed.schemaVersion !== PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION
394
+ || !Number.isSafeInteger(parsed.pid)
395
+ || typeof parsed.pid !== "number"
396
+ || parsed.pid < 1
397
+ || typeof parsed.bootId !== "string"
398
+ || !sha256Pattern.test(parsed.bootId)
399
+ || typeof parsed.processStartId !== "string"
400
+ || !sha256Pattern.test(parsed.processStartId)
401
+ ) {
402
+ throw new Error("portable plugin activation lock is invalid");
403
+ }
404
+ return Object.freeze({
405
+ schemaVersion: parsed.schemaVersion,
406
+ pluginId: pluginId(parsed.pluginId, "locked plugin ID"),
407
+ acquiredAt: timestamp(parsed.acquiredAt, "plugin lock acquisition time"),
408
+ pid: parsed.pid,
409
+ bootId: parsed.bootId,
410
+ processStartId: parsed.processStartId,
411
+ });
412
+ }
413
+
414
+ function parseCatalogLockRecord(
415
+ value: unknown,
416
+ ): PortableProviderPluginCatalogLockRecordV1 {
417
+ const parsed = record(value, "portable plugin catalog mutation lock");
418
+ exactKeys(
419
+ parsed,
420
+ [
421
+ "acquiredAt",
422
+ "bootId",
423
+ "pid",
424
+ "processStartId",
425
+ "schemaVersion",
426
+ "scope",
427
+ ],
428
+ "portable plugin catalog mutation lock",
429
+ );
430
+ if (
431
+ parsed.schemaVersion !== PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION
432
+ || parsed.scope !== "catalog-mutation"
433
+ || !Number.isSafeInteger(parsed.pid)
434
+ || typeof parsed.pid !== "number"
435
+ || parsed.pid < 1
436
+ || typeof parsed.bootId !== "string"
437
+ || !sha256Pattern.test(parsed.bootId)
438
+ || typeof parsed.processStartId !== "string"
439
+ || !sha256Pattern.test(parsed.processStartId)
440
+ ) {
441
+ throw new Error("portable plugin catalog mutation lock is invalid");
442
+ }
443
+ return Object.freeze({
444
+ schemaVersion: parsed.schemaVersion,
445
+ scope: parsed.scope,
446
+ acquiredAt: timestamp(
447
+ parsed.acquiredAt,
448
+ "plugin catalog lock acquisition time",
449
+ ),
450
+ pid: parsed.pid,
451
+ bootId: parsed.bootId,
452
+ processStartId: parsed.processStartId,
453
+ });
454
+ }
455
+
456
+ function parseLockClaimRecord(
457
+ value: unknown,
458
+ targetSha256: string,
459
+ claimId: string,
460
+ ): PortableProviderPluginLockClaimV1 {
461
+ const parsed = record(value, "portable plugin lock arbitration claim");
462
+ exactKeys(
463
+ parsed,
464
+ [
465
+ "bootId",
466
+ "claimId",
467
+ "kind",
468
+ "pid",
469
+ "processStartId",
470
+ "schemaVersion",
471
+ "targetSha256",
472
+ ],
473
+ "portable plugin lock arbitration claim",
474
+ );
475
+ if (
476
+ parsed.schemaVersion !== PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION
477
+ || parsed.kind !== "portable-provider-plugin-lock-claim"
478
+ || parsed.targetSha256 !== targetSha256
479
+ || parsed.claimId !== claimId
480
+ || !lockClaimIdPattern.test(claimId)
481
+ || !Number.isSafeInteger(parsed.pid)
482
+ || typeof parsed.pid !== "number"
483
+ || parsed.pid < 1
484
+ || typeof parsed.bootId !== "string"
485
+ || !sha256Pattern.test(parsed.bootId)
486
+ || typeof parsed.processStartId !== "string"
487
+ || !sha256Pattern.test(parsed.processStartId)
488
+ ) {
489
+ throw new Error("portable plugin lock arbitration claim is invalid");
490
+ }
491
+ return Object.freeze({
492
+ schemaVersion: parsed.schemaVersion,
493
+ kind: parsed.kind,
494
+ targetSha256: parsed.targetSha256,
495
+ claimId: parsed.claimId,
496
+ pid: parsed.pid,
497
+ bootId: parsed.bootId,
498
+ processStartId: parsed.processStartId,
499
+ });
500
+ }
501
+
502
+ function physicalPathThroughExistingAncestor(path: string): string {
503
+ let ancestor = resolve(path);
504
+ const missingSegments: string[] = [];
505
+ for (;;) {
506
+ try {
507
+ return join(
508
+ realpathSync(ancestor),
509
+ ...[...missingSegments].reverse(),
510
+ );
511
+ } catch (error) {
512
+ if (!hasCode(error, "ENOENT") && !hasCode(error, "ENOTDIR")) {
513
+ throw new Error(
514
+ "portable plugin store path could not be resolved safely",
515
+ { cause: error },
516
+ );
517
+ }
518
+ const parent = dirname(ancestor);
519
+ if (parent === ancestor) {
520
+ throw new Error(
521
+ "portable plugin store path has no resolvable physical ancestor",
522
+ { cause: error },
523
+ );
524
+ }
525
+ missingSegments.push(basename(ancestor));
526
+ ancestor = parent;
527
+ }
528
+ }
529
+ }
530
+
531
+ function physicalPathThroughExistingParent(path: string): string {
532
+ const absolute = resolve(path);
533
+ return join(
534
+ physicalPathThroughExistingAncestor(dirname(absolute)),
535
+ basename(absolute),
536
+ );
537
+ }
538
+
539
+ export function portableProviderPluginStorePaths(
540
+ root: string,
541
+ ): PortableProviderPluginStorePaths {
542
+ const canonicalRoot = physicalPathThroughExistingAncestor(root);
543
+ return Object.freeze({
544
+ root: canonicalRoot,
545
+ artifacts: join(canonicalRoot, "artifacts"),
546
+ trust: join(canonicalRoot, "trust"),
547
+ active: join(canonicalRoot, "active"),
548
+ locks: join(canonicalRoot, "locks"),
549
+ });
550
+ }
551
+
552
+ function ownedByCurrentUser(stats: { readonly uid: number }): boolean {
553
+ const currentUid = typeof process.getuid === "function"
554
+ ? process.getuid()
555
+ : undefined;
556
+ return currentUid === undefined || stats.uid === currentUid;
557
+ }
558
+
559
+ function sameFileIdentity(
560
+ left: ReturnType<typeof fstatSync>,
561
+ right: ReturnType<typeof fstatSync>,
562
+ ): boolean {
563
+ return left.dev === right.dev
564
+ && left.ino === right.ino
565
+ && left.size === right.size
566
+ && left.mtimeMs === right.mtimeMs
567
+ && left.ctimeMs === right.ctimeMs;
568
+ }
569
+
570
+ function sameInodeIdentity(
571
+ left: ReturnType<typeof fstatSync>,
572
+ right: ReturnType<typeof fstatSync>,
573
+ ): boolean {
574
+ return left.dev === right.dev && left.ino === right.ino;
575
+ }
576
+
577
+ type PrivateRecordSnapshot<T> = {
578
+ readonly value: T;
579
+ readonly identity: ReturnType<typeof fstatSync>;
580
+ };
581
+
582
+ class PrivateRecordChangedDuringReadError extends Error {}
583
+
584
+ function changedDuringPrivateRecordRead(
585
+ label: string,
586
+ ): PrivateRecordChangedDuringReadError {
587
+ return new PrivateRecordChangedDuringReadError(
588
+ `${label} changed while it was being read`,
589
+ );
590
+ }
591
+
592
+ function pauseAfterLockClaimReadForTest(
593
+ path: string,
594
+ label: string,
595
+ ): void {
596
+ if (
597
+ process.env.NODE_ENV !== "test"
598
+ || label !== "portable plugin lock arbitration claim"
599
+ ) {
600
+ return;
601
+ }
602
+ const readyPath = (process.env.GHOSTGET_TEST_PLUGIN_LOCK_CLAIM_READ_READY ?? process.env.WRENCH_TEST_PLUGIN_LOCK_CLAIM_READ_READY);
603
+ const releasePath = (process.env.GHOSTGET_TEST_PLUGIN_LOCK_CLAIM_READ_RELEASE ?? process.env.WRENCH_TEST_PLUGIN_LOCK_CLAIM_READ_RELEASE);
604
+ const targetName = (process.env.GHOSTGET_TEST_PLUGIN_LOCK_CLAIM_READ_TARGET ?? process.env.WRENCH_TEST_PLUGIN_LOCK_CLAIM_READ_TARGET);
605
+ if (
606
+ readyPath === undefined
607
+ || releasePath === undefined
608
+ || targetName === undefined
609
+ || basename(path) !== targetName
610
+ || existsSync(readyPath)
611
+ ) {
612
+ return;
613
+ }
614
+ writePrivateFile(readyPath, Buffer.from("read\n", "utf8"));
615
+ syncDirectory(dirname(readyPath));
616
+ const deadline = Date.now() + TEST_BARRIER_TIMEOUT_MS;
617
+ while (!existsSync(releasePath)) {
618
+ if (Date.now() >= deadline) {
619
+ throw new Error("portable plugin lock claim read test barrier timed out");
620
+ }
621
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
622
+ }
623
+ }
624
+
625
+ function readPrivateRecordSnapshotIfPresent<T>(
626
+ path: string,
627
+ label: string,
628
+ parse: (value: unknown) => T,
629
+ ): PrivateRecordSnapshot<T> | null {
630
+ const noFollow = "O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0;
631
+ let descriptor: number;
632
+ try {
633
+ descriptor = openSync(path, constants.O_RDONLY | noFollow);
634
+ } catch (error) {
635
+ if (hasCode(error, "ENOENT")) return null;
636
+ throw new Error(`could not safely open ${label}`, { cause: error });
637
+ }
638
+ try {
639
+ const before = fstatSync(descriptor);
640
+ if (
641
+ !before.isFile()
642
+ || !ownedByCurrentUser(before)
643
+ || (before.mode & 0o077) !== 0
644
+ || before.size < 2
645
+ || before.size > MAX_STORE_RECORD_BYTES
646
+ ) {
647
+ throw new Error(`${label} must be one bounded private regular file`);
648
+ }
649
+ const bytes = readFileSync(descriptor);
650
+ pauseAfterLockClaimReadForTest(path, label);
651
+ const after = fstatSync(descriptor);
652
+ if (
653
+ bytes.byteLength !== before.size
654
+ || !sameFileIdentity(before, after)
655
+ ) {
656
+ throw changedDuringPrivateRecordRead(label);
657
+ }
658
+ let linked: ReturnType<typeof lstatSync>;
659
+ try {
660
+ linked = lstatSync(path);
661
+ } catch (error) {
662
+ if (hasCode(error, "ENOENT")) {
663
+ throw changedDuringPrivateRecordRead(label);
664
+ }
665
+ throw new Error(`could not safely verify ${label}`, { cause: error });
666
+ }
667
+ if (
668
+ linked.isSymbolicLink()
669
+ || !linked.isFile()
670
+ || !sameInodeIdentity(after, linked)
671
+ ) {
672
+ throw changedDuringPrivateRecordRead(label);
673
+ }
674
+ let text: string;
675
+ try {
676
+ text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
677
+ } catch {
678
+ throw new Error(`${label} must contain valid UTF-8`);
679
+ }
680
+ let value: unknown;
681
+ try {
682
+ value = JSON.parse(text) as unknown;
683
+ } catch {
684
+ throw new Error(`${label} must contain valid JSON`);
685
+ }
686
+ const result = parse(value);
687
+ if (text !== `${canonicalJson(result)}\n`) {
688
+ throw new Error(`${label} must use canonical JSON encoding`);
689
+ }
690
+ return Object.freeze({ value: result, identity: after });
691
+ } finally {
692
+ closeSync(descriptor);
693
+ }
694
+ }
695
+
696
+ function readPrivateRecordIfPresent<T>(
697
+ path: string,
698
+ label: string,
699
+ parse: (value: unknown) => T,
700
+ ): T | null {
701
+ return readPrivateRecordSnapshotIfPresent(path, label, parse)?.value ?? null;
702
+ }
703
+
704
+ function syncDirectory(path: string): void {
705
+ const directoryFlag = "O_DIRECTORY" in constants ? constants.O_DIRECTORY : 0;
706
+ const descriptor = openSync(path, constants.O_RDONLY | directoryFlag);
707
+ try {
708
+ fsyncSync(descriptor);
709
+ } finally {
710
+ closeSync(descriptor);
711
+ }
712
+ }
713
+
714
+ type LockPublicationKind = "claim" | "lock";
715
+
716
+ type PublishedPrivateLockFile = {
717
+ readonly descriptor: number;
718
+ readonly identity: ReturnType<typeof fstatSync>;
719
+ };
720
+
721
+ function lockPublicationStagePath(
722
+ parent: string,
723
+ owner: ProcessOwnerIdentity,
724
+ ): string {
725
+ return join(
726
+ parent,
727
+ `${lockPublicationPrefix}${owner.pid}-${owner.bootId}-`
728
+ + `${owner.processStartId}-${randomUUID()}.tmp`,
729
+ );
730
+ }
731
+
732
+ function parseLockPublicationOwner(name: string): ProcessOwnerIdentity {
733
+ const match = lockPublicationNamePattern.exec(name);
734
+ if (match === null) {
735
+ throw new Error("portable plugin lock publication filename is invalid");
736
+ }
737
+ const pid = Number(match[1]);
738
+ if (!Number.isSafeInteger(pid) || pid < 1) {
739
+ throw new Error("portable plugin lock publication owner is invalid");
740
+ }
741
+ return Object.freeze({
742
+ pid,
743
+ bootId: match[2]!,
744
+ processStartId: match[3]!,
745
+ });
746
+ }
747
+
748
+ function stableProcessOwnerStatus(
749
+ owner: ProcessOwnerIdentity,
750
+ ): ReturnType<typeof processOwnerStatus> {
751
+ let status = processOwnerStatus(owner);
752
+ for (let attempt = 0; status === "unknown" && attempt < 3; attempt += 1) {
753
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
754
+ status = processOwnerStatus(owner);
755
+ }
756
+ return status;
757
+ }
758
+
759
+ function reclaimDeadLockPublications(parent: string): void {
760
+ for (let attempt = 0; attempt < 8; attempt += 1) {
761
+ const names = readdirSync(parent)
762
+ .filter((name) => name.startsWith(lockPublicationPrefix))
763
+ .sort();
764
+ if (names.length > MAX_LOCK_PUBLICATIONS) {
765
+ throw new Error("portable plugin lock publication count exceeds its bound");
766
+ }
767
+ let retry = false;
768
+ let removed = false;
769
+ for (const name of names) {
770
+ const owner = parseLockPublicationOwner(name);
771
+ const status = stableProcessOwnerStatus(owner);
772
+ if (status === "unknown") {
773
+ throw new Error(
774
+ "portable plugin lock publication owner cannot be inspected safely",
775
+ );
776
+ }
777
+ if (status === "exact-live-owner") continue;
778
+ const path = join(parent, name);
779
+ let identity: ReturnType<typeof fstatSync>;
780
+ try {
781
+ const descriptor = openSync(
782
+ path,
783
+ constants.O_RDONLY
784
+ | ("O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0),
785
+ );
786
+ try {
787
+ identity = fstatSync(descriptor);
788
+ if (
789
+ !identity.isFile()
790
+ || !ownedByCurrentUser(identity)
791
+ || (identity.mode & 0o077) !== 0
792
+ || identity.size > MAX_STORE_RECORD_BYTES
793
+ ) {
794
+ throw new Error(
795
+ "portable plugin lock publication must be one bounded "
796
+ + "private regular file",
797
+ );
798
+ }
799
+ } finally {
800
+ closeSync(descriptor);
801
+ }
802
+ } catch (error) {
803
+ if (hasCode(error, "ENOENT")) {
804
+ retry = true;
805
+ break;
806
+ }
807
+ throw error;
808
+ }
809
+ if (removeExactPrivateFile(
810
+ path,
811
+ identity,
812
+ "portable plugin lock publication",
813
+ )) {
814
+ removed = true;
815
+ } else {
816
+ retry = true;
817
+ break;
818
+ }
819
+ }
820
+ if (retry || removed) continue;
821
+ return;
822
+ }
823
+ throw new Error(
824
+ "portable plugin lock publications did not reach a stable snapshot",
825
+ );
826
+ }
827
+
828
+ function crashLockPublicationForTest(
829
+ kind: LockPublicationKind,
830
+ logicalLockPath: string,
831
+ descriptor: number,
832
+ bytes: Uint8Array,
833
+ ): void {
834
+ if (process.env.NODE_ENV !== "test") return;
835
+ const fault = (process.env.GHOSTGET_TEST_PLUGIN_LOCK_PUBLICATION_CRASH ?? process.env.WRENCH_TEST_PLUGIN_LOCK_PUBLICATION_CRASH);
836
+ const targetName = (process.env.GHOSTGET_TEST_PLUGIN_LOCK_PUBLICATION_TARGET ?? process.env.WRENCH_TEST_PLUGIN_LOCK_PUBLICATION_TARGET);
837
+ if (
838
+ targetName === undefined
839
+ || basename(logicalLockPath) !== targetName
840
+ || (
841
+ fault !== `${kind}-empty-stage`
842
+ && fault !== `${kind}-partial-stage`
843
+ )
844
+ ) {
845
+ return;
846
+ }
847
+ if (fault.endsWith("partial-stage")) {
848
+ writeFileSync(
849
+ descriptor,
850
+ bytes.subarray(0, Math.max(1, Math.floor(bytes.byteLength / 2))),
851
+ );
852
+ }
853
+ fchmodSync(descriptor, 0o600);
854
+ fsyncSync(descriptor);
855
+ process.exit(92);
856
+ }
857
+
858
+ function unlinkIfPresent(path: string): boolean {
859
+ try {
860
+ unlinkSync(path);
861
+ return true;
862
+ } catch (error) {
863
+ if (hasCode(error, "ENOENT")) return false;
864
+ throw error;
865
+ }
866
+ }
867
+
868
+ function cleanupFailedLockPublication(
869
+ path: string,
870
+ stage: string,
871
+ parent: string,
872
+ descriptor: number | null,
873
+ identity: ReturnType<typeof fstatSync> | null,
874
+ targetLinked: boolean,
875
+ directoryChanged: boolean,
876
+ ): void {
877
+ let cleanupError: unknown;
878
+ if (targetLinked && identity !== null) {
879
+ try {
880
+ removeExactPrivateFile(
881
+ path,
882
+ identity,
883
+ "portable plugin lock publication",
884
+ );
885
+ } catch (error) {
886
+ cleanupError = error;
887
+ }
888
+ }
889
+ if (descriptor !== null) {
890
+ try {
891
+ closeSync(descriptor);
892
+ } catch (error) {
893
+ cleanupError ??= error;
894
+ }
895
+ }
896
+ try {
897
+ if (unlinkIfPresent(stage)) directoryChanged = true;
898
+ } catch (error) {
899
+ cleanupError ??= error;
900
+ }
901
+ if (directoryChanged) {
902
+ try {
903
+ syncDirectory(parent);
904
+ } catch (error) {
905
+ cleanupError ??= error;
906
+ }
907
+ }
908
+ if (cleanupError !== undefined) {
909
+ throw cleanupError instanceof Error
910
+ ? cleanupError
911
+ : new Error("portable plugin lock publication cleanup failed", {
912
+ cause: cleanupError,
913
+ });
914
+ }
915
+ }
916
+
917
+ function publishPrivateLockFileExclusive(
918
+ path: string,
919
+ bytes: Uint8Array,
920
+ owner: ProcessOwnerIdentity,
921
+ kind: LockPublicationKind,
922
+ logicalLockPath: string,
923
+ ): PublishedPrivateLockFile | null {
924
+ const parent = dirname(path);
925
+ const stage = lockPublicationStagePath(parent, owner);
926
+ const noFollow = "O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0;
927
+ let descriptor: number | null = null;
928
+ let identity: ReturnType<typeof fstatSync> | null = null;
929
+ let targetLinked = false;
930
+ let transferred = false;
931
+ let directoryChanged = false;
932
+ try {
933
+ // The stage name carries enough process identity to reclaim even an empty
934
+ // crash remnant. The hard link below is the only publication point, so a
935
+ // reader can observe either no lock or one complete, fsynced record.
936
+ descriptor = openSync(
937
+ stage,
938
+ constants.O_WRONLY
939
+ | constants.O_CREAT
940
+ | constants.O_EXCL
941
+ | noFollow,
942
+ 0o600,
943
+ );
944
+ directoryChanged = true;
945
+ crashLockPublicationForTest(
946
+ kind,
947
+ logicalLockPath,
948
+ descriptor,
949
+ bytes,
950
+ );
951
+ writeFileSync(descriptor, bytes);
952
+ fchmodSync(descriptor, 0o600);
953
+ fsyncSync(descriptor);
954
+ identity = fstatSync(descriptor);
955
+ try {
956
+ linkSync(stage, path);
957
+ targetLinked = true;
958
+ } catch (error) {
959
+ if (!hasCode(error, "EEXIST")) throw error;
960
+ return null;
961
+ }
962
+ unlinkSync(stage);
963
+ syncDirectory(parent);
964
+ directoryChanged = false;
965
+ transferred = true;
966
+ return Object.freeze({ descriptor, identity });
967
+ } finally {
968
+ if (!transferred) {
969
+ cleanupFailedLockPublication(
970
+ path,
971
+ stage,
972
+ parent,
973
+ descriptor,
974
+ identity,
975
+ targetLinked,
976
+ directoryChanged,
977
+ );
978
+ }
979
+ }
980
+ }
981
+
982
+ function writePrivateFile(path: string, bytes: Uint8Array): void {
983
+ const noFollow = "O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0;
984
+ const descriptor = openSync(
985
+ path,
986
+ constants.O_WRONLY
987
+ | constants.O_CREAT
988
+ | constants.O_EXCL
989
+ | noFollow,
990
+ 0o600,
991
+ );
992
+ try {
993
+ writeFileSync(descriptor, bytes);
994
+ fchmodSync(descriptor, 0o600);
995
+ fsyncSync(descriptor);
996
+ } finally {
997
+ closeSync(descriptor);
998
+ }
999
+ }
1000
+
1001
+ function lockClaimTargetSha256(path: string): string {
1002
+ return createHash("sha256")
1003
+ .update("portable-provider-plugin-lock", "utf8")
1004
+ .update("\0", "utf8")
1005
+ .update(path, "utf8")
1006
+ .digest("hex");
1007
+ }
1008
+
1009
+ function lockClaimPrefix(targetSha256: string): string {
1010
+ return `.lock-claim-${targetSha256}-`;
1011
+ }
1012
+
1013
+ function lockClaimName(
1014
+ parent: string,
1015
+ targetSha256: string,
1016
+ phase: PortableProviderPluginLockClaimPhase,
1017
+ claimId: string,
1018
+ ): string {
1019
+ return join(
1020
+ parent,
1021
+ `${lockClaimPrefix(targetSha256)}${phase}-${claimId}.json`,
1022
+ );
1023
+ }
1024
+
1025
+ function parseLockClaimName(
1026
+ name: string,
1027
+ targetSha256: string,
1028
+ ): {
1029
+ readonly phase: PortableProviderPluginLockClaimPhase;
1030
+ readonly claimId: string;
1031
+ } {
1032
+ const prefix = lockClaimPrefix(targetSha256);
1033
+ if (!name.startsWith(prefix)) {
1034
+ throw new Error("portable plugin lock claim does not match its target");
1035
+ }
1036
+ const match =
1037
+ /^(waiting|candidate|held)-([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})\.json$/u
1038
+ .exec(name.slice(prefix.length));
1039
+ if (match === null) {
1040
+ throw new Error("portable plugin lock claim filename is invalid");
1041
+ }
1042
+ return {
1043
+ phase: match[1] as PortableProviderPluginLockClaimPhase,
1044
+ claimId: match[2]!,
1045
+ };
1046
+ }
1047
+
1048
+ function listLiveLockClaims(
1049
+ parent: string,
1050
+ targetSha256: string,
1051
+ ): readonly {
1052
+ readonly path: string;
1053
+ readonly phase: PortableProviderPluginLockClaimPhase;
1054
+ readonly claim: PortableProviderPluginLockClaimV1;
1055
+ }[] {
1056
+ const prefix = lockClaimPrefix(targetSha256);
1057
+ for (let attempt = 0; attempt < 8; attempt += 1) {
1058
+ const names = readdirSync(parent)
1059
+ .filter((name) => name.startsWith(prefix))
1060
+ .sort();
1061
+ if (names.length > MAX_LOCK_CLAIMS) {
1062
+ throw new Error("portable plugin lock claim count exceeds its bound");
1063
+ }
1064
+ let retry = false;
1065
+ let removedStaleClaim = false;
1066
+ const live: {
1067
+ readonly path: string;
1068
+ readonly phase: PortableProviderPluginLockClaimPhase;
1069
+ readonly claim: PortableProviderPluginLockClaimV1;
1070
+ }[] = [];
1071
+ for (const name of names) {
1072
+ const parsedName = parseLockClaimName(name, targetSha256);
1073
+ const path = join(parent, name);
1074
+ let snapshot: PrivateRecordSnapshot<
1075
+ PortableProviderPluginLockClaimV1
1076
+ > | null;
1077
+ try {
1078
+ snapshot = readPrivateRecordSnapshotIfPresent(
1079
+ path,
1080
+ "portable plugin lock arbitration claim",
1081
+ (value) => parseLockClaimRecord(
1082
+ value,
1083
+ targetSha256,
1084
+ parsedName.claimId,
1085
+ ),
1086
+ );
1087
+ } catch (error) {
1088
+ if (error instanceof PrivateRecordChangedDuringReadError) {
1089
+ retry = true;
1090
+ break;
1091
+ }
1092
+ throw error;
1093
+ }
1094
+ if (snapshot === null) {
1095
+ retry = true;
1096
+ break;
1097
+ }
1098
+ const status = stableProcessOwnerStatus(snapshot.value);
1099
+ if (status === "unknown") {
1100
+ throw new Error(
1101
+ "portable plugin lock arbitration owner cannot be inspected safely",
1102
+ );
1103
+ }
1104
+ if (status === "different-or-dead") {
1105
+ if (removeExactPrivateFile(
1106
+ path,
1107
+ snapshot.identity,
1108
+ "portable plugin lock arbitration claim",
1109
+ )) {
1110
+ removedStaleClaim = true;
1111
+ } else {
1112
+ retry = true;
1113
+ break;
1114
+ }
1115
+ continue;
1116
+ }
1117
+ live.push({
1118
+ path,
1119
+ phase: parsedName.phase,
1120
+ claim: snapshot.value,
1121
+ });
1122
+ }
1123
+ if (retry || removedStaleClaim) continue;
1124
+ return live;
1125
+ }
1126
+ throw new Error(
1127
+ "portable plugin lock claims did not reach a stable snapshot",
1128
+ );
1129
+ }
1130
+
1131
+ function acquireLockClaim(path: string): (() => void) | null {
1132
+ const logicalLockPath = physicalPathThroughExistingParent(path);
1133
+ const parent = dirname(logicalLockPath);
1134
+ const targetSha256 = lockClaimTargetSha256(logicalLockPath);
1135
+ const claimId = randomUUID();
1136
+ const claim: PortableProviderPluginLockClaimV1 = Object.freeze({
1137
+ schemaVersion: PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION,
1138
+ kind: "portable-provider-plugin-lock-claim",
1139
+ targetSha256,
1140
+ claimId,
1141
+ pid: process.pid,
1142
+ ...currentProcessStartIdentity(),
1143
+ });
1144
+ let claimPath = lockClaimName(
1145
+ parent,
1146
+ targetSha256,
1147
+ "waiting",
1148
+ claimId,
1149
+ );
1150
+ reclaimDeadLockPublications(parent);
1151
+ const publication = publishPrivateLockFileExclusive(
1152
+ claimPath,
1153
+ Buffer.from(`${canonicalJson(claim)}\n`, "utf8"),
1154
+ claim,
1155
+ "claim",
1156
+ logicalLockPath,
1157
+ );
1158
+ if (publication === null) {
1159
+ throw new Error("portable plugin lock claim ID was already published");
1160
+ }
1161
+ const claimIdentity = publication.identity;
1162
+ const release = (): void => {
1163
+ removeExactPrivateFile(
1164
+ claimPath,
1165
+ claimIdentity,
1166
+ "portable plugin lock arbitration claim",
1167
+ );
1168
+ };
1169
+ try {
1170
+ closeSync(publication.descriptor);
1171
+ pauseAfterWaitingLockClaimForTest(logicalLockPath);
1172
+ const waiting = listLiveLockClaims(parent, targetSha256);
1173
+ if (
1174
+ waiting.some((candidate) =>
1175
+ candidate.claim.claimId !== claimId
1176
+ && (
1177
+ candidate.phase !== "waiting"
1178
+ || candidate.claim.claimId < claimId
1179
+ )
1180
+ )
1181
+ ) {
1182
+ release();
1183
+ return null;
1184
+ }
1185
+
1186
+ const candidatePath = lockClaimName(
1187
+ parent,
1188
+ targetSha256,
1189
+ "candidate",
1190
+ claimId,
1191
+ );
1192
+ renameSync(claimPath, candidatePath);
1193
+ claimPath = candidatePath;
1194
+ syncDirectory(parent);
1195
+ const candidates = listLiveLockClaims(parent, targetSha256);
1196
+ if (
1197
+ candidates.some((candidate) =>
1198
+ candidate.claim.claimId !== claimId
1199
+ && (
1200
+ candidate.phase === "held"
1201
+ || candidate.claim.claimId < claimId
1202
+ )
1203
+ )
1204
+ ) {
1205
+ release();
1206
+ return null;
1207
+ }
1208
+
1209
+ const heldPath = lockClaimName(
1210
+ parent,
1211
+ targetSha256,
1212
+ "held",
1213
+ claimId,
1214
+ );
1215
+ renameSync(claimPath, heldPath);
1216
+ claimPath = heldPath;
1217
+ syncDirectory(parent);
1218
+ const held = listLiveLockClaims(parent, targetSha256);
1219
+ if (
1220
+ held.some((candidate) =>
1221
+ candidate.claim.claimId !== claimId
1222
+ && candidate.phase === "held"
1223
+ )
1224
+ ) {
1225
+ throw new Error(
1226
+ "portable plugin lock arbitration admitted two owners",
1227
+ );
1228
+ }
1229
+ return release;
1230
+ } catch (error) {
1231
+ release();
1232
+ throw error;
1233
+ }
1234
+ }
1235
+
1236
+ function pauseBeforeLockClaimForTest(path: string): void {
1237
+ if (process.env.NODE_ENV !== "test") return;
1238
+ const readyPath = (process.env.GHOSTGET_TEST_PLUGIN_LOCK_CLAIM_READY ?? process.env.WRENCH_TEST_PLUGIN_LOCK_CLAIM_READY);
1239
+ const releasePath = (process.env.GHOSTGET_TEST_PLUGIN_LOCK_CLAIM_RELEASE ?? process.env.WRENCH_TEST_PLUGIN_LOCK_CLAIM_RELEASE);
1240
+ const targetName = (process.env.GHOSTGET_TEST_PLUGIN_LOCK_CLAIM_TARGET ?? process.env.WRENCH_TEST_PLUGIN_LOCK_CLAIM_TARGET);
1241
+ if (
1242
+ readyPath === undefined
1243
+ || releasePath === undefined
1244
+ || targetName === undefined
1245
+ || basename(path) !== targetName
1246
+ ) {
1247
+ return;
1248
+ }
1249
+ writePrivateFile(readyPath, Buffer.from("ready\n", "utf8"));
1250
+ syncDirectory(dirname(readyPath));
1251
+ const deadline = Date.now() + TEST_BARRIER_TIMEOUT_MS;
1252
+ while (!existsSync(releasePath)) {
1253
+ if (Date.now() >= deadline) {
1254
+ throw new Error("portable plugin lock claim test barrier timed out");
1255
+ }
1256
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
1257
+ }
1258
+ }
1259
+
1260
+ function pauseAfterWaitingLockClaimForTest(path: string): void {
1261
+ if (process.env.NODE_ENV !== "test") return;
1262
+ const readyPath = (process.env.GHOSTGET_TEST_PLUGIN_LOCK_WAITING_READY ?? process.env.WRENCH_TEST_PLUGIN_LOCK_WAITING_READY);
1263
+ const releasePath = (process.env.GHOSTGET_TEST_PLUGIN_LOCK_WAITING_RELEASE ?? process.env.WRENCH_TEST_PLUGIN_LOCK_WAITING_RELEASE);
1264
+ const targetName = (process.env.GHOSTGET_TEST_PLUGIN_LOCK_WAITING_TARGET ?? process.env.WRENCH_TEST_PLUGIN_LOCK_WAITING_TARGET);
1265
+ if (
1266
+ readyPath === undefined
1267
+ || releasePath === undefined
1268
+ || targetName === undefined
1269
+ || basename(path) !== targetName
1270
+ ) {
1271
+ return;
1272
+ }
1273
+ if (existsSync(readyPath)) return;
1274
+ writePrivateFile(readyPath, Buffer.from("waiting\n", "utf8"));
1275
+ syncDirectory(dirname(readyPath));
1276
+ const deadline = Date.now() + TEST_BARRIER_TIMEOUT_MS;
1277
+ while (!existsSync(releasePath)) {
1278
+ if (Date.now() >= deadline) {
1279
+ throw new Error("portable plugin waiting-claim test barrier timed out");
1280
+ }
1281
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
1282
+ }
1283
+ }
1284
+
1285
+ function removeExactPrivateFile(
1286
+ path: string,
1287
+ expectedIdentity: ReturnType<typeof fstatSync>,
1288
+ label: string,
1289
+ changedMessage = `${label} changed during exact removal`,
1290
+ ): boolean {
1291
+ const parent = dirname(path);
1292
+ const quarantine = join(
1293
+ parent,
1294
+ `.lock-reclaim-${process.pid}-${randomUUID()}`,
1295
+ );
1296
+ try {
1297
+ renameSync(path, quarantine);
1298
+ } catch (error) {
1299
+ if (hasCode(error, "ENOENT")) return false;
1300
+ throw error;
1301
+ }
1302
+ const moved = lstatSync(quarantine);
1303
+ if (
1304
+ moved.isSymbolicLink()
1305
+ || !moved.isFile()
1306
+ || !sameInodeIdentity(expectedIdentity, moved)
1307
+ || !ownedByCurrentUser(moved)
1308
+ || (moved.mode & 0o077) !== 0
1309
+ ) {
1310
+ try {
1311
+ linkSync(quarantine, path);
1312
+ unlinkSync(quarantine);
1313
+ syncDirectory(parent);
1314
+ } catch (error) {
1315
+ throw new Error(
1316
+ `${changedMessage} and could not be restored`,
1317
+ { cause: error },
1318
+ );
1319
+ }
1320
+ throw new Error(changedMessage);
1321
+ }
1322
+ unlinkSync(quarantine);
1323
+ syncDirectory(parent);
1324
+ return true;
1325
+ }
1326
+
1327
+ function removeExactStaleLock<T>(
1328
+ path: string,
1329
+ snapshot: PrivateRecordSnapshot<T>,
1330
+ label: string,
1331
+ ): boolean {
1332
+ return removeExactPrivateFile(path, snapshot.identity, label);
1333
+ }
1334
+
1335
+ function writeAtomicPrivateRecord(path: string, value: unknown): void {
1336
+ const parent = dirname(path);
1337
+ const temporary = join(
1338
+ parent,
1339
+ `.${basename(path)}.stage-${process.pid}-${randomUUID()}`,
1340
+ );
1341
+ try {
1342
+ writePrivateFile(
1343
+ temporary,
1344
+ Buffer.from(`${canonicalJson(value)}\n`, "utf8"),
1345
+ );
1346
+ renameSync(temporary, path);
1347
+ syncDirectory(parent);
1348
+ } finally {
1349
+ rmSync(temporary, { force: true });
1350
+ }
1351
+ }
1352
+
1353
+ function writeImmutablePrivateRecord<T>(
1354
+ path: string,
1355
+ value: T,
1356
+ label: string,
1357
+ parse: (candidate: unknown) => T,
1358
+ ): T {
1359
+ const parent = dirname(path);
1360
+ const temporary = join(
1361
+ parent,
1362
+ `.${basename(path)}.stage-${process.pid}-${randomUUID()}`,
1363
+ );
1364
+ try {
1365
+ writePrivateFile(
1366
+ temporary,
1367
+ Buffer.from(`${canonicalJson(value)}\n`, "utf8"),
1368
+ );
1369
+ try {
1370
+ linkSync(temporary, path);
1371
+ syncDirectory(parent);
1372
+ return value;
1373
+ } catch (error) {
1374
+ if (!hasCode(error, "EEXIST")) throw error;
1375
+ const existing = readPrivateRecordIfPresent(path, label, parse);
1376
+ if (existing === null || canonicalJson(existing) !== canonicalJson(value)) {
1377
+ throw new Error(`${label} already exists with different content`);
1378
+ }
1379
+ return existing;
1380
+ }
1381
+ } finally {
1382
+ rmSync(temporary, { force: true });
1383
+ }
1384
+ }
1385
+
1386
+ function ensureStore(root: string): PortableProviderPluginStorePaths {
1387
+ const paths = portableProviderPluginStorePaths(root);
1388
+ ensurePrivateDirectory(paths.root);
1389
+ ensurePrivateDirectory(paths.artifacts);
1390
+ ensurePrivateDirectory(paths.trust);
1391
+ ensurePrivateDirectory(paths.active);
1392
+ ensurePrivateDirectory(paths.locks);
1393
+ return paths;
1394
+ }
1395
+
1396
+ function openStoreIfPresent(
1397
+ root: string,
1398
+ ): PortableProviderPluginStorePaths | null {
1399
+ const paths = portableProviderPluginStorePaths(root);
1400
+ if (!existsSync(paths.root)) return null;
1401
+ ensurePrivateDirectory(paths.root);
1402
+ for (const directory of [
1403
+ paths.artifacts,
1404
+ paths.trust,
1405
+ paths.active,
1406
+ paths.locks,
1407
+ ]) {
1408
+ if (!existsSync(directory)) {
1409
+ throw new Error("portable plugin store is missing a required directory");
1410
+ }
1411
+ ensurePrivateDirectory(directory);
1412
+ }
1413
+ return paths;
1414
+ }
1415
+
1416
+ function writeVerifiedArtifact(
1417
+ artifactsRoot: string,
1418
+ source: VerifiedPortableProviderPluginPackage,
1419
+ ): string {
1420
+ const destination = join(artifactsRoot, source.bundleSha256);
1421
+ if (existsSync(destination)) {
1422
+ const existing = verifyPortableProviderPluginPackageDirectory(destination);
1423
+ if (existing.bundleSha256 !== source.bundleSha256) {
1424
+ throw new Error("installed portable plugin artifact digest is corrupt");
1425
+ }
1426
+ return destination;
1427
+ }
1428
+ const stage = join(
1429
+ artifactsRoot,
1430
+ `.stage-${source.bundleSha256}-${process.pid}-${randomUUID()}`,
1431
+ );
1432
+ mkdirSync(stage, { mode: 0o700 });
1433
+ chmodSync(stage, 0o700);
1434
+ try {
1435
+ for (const file of source.files) {
1436
+ const target = join(stage, ...file.path.split("/"));
1437
+ ensurePrivateDirectory(dirname(target));
1438
+ writePrivateFile(target, file.bytes);
1439
+ }
1440
+ writePrivateFile(
1441
+ join(stage, PORTABLE_PROVIDER_PLUGIN_MANIFEST_NAME),
1442
+ source.manifestBytes,
1443
+ );
1444
+ const staged = verifyPortableProviderPluginPackageDirectory(stage);
1445
+ if (staged.bundleSha256 !== source.bundleSha256) {
1446
+ throw new Error("staged portable plugin artifact digest changed");
1447
+ }
1448
+ try {
1449
+ renameSync(stage, destination);
1450
+ syncDirectory(artifactsRoot);
1451
+ } catch (error) {
1452
+ if (!hasCode(error, "EEXIST") && !hasCode(error, "ENOTEMPTY")) {
1453
+ throw error;
1454
+ }
1455
+ const existing = verifyPortableProviderPluginPackageDirectory(destination);
1456
+ if (existing.bundleSha256 !== source.bundleSha256) {
1457
+ throw new Error("concurrent portable plugin artifact is corrupt");
1458
+ }
1459
+ }
1460
+ return destination;
1461
+ } finally {
1462
+ rmSync(stage, { recursive: true, force: true });
1463
+ }
1464
+ }
1465
+
1466
+ function acquireOwnedLock<T extends ProcessOwnerIdentity>(
1467
+ path: string,
1468
+ value: T,
1469
+ options: {
1470
+ readonly label: string;
1471
+ readonly parse: (candidate: unknown) => T;
1472
+ readonly assertExisting?: (existing: T) => void;
1473
+ readonly liveOwnerMessage: string;
1474
+ readonly unknownOwnerMessage: string;
1475
+ readonly acquisitionFailureMessage: string;
1476
+ readonly changedBeforeReleaseMessage: string;
1477
+ },
1478
+ ): () => void {
1479
+ const lockPath = physicalPathThroughExistingParent(path);
1480
+ let descriptor: number | null = null;
1481
+ let owned: ReturnType<typeof fstatSync> | null = null;
1482
+ if (existsSync(lockPath)) pauseBeforeLockClaimForTest(lockPath);
1483
+
1484
+ acquire: for (let claimAttempt = 0; claimAttempt < 64; claimAttempt += 1) {
1485
+ const releaseClaim = acquireLockClaim(lockPath);
1486
+ if (releaseClaim === null) {
1487
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5);
1488
+ continue;
1489
+ }
1490
+ try {
1491
+ for (let attempt = 0; attempt < 4; attempt += 1) {
1492
+ const publication = publishPrivateLockFileExclusive(
1493
+ lockPath,
1494
+ Buffer.from(`${canonicalJson(value)}\n`, "utf8"),
1495
+ value,
1496
+ "lock",
1497
+ lockPath,
1498
+ );
1499
+ if (publication !== null) {
1500
+ descriptor = publication.descriptor;
1501
+ owned = publication.identity;
1502
+ break acquire;
1503
+ }
1504
+ const existing = readPrivateRecordSnapshotIfPresent(
1505
+ lockPath,
1506
+ options.label,
1507
+ options.parse,
1508
+ );
1509
+ if (existing === null) continue;
1510
+ options.assertExisting?.(existing.value);
1511
+ const status = processOwnerStatus(existing.value);
1512
+ if (status === "exact-live-owner") {
1513
+ throw new Error(options.liveOwnerMessage);
1514
+ }
1515
+ if (status === "unknown") {
1516
+ throw new Error(options.unknownOwnerMessage);
1517
+ }
1518
+ removeExactStaleLock(lockPath, existing, options.label);
1519
+ }
1520
+ } finally {
1521
+ releaseClaim();
1522
+ }
1523
+ }
1524
+ if (descriptor === null || owned === null) {
1525
+ throw new Error(options.acquisitionFailureMessage);
1526
+ }
1527
+ const ownedDescriptor = descriptor;
1528
+ const ownedIdentity = owned;
1529
+ return () => {
1530
+ try {
1531
+ if (!removeExactPrivateFile(
1532
+ lockPath,
1533
+ ownedIdentity,
1534
+ options.label,
1535
+ options.changedBeforeReleaseMessage,
1536
+ )) {
1537
+ throw new Error(options.changedBeforeReleaseMessage);
1538
+ }
1539
+ } finally {
1540
+ closeSync(ownedDescriptor);
1541
+ }
1542
+ };
1543
+ }
1544
+
1545
+ function acquirePluginLock(
1546
+ path: string,
1547
+ id: string,
1548
+ now: Date,
1549
+ ): () => void {
1550
+ const processIdentity = currentProcessStartIdentity();
1551
+ const lock: PortableProviderPluginLockRecordV1 = Object.freeze({
1552
+ schemaVersion: PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION,
1553
+ pluginId: id,
1554
+ acquiredAt: canonicalNow(now),
1555
+ pid: process.pid,
1556
+ ...processIdentity,
1557
+ });
1558
+ return acquireOwnedLock(path, lock, {
1559
+ label: "portable plugin activation lock",
1560
+ parse: parseLockRecord,
1561
+ assertExisting: (existing) => {
1562
+ if (existing.pluginId !== id) {
1563
+ throw new Error(
1564
+ "portable plugin activation lock does not match its filename",
1565
+ );
1566
+ }
1567
+ },
1568
+ liveOwnerMessage: "portable plugin activation is already locked",
1569
+ unknownOwnerMessage:
1570
+ "portable plugin activation lock owner cannot be inspected safely",
1571
+ acquisitionFailureMessage:
1572
+ "portable plugin activation lock could not be acquired",
1573
+ changedBeforeReleaseMessage:
1574
+ "portable plugin activation lock changed before release",
1575
+ });
1576
+ }
1577
+
1578
+ function acquireCatalogMutationLock(
1579
+ paths: PortableProviderPluginStorePaths,
1580
+ now: Date,
1581
+ ): () => void {
1582
+ const processIdentity = currentProcessStartIdentity();
1583
+ const lock: PortableProviderPluginCatalogLockRecordV1 = Object.freeze({
1584
+ schemaVersion: PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION,
1585
+ scope: "catalog-mutation",
1586
+ acquiredAt: canonicalNow(now),
1587
+ pid: process.pid,
1588
+ ...processIdentity,
1589
+ });
1590
+ return acquireOwnedLock(
1591
+ join(paths.locks, CATALOG_MUTATION_LOCK_NAME),
1592
+ lock,
1593
+ {
1594
+ label: "portable plugin catalog mutation lock",
1595
+ parse: parseCatalogLockRecord,
1596
+ liveOwnerMessage:
1597
+ "portable plugin catalog mutation is already locked",
1598
+ unknownOwnerMessage:
1599
+ "portable plugin catalog mutation lock owner cannot be inspected safely",
1600
+ acquisitionFailureMessage:
1601
+ "portable plugin catalog mutation lock could not be acquired",
1602
+ changedBeforeReleaseMessage:
1603
+ "portable plugin catalog mutation lock changed before release",
1604
+ },
1605
+ );
1606
+ }
1607
+
1608
+ function runSynchronousCatalogOperation<T>(operation: () => T): T {
1609
+ const result = operation();
1610
+ if (result instanceof Promise) {
1611
+ throw new Error(
1612
+ "portable plugin catalog lock callback must complete synchronously",
1613
+ );
1614
+ }
1615
+ return result;
1616
+ }
1617
+
1618
+ function withCatalogMutationLock<T>(
1619
+ paths: PortableProviderPluginStorePaths,
1620
+ now: Date,
1621
+ operation: () => T,
1622
+ ): T {
1623
+ const lockPath = join(paths.locks, CATALOG_MUTATION_LOCK_NAME);
1624
+ if (activeCatalogMutationLocks.has(lockPath)) {
1625
+ return runSynchronousCatalogOperation(operation);
1626
+ }
1627
+ const release = acquireCatalogMutationLock(paths, now);
1628
+ activeCatalogMutationLocks.add(lockPath);
1629
+ try {
1630
+ return runSynchronousCatalogOperation(operation);
1631
+ } finally {
1632
+ activeCatalogMutationLocks.delete(lockPath);
1633
+ release();
1634
+ }
1635
+ }
1636
+
1637
+ /**
1638
+ * Serialize a synchronous kernel transition against every portable-plugin
1639
+ * activation mutation. The callback receives no auth or session material.
1640
+ */
1641
+ export function withPortableProviderPluginCatalogLock<T>(
1642
+ storeRoot: string,
1643
+ now: Date,
1644
+ operation: () => T,
1645
+ ): T {
1646
+ return withCatalogMutationLock(ensureStore(storeRoot), now, operation);
1647
+ }
1648
+
1649
+ function withPluginMutationLocks<T>(
1650
+ paths: PortableProviderPluginStorePaths,
1651
+ id: string,
1652
+ now: Date,
1653
+ operation: () => T,
1654
+ ): T {
1655
+ return withCatalogMutationLock(paths, now, () => {
1656
+ if (activePluginMutations.has(paths.root)) {
1657
+ throw new Error("portable plugin catalog mutation cannot be nested");
1658
+ }
1659
+ activePluginMutations.add(paths.root);
1660
+ try {
1661
+ const releasePlugin = acquirePluginLock(
1662
+ join(paths.locks, `${id}.lock`),
1663
+ id,
1664
+ now,
1665
+ );
1666
+ try {
1667
+ return operation();
1668
+ } finally {
1669
+ releasePlugin();
1670
+ }
1671
+ } finally {
1672
+ activePluginMutations.delete(paths.root);
1673
+ }
1674
+ });
1675
+ }
1676
+
1677
+ export function assertPortableProviderPluginTrustApprovalMatches(
1678
+ approval: PortableProviderPluginTrustApprovalV1,
1679
+ source: VerifiedPortableProviderPluginPackage,
1680
+ ): void {
1681
+ if (
1682
+ approval.pluginId !== source.manifest.id
1683
+ || approval.pluginVersion !== source.manifest.version
1684
+ || approval.bundleSha256 !== source.bundleSha256
1685
+ ) {
1686
+ throw new Error(
1687
+ "portable plugin trust approval does not match the verified package identity",
1688
+ );
1689
+ }
1690
+ }
1691
+
1692
+ function assertExpectedActive(
1693
+ current: PortableProviderPluginActiveRecordV1 | null,
1694
+ expectedBundleSha256: string | null,
1695
+ ): void {
1696
+ if (expectedBundleSha256 === null) {
1697
+ if (current !== null) {
1698
+ throw new Error(
1699
+ "portable plugin activation changed: an installed version already exists",
1700
+ );
1701
+ }
1702
+ return;
1703
+ }
1704
+ const expected = sha256(
1705
+ expectedBundleSha256,
1706
+ "expected active plugin bundle digest",
1707
+ );
1708
+ if (current?.bundleSha256 !== expected) {
1709
+ throw new Error(
1710
+ "portable plugin activation changed from its expected bundle digest",
1711
+ );
1712
+ }
1713
+ }
1714
+
1715
+ function canonicalNow(now: Date): string {
1716
+ const value = now.toISOString();
1717
+ return timestamp(value, "portable plugin store time");
1718
+ }
1719
+
1720
+ function runAssertActivatable(
1721
+ hook: PortableProviderPluginAssertActivatable,
1722
+ verifiedPackage: VerifiedPortableProviderPluginPackage,
1723
+ ): void {
1724
+ const result: unknown = hook(verifiedPackage);
1725
+ if (result !== undefined) {
1726
+ throw new Error(
1727
+ "portable plugin activatability hook must complete synchronously without returning a value",
1728
+ );
1729
+ }
1730
+ }
1731
+
1732
+ function runAssertQuiescent(
1733
+ hook: PortableProviderPluginAssertQuiescent,
1734
+ bundleSha256: string,
1735
+ artifactPath: string,
1736
+ ): void {
1737
+ const result: unknown = hook(bundleSha256, artifactPath);
1738
+ if (result !== undefined) {
1739
+ throw new Error(
1740
+ "portable plugin quiescence hook must complete synchronously without returning a value",
1741
+ );
1742
+ }
1743
+ }
1744
+
1745
+ function trustMatchesPackage(
1746
+ trust: PortableProviderPluginTrustRecordV1,
1747
+ packageValue: VerifiedPortableProviderPluginPackage,
1748
+ ): boolean {
1749
+ return (
1750
+ trust.bundleSha256 === packageValue.bundleSha256
1751
+ && trust.pluginId === packageValue.manifest.id
1752
+ && trust.pluginVersion === packageValue.manifest.version
1753
+ && trust.hostApiVersion === packageValue.manifest.hostApiVersion
1754
+ && trust.manifestSha256 === packageValue.manifestSha256
1755
+ && canonicalJson(trust.provenance)
1756
+ === canonicalJson(packageValue.manifest.provenance)
1757
+ );
1758
+ }
1759
+
1760
+ export function installPortableProviderPluginPackage(
1761
+ sourcePath: string,
1762
+ options: {
1763
+ readonly storeRoot: string;
1764
+ readonly approval: unknown;
1765
+ /**
1766
+ * `null` means the plugin must be absent. An update must name the exact
1767
+ * currently active digest; blind replacement is intentionally unsupported.
1768
+ */
1769
+ readonly expectedCurrentBundleSha256: string | null;
1770
+ /**
1771
+ * Kernel catalog check. It receives only the verified immutable package
1772
+ * and runs under the catalog-wide mutation lock.
1773
+ */
1774
+ readonly assertActivatable: PortableProviderPluginAssertActivatable;
1775
+ /**
1776
+ * Kernel lease/journal check for the exact activation being replaced. It is
1777
+ * required for a uniform fail-closed API and invoked only for an update.
1778
+ */
1779
+ readonly assertCurrentQuiescent: PortableProviderPluginAssertQuiescent;
1780
+ readonly now?: Date;
1781
+ /** Test seam for a deterministic activation race. */
1782
+ readonly beforeActivate?: () => void;
1783
+ },
1784
+ ): InstalledPortableProviderPlugin {
1785
+ const source = verifyPortableProviderPluginPackageDirectory(sourcePath);
1786
+ const approval = parsePortableProviderPluginTrustApproval(options.approval);
1787
+ assertPortableProviderPluginTrustApprovalMatches(approval, source);
1788
+ const paths = ensureStore(options.storeRoot);
1789
+ const nowValue = options.now ?? new Date();
1790
+ return withPluginMutationLocks(
1791
+ paths,
1792
+ source.manifest.id,
1793
+ nowValue,
1794
+ () => {
1795
+ const activePath = join(paths.active, `${source.manifest.id}.json`);
1796
+ const current = readPrivateRecordIfPresent(
1797
+ activePath,
1798
+ "portable plugin active record",
1799
+ parseActiveRecord,
1800
+ );
1801
+ assertExpectedActive(current, options.expectedCurrentBundleSha256);
1802
+ const currentInstallation = current === null
1803
+ ? null
1804
+ : loadInstalledWithPaths(paths, source.manifest.id);
1805
+ if (current !== null && currentInstallation === null) {
1806
+ throw new Error("current portable plugin installation is incomplete");
1807
+ }
1808
+
1809
+ const artifactPath = writeVerifiedArtifact(paths.artifacts, source);
1810
+ const storedPackage =
1811
+ verifyPortableProviderPluginPackageDirectory(artifactPath);
1812
+ if (storedPackage.bundleSha256 !== source.bundleSha256) {
1813
+ throw new Error("installed portable plugin artifact digest changed");
1814
+ }
1815
+ const now = canonicalNow(nowValue);
1816
+ const trust: PortableProviderPluginTrustRecordV1 = Object.freeze({
1817
+ schemaVersion: PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION,
1818
+ decision: "trust-executable-code",
1819
+ pluginId: source.manifest.id,
1820
+ pluginVersion: source.manifest.version,
1821
+ hostApiVersion: source.manifest.hostApiVersion,
1822
+ bundleSha256: source.bundleSha256,
1823
+ manifestSha256: source.manifestSha256,
1824
+ provenance: source.manifest.provenance,
1825
+ trustedAt: now,
1826
+ });
1827
+ const trustPath = join(paths.trust, `${source.bundleSha256}.json`);
1828
+ const existingTrust = readPrivateRecordIfPresent(
1829
+ trustPath,
1830
+ "portable plugin trust record",
1831
+ parseTrustRecord,
1832
+ );
1833
+ if (
1834
+ existingTrust !== null
1835
+ && !trustMatchesPackage(existingTrust, source)
1836
+ ) {
1837
+ throw new Error(
1838
+ "portable plugin trust record does not match its content-addressed package",
1839
+ );
1840
+ }
1841
+ const storedTrust = existingTrust ?? writeImmutablePrivateRecord(
1842
+ trustPath,
1843
+ trust,
1844
+ "portable plugin trust record",
1845
+ parseTrustRecord,
1846
+ );
1847
+ const active: PortableProviderPluginActiveRecordV1 = Object.freeze({
1848
+ schemaVersion: PORTABLE_PROVIDER_PLUGIN_STORE_SCHEMA_VERSION,
1849
+ pluginId: source.manifest.id,
1850
+ pluginVersion: source.manifest.version,
1851
+ bundleSha256: source.bundleSha256,
1852
+ activatedAt: now,
1853
+ status: "enabled",
1854
+ });
1855
+ options.beforeActivate?.();
1856
+ const currentAfterWork = readPrivateRecordIfPresent(
1857
+ activePath,
1858
+ "portable plugin active record",
1859
+ parseActiveRecord,
1860
+ );
1861
+ if (canonicalJson(currentAfterWork) !== canonicalJson(current)) {
1862
+ throw new Error(
1863
+ "portable plugin activation changed during installation",
1864
+ );
1865
+ }
1866
+ runAssertActivatable(options.assertActivatable, storedPackage);
1867
+ const currentAfterHook = readPrivateRecordIfPresent(
1868
+ activePath,
1869
+ "portable plugin active record",
1870
+ parseActiveRecord,
1871
+ );
1872
+ if (canonicalJson(currentAfterHook) !== canonicalJson(current)) {
1873
+ throw new Error(
1874
+ "portable plugin activation changed during activatability check",
1875
+ );
1876
+ }
1877
+ if (currentInstallation !== null) {
1878
+ runAssertQuiescent(
1879
+ options.assertCurrentQuiescent,
1880
+ currentInstallation.package.bundleSha256,
1881
+ currentInstallation.artifactPath,
1882
+ );
1883
+ }
1884
+ const currentAfterQuiescence = readPrivateRecordIfPresent(
1885
+ activePath,
1886
+ "portable plugin active record",
1887
+ parseActiveRecord,
1888
+ );
1889
+ if (
1890
+ canonicalJson(currentAfterQuiescence) !== canonicalJson(current)
1891
+ ) {
1892
+ throw new Error(
1893
+ "portable plugin activation changed during current-bundle quiescence check",
1894
+ );
1895
+ }
1896
+ if (currentInstallation !== null) {
1897
+ const installationAfterQuiescence = loadInstalledWithPaths(
1898
+ paths,
1899
+ source.manifest.id,
1900
+ );
1901
+ if (
1902
+ installationAfterQuiescence === null
1903
+ || installationAfterQuiescence.package.bundleSha256
1904
+ !== currentInstallation.package.bundleSha256
1905
+ || installationAfterQuiescence.artifactPath
1906
+ !== currentInstallation.artifactPath
1907
+ ) {
1908
+ throw new Error(
1909
+ "current portable plugin artifact changed during quiescence check",
1910
+ );
1911
+ }
1912
+ }
1913
+ const packageAfterHook =
1914
+ verifyPortableProviderPluginPackageDirectory(artifactPath);
1915
+ if (packageAfterHook.bundleSha256 !== source.bundleSha256) {
1916
+ throw new Error(
1917
+ "portable plugin artifact changed during activatability check",
1918
+ );
1919
+ }
1920
+ writeAtomicPrivateRecord(activePath, active);
1921
+ return Object.freeze({
1922
+ artifactPath,
1923
+ package: packageAfterHook,
1924
+ trust: storedTrust,
1925
+ active,
1926
+ });
1927
+ },
1928
+ );
1929
+ }
1930
+
1931
+ export function disablePortableProviderPluginPackage(
1932
+ storeRoot: string,
1933
+ id: string,
1934
+ options: {
1935
+ readonly expectedBundleSha256: string;
1936
+ /**
1937
+ * Kernel lease/journal check. It receives only immutable artifact identity
1938
+ * and runs under the catalog-wide mutation lock.
1939
+ */
1940
+ readonly assertQuiescent: PortableProviderPluginAssertQuiescent;
1941
+ readonly now?: Date;
1942
+ },
1943
+ ): InstalledPortableProviderPlugin {
1944
+ const paths = openStoreIfPresent(storeRoot);
1945
+ if (paths === null) {
1946
+ throw new Error("portable plugin is not installed");
1947
+ }
1948
+ const safeId = pluginId(id, "installed plugin ID");
1949
+ const nowValue = options.now ?? new Date();
1950
+ return withPluginMutationLocks(
1951
+ paths,
1952
+ safeId,
1953
+ nowValue,
1954
+ () => {
1955
+ const activePath = join(paths.active, `${safeId}.json`);
1956
+ const current = readPrivateRecordIfPresent(
1957
+ activePath,
1958
+ "portable plugin active record",
1959
+ parseActiveRecord,
1960
+ );
1961
+ if (current === null) throw new Error("portable plugin is not installed");
1962
+ assertExpectedActive(current, options.expectedBundleSha256);
1963
+ const installed = loadInstalledWithPaths(paths, safeId);
1964
+ if (installed === null) throw new Error("portable plugin is not installed");
1965
+ if (current.status === "disabled") return installed;
1966
+ const disabled: PortableProviderPluginActiveRecordV1 = Object.freeze({
1967
+ ...current,
1968
+ status: "disabled",
1969
+ disabledAt: canonicalNow(nowValue),
1970
+ });
1971
+ runAssertQuiescent(
1972
+ options.assertQuiescent,
1973
+ installed.package.bundleSha256,
1974
+ installed.artifactPath,
1975
+ );
1976
+ const installedAfterHook = loadInstalledWithPaths(paths, safeId);
1977
+ if (
1978
+ installedAfterHook === null
1979
+ || canonicalJson(installedAfterHook.active) !== canonicalJson(current)
1980
+ || installedAfterHook.package.bundleSha256
1981
+ !== installed.package.bundleSha256
1982
+ || installedAfterHook.artifactPath !== installed.artifactPath
1983
+ ) {
1984
+ throw new Error(
1985
+ "portable plugin installation changed during quiescence check",
1986
+ );
1987
+ }
1988
+ writeAtomicPrivateRecord(activePath, disabled);
1989
+ return Object.freeze({ ...installedAfterHook, active: disabled });
1990
+ },
1991
+ );
1992
+ }
1993
+
1994
+ export function removePortableProviderPluginPackage(
1995
+ storeRoot: string,
1996
+ id: string,
1997
+ options: {
1998
+ readonly expectedBundleSha256: string;
1999
+ /**
2000
+ * Kernel lease/journal check. It receives only immutable artifact identity
2001
+ * and runs under the catalog-wide mutation lock.
2002
+ */
2003
+ readonly assertQuiescent: PortableProviderPluginAssertQuiescent;
2004
+ readonly now?: Date;
2005
+ },
2006
+ ): InstalledPortableProviderPlugin {
2007
+ const paths = openStoreIfPresent(storeRoot);
2008
+ if (paths === null) {
2009
+ throw new Error("portable plugin is not installed");
2010
+ }
2011
+ const safeId = pluginId(id, "installed plugin ID");
2012
+ const nowValue = options.now ?? new Date();
2013
+ return withPluginMutationLocks(
2014
+ paths,
2015
+ safeId,
2016
+ nowValue,
2017
+ () => {
2018
+ const activePath = join(paths.active, `${safeId}.json`);
2019
+ const current = readPrivateRecordIfPresent(
2020
+ activePath,
2021
+ "portable plugin active record",
2022
+ parseActiveRecord,
2023
+ );
2024
+ if (current === null) throw new Error("portable plugin is not installed");
2025
+ assertExpectedActive(current, options.expectedBundleSha256);
2026
+ const installed = loadInstalledWithPaths(paths, safeId);
2027
+ if (installed === null) throw new Error("portable plugin is not installed");
2028
+ runAssertQuiescent(
2029
+ options.assertQuiescent,
2030
+ installed.package.bundleSha256,
2031
+ installed.artifactPath,
2032
+ );
2033
+ const installedAfterHook = loadInstalledWithPaths(paths, safeId);
2034
+ if (
2035
+ installedAfterHook === null
2036
+ || canonicalJson(installedAfterHook.active) !== canonicalJson(current)
2037
+ || installedAfterHook.package.bundleSha256
2038
+ !== installed.package.bundleSha256
2039
+ || installedAfterHook.artifactPath !== installed.artifactPath
2040
+ ) {
2041
+ throw new Error(
2042
+ "portable plugin installation changed during quiescence check",
2043
+ );
2044
+ }
2045
+ unlinkSync(activePath);
2046
+ syncDirectory(paths.active);
2047
+ return installedAfterHook;
2048
+ },
2049
+ );
2050
+ }
2051
+
2052
+ function loadInstalledWithPaths(
2053
+ paths: PortableProviderPluginStorePaths,
2054
+ id: string,
2055
+ ): InstalledPortableProviderPlugin | null {
2056
+ const safeId = pluginId(id, "installed plugin ID");
2057
+ const active = readPrivateRecordIfPresent(
2058
+ join(paths.active, `${safeId}.json`),
2059
+ "portable plugin active record",
2060
+ parseActiveRecord,
2061
+ );
2062
+ if (active === null) return null;
2063
+ if (active.pluginId !== safeId) {
2064
+ throw new Error("portable plugin active record does not match its filename");
2065
+ }
2066
+ const artifactPath = join(paths.artifacts, active.bundleSha256);
2067
+ const packageValue =
2068
+ verifyPortableProviderPluginPackageDirectory(artifactPath);
2069
+ const trust = readPrivateRecordIfPresent(
2070
+ join(paths.trust, `${active.bundleSha256}.json`),
2071
+ "portable plugin trust record",
2072
+ parseTrustRecord,
2073
+ );
2074
+ if (trust === null) {
2075
+ throw new Error("active portable plugin has no executable-code trust record");
2076
+ }
2077
+ if (
2078
+ packageValue.bundleSha256 !== active.bundleSha256
2079
+ || packageValue.manifest.id !== active.pluginId
2080
+ || packageValue.manifest.version !== active.pluginVersion
2081
+ || !trustMatchesPackage(trust, packageValue)
2082
+ ) {
2083
+ throw new Error(
2084
+ "portable plugin artifact, trust, and active records do not share one exact identity",
2085
+ );
2086
+ }
2087
+ return Object.freeze({
2088
+ artifactPath,
2089
+ package: packageValue,
2090
+ trust,
2091
+ active,
2092
+ });
2093
+ }
2094
+
2095
+ export function loadInstalledPortableProviderPlugin(
2096
+ storeRoot: string,
2097
+ id: string,
2098
+ ): InstalledPortableProviderPlugin | null {
2099
+ const paths = openStoreIfPresent(storeRoot);
2100
+ if (paths === null) return null;
2101
+ const installed = loadInstalledWithPaths(paths, id);
2102
+ return installed?.active.status === "enabled" ? installed : null;
2103
+ }
2104
+
2105
+ export function loadPortableProviderPluginInstallation(
2106
+ storeRoot: string,
2107
+ id: string,
2108
+ ): InstalledPortableProviderPlugin | null {
2109
+ const paths = openStoreIfPresent(storeRoot);
2110
+ return paths === null ? null : loadInstalledWithPaths(paths, id);
2111
+ }
2112
+
2113
+ export function listPortableProviderPluginInstallations(
2114
+ storeRoot: string,
2115
+ ): readonly InstalledPortableProviderPlugin[] {
2116
+ const paths = openStoreIfPresent(storeRoot);
2117
+ if (paths === null) return Object.freeze([]);
2118
+ const ids: string[] = [];
2119
+ for (const entry of readdirSync(paths.active, { withFileTypes: true })) {
2120
+ if (
2121
+ !entry.isFile()
2122
+ || entry.isSymbolicLink()
2123
+ || !entry.name.endsWith(".json")
2124
+ ) {
2125
+ throw new Error(
2126
+ `portable plugin active store contains unexpected entry ${entry.name}`,
2127
+ );
2128
+ }
2129
+ ids.push(
2130
+ pluginId(
2131
+ entry.name.slice(0, -".json".length),
2132
+ "installed plugin filename",
2133
+ ),
2134
+ );
2135
+ }
2136
+ ids.sort();
2137
+ return Object.freeze(
2138
+ ids.map((id) => {
2139
+ const installed = loadInstalledWithPaths(paths, id);
2140
+ if (installed === null) {
2141
+ throw new Error("portable plugin disappeared while listing the store");
2142
+ }
2143
+ return installed;
2144
+ }),
2145
+ );
2146
+ }
2147
+
2148
+ export function listInstalledPortableProviderPlugins(
2149
+ storeRoot: string,
2150
+ ): readonly InstalledPortableProviderPlugin[] {
2151
+ return Object.freeze(
2152
+ listPortableProviderPluginInstallations(storeRoot)
2153
+ .filter((installed) => installed.active.status === "enabled"),
2154
+ );
2155
+ }