@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,723 @@
1
+ import {
2
+ closeSync,
3
+ constants,
4
+ fstatSync,
5
+ lstatSync,
6
+ openSync,
7
+ readSync,
8
+ realpathSync,
9
+ type BigIntStats,
10
+ } from "node:fs";
11
+ import { createHash } from "node:crypto";
12
+ import { isAbsolute, parse, resolve, sep } from "node:path";
13
+
14
+ import { BoundedByteBuffer } from "@hraness/kb/clip/bounded-byte-buffer";
15
+ import type { GhostgetAuth } from "./auth";
16
+ import {
17
+ OperationDeadline,
18
+ OperationDeadlineError,
19
+ } from "./operation-deadline";
20
+
21
+ export type OAuthTokenAuth = Extract<GhostgetAuth, { readonly kind: "oauth-token-file" }>;
22
+
23
+ export type LoadedOAuthToken = {
24
+ readonly accessToken: string;
25
+ readonly expiresAt: string | null;
26
+ };
27
+
28
+ export type GoogleInstalledAppRefresh = Readonly<{
29
+ kind: "google-installed-app";
30
+ clientId: string;
31
+ clientSecret: string | null;
32
+ refreshToken: string;
33
+ refreshTokenExpiresAt: string | null;
34
+ }>;
35
+
36
+ export type LoadedOAuthCredential = LoadedOAuthToken & Readonly<{
37
+ schemaVersion: 1 | 2;
38
+ contentSha256: string;
39
+ refresh: GoogleInstalledAppRefresh | null;
40
+ }>;
41
+
42
+ const MAX_TOKEN_FILE_BYTES = 64 * 1024;
43
+ const MAX_ACCESS_TOKEN_BYTES = 16 * 1024;
44
+ const DEFAULT_MINIMUM_TOKEN_VALIDITY_MS = 30_000;
45
+ const PROVIDER_OPERATION_LABEL = "official provider operation";
46
+ const PROVIDER_RESPONSE_CLEANUP_JOIN_MS = 500;
47
+
48
+ function sameIdentity(left: BigIntStats, right: BigIntStats): boolean {
49
+ return left.dev === right.dev && left.ino === right.ino;
50
+ }
51
+
52
+ function ownedByCurrentUser(stats: BigIntStats): boolean {
53
+ const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
54
+ return uid === undefined || stats.uid === BigInt(uid);
55
+ }
56
+
57
+ function canonicalTokenPath(path: string): string {
58
+ const absolute = resolve(path);
59
+ const root = parse(absolute).root;
60
+ const relative = absolute.slice(root.length);
61
+ const segments = relative === "" ? [] : relative.split(sep).filter((segment) => segment !== "");
62
+ let current = root;
63
+ for (const [index, segment] of segments.entries()) {
64
+ const candidate = resolve(current, segment);
65
+ const stats = lstatSync(candidate, { bigint: true });
66
+ if (stats.isSymbolicLink()) {
67
+ // Permit only intermediate, root-owned compatibility aliases such as
68
+ // macOS /var -> /private/var. The token itself and every user-controlled
69
+ // link remain forbidden, and the resolved path is used from this point.
70
+ if (index === segments.length - 1 || process.platform === "win32" || stats.uid !== 0n) {
71
+ throw new Error("OAuth token path cannot contain user-controlled symbolic links");
72
+ }
73
+ current = realpathSync(candidate);
74
+ } else current = candidate;
75
+ }
76
+ return current;
77
+ }
78
+
79
+ function readPrivateTokenFile(path: string): string {
80
+ if (!isAbsolute(path)) throw new Error("OAuth token file path must be absolute");
81
+ const canonical = canonicalTokenPath(path);
82
+ const descriptor = openSync(
83
+ canonical,
84
+ constants.O_RDONLY
85
+ | ("O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0)
86
+ | ("O_NONBLOCK" in constants ? constants.O_NONBLOCK : 0),
87
+ );
88
+ try {
89
+ const before = fstatSync(descriptor, { bigint: true });
90
+ if (
91
+ !before.isFile()
92
+ || !ownedByCurrentUser(before)
93
+ || (process.platform !== "win32" && (before.mode & 0o777n) !== 0o600n)
94
+ || before.size < 1n
95
+ || before.size > BigInt(MAX_TOKEN_FILE_BYTES)
96
+ ) {
97
+ throw new Error("OAuth token file must be a current-user-owned regular file with mode 0600 and at most 65536 bytes");
98
+ }
99
+ const buffer = Buffer.alloc(Number(before.size));
100
+ let offset = 0;
101
+ while (offset < buffer.byteLength) {
102
+ const count = readSync(descriptor, buffer, offset, buffer.byteLength - offset, offset);
103
+ if (count === 0) break;
104
+ offset += count;
105
+ }
106
+ const after = fstatSync(descriptor, { bigint: true });
107
+ if (
108
+ offset !== buffer.byteLength
109
+ || !sameIdentity(before, after)
110
+ || before.size !== after.size
111
+ || before.mtimeNs !== after.mtimeNs
112
+ || before.ctimeNs !== after.ctimeNs
113
+ || before.mode !== after.mode
114
+ ) throw new Error("OAuth token file changed while it was read");
115
+ return new TextDecoder("utf-8", { fatal: true }).decode(buffer);
116
+ } finally {
117
+ closeSync(descriptor);
118
+ }
119
+ }
120
+
121
+ function isRecord(value: unknown): value is Record<string, unknown> {
122
+ return typeof value === "object" && value !== null && !Array.isArray(value);
123
+ }
124
+
125
+ function exactKeys(value: Record<string, unknown>, expected: readonly string[]): boolean {
126
+ const actual = Object.keys(value).sort();
127
+ const sorted = [...expected].sort();
128
+ return actual.length === sorted.length && actual.every((key, index) => key === sorted[index]);
129
+ }
130
+
131
+ function tokenExpiry(value: unknown): string | null {
132
+ if (value === null) return null;
133
+ if (typeof value !== "string" || !Number.isFinite(Date.parse(value))) {
134
+ throw new Error("OAuth token file has an invalid expiresAt timestamp");
135
+ }
136
+ return new Date(value).toISOString();
137
+ }
138
+
139
+ function hasForbiddenAccessTokenCharacter(value: string): boolean {
140
+ for (const character of value) {
141
+ const codePoint = character.codePointAt(0);
142
+ if (codePoint !== undefined && (codePoint <= 0x20 || codePoint === 0x7f)) return true;
143
+ }
144
+ return false;
145
+ }
146
+
147
+ function boundedOAuthSecret(
148
+ value: unknown,
149
+ label: string,
150
+ minimumBytes: number,
151
+ maximumBytes: number,
152
+ ): string {
153
+ if (
154
+ typeof value !== "string"
155
+ || Buffer.byteLength(value, "utf8") < minimumBytes
156
+ || Buffer.byteLength(value, "utf8") > maximumBytes
157
+ || hasForbiddenAccessTokenCharacter(value)
158
+ ) throw new Error(`OAuth token document contains an invalid ${label}`);
159
+ return value;
160
+ }
161
+
162
+ function parseGoogleInstalledAppRefresh(value: unknown): GoogleInstalledAppRefresh {
163
+ if (!isRecord(value)) throw new Error("OAuth token document refresh configuration must be an object");
164
+ if (!exactKeys(value, [
165
+ "kind",
166
+ "clientId",
167
+ "clientSecret",
168
+ "refreshToken",
169
+ "refreshTokenExpiresAt",
170
+ ])) throw new Error("OAuth token document refresh configuration has unsupported fields");
171
+ if (value.kind !== "google-installed-app") {
172
+ throw new Error("OAuth token document has an unsupported refresh configuration");
173
+ }
174
+ const clientId = boundedOAuthSecret(value.clientId, "Google OAuth clientId", 16, 1_024);
175
+ if (!/^[A-Za-z0-9._-]+\.apps\.googleusercontent\.com$/u.test(clientId)) {
176
+ throw new Error("OAuth token document contains an invalid Google OAuth clientId");
177
+ }
178
+ const clientSecret = value.clientSecret === null
179
+ ? null
180
+ : boundedOAuthSecret(value.clientSecret, "Google OAuth clientSecret", 1, 4_096);
181
+ const refreshToken = boundedOAuthSecret(value.refreshToken, "Google OAuth refreshToken", 8, 16 * 1_024);
182
+ const refreshTokenExpiresAt = tokenExpiry(value.refreshTokenExpiresAt);
183
+ return Object.freeze({
184
+ kind: "google-installed-app" as const,
185
+ clientId,
186
+ clientSecret,
187
+ refreshToken,
188
+ refreshTokenExpiresAt,
189
+ });
190
+ }
191
+
192
+ /** Load and strictly bind a private token document without enforcing freshness. */
193
+ export function loadOAuthCredential(auth: OAuthTokenAuth): LoadedOAuthCredential {
194
+ let content: string;
195
+ let parsed: unknown;
196
+ try {
197
+ content = readPrivateTokenFile(auth.path);
198
+ parsed = JSON.parse(content) as unknown;
199
+ } catch (error) {
200
+ throw new Error(`could not load private ${auth.provider} OAuth token document`, { cause: error });
201
+ }
202
+ if (!isRecord(parsed)) throw new Error("OAuth token document must be an object");
203
+ const schemaVersion = parsed.schemaVersion;
204
+ if (schemaVersion !== 1 && schemaVersion !== 2) {
205
+ throw new Error("OAuth token document provider or schema version does not match its locator");
206
+ }
207
+ const expected = ["schemaVersion", "provider", "subject", "scopes", "accessToken", "expiresAt"];
208
+ if (schemaVersion === 2) expected.push("refresh");
209
+ if (!exactKeys(parsed, expected)) throw new Error("OAuth token document has unsupported fields");
210
+ if (parsed.provider !== auth.provider) {
211
+ throw new Error("OAuth token document provider or schema version does not match its locator");
212
+ }
213
+ if ((parsed.subject ?? null) !== (auth.subject ?? null)) {
214
+ throw new Error("OAuth token document subject does not match its locator");
215
+ }
216
+ if (
217
+ !Array.isArray(parsed.scopes)
218
+ || !parsed.scopes.every((scope) => typeof scope === "string")
219
+ || parsed.scopes.length !== auth.scopes.length
220
+ || parsed.scopes.some((scope, index) => scope !== auth.scopes[index])
221
+ ) throw new Error("OAuth token document scopes do not match its locator");
222
+ const accessToken = boundedOAuthSecret(
223
+ parsed.accessToken,
224
+ "accessToken",
225
+ 8,
226
+ MAX_ACCESS_TOKEN_BYTES,
227
+ );
228
+ const expiresAt = tokenExpiry(parsed.expiresAt);
229
+ if (schemaVersion === 1) {
230
+ if (auth.managed === true) {
231
+ throw new Error("Ghostget-managed OAuth auth requires a renewable schema-version-2 credential");
232
+ }
233
+ return Object.freeze({
234
+ schemaVersion,
235
+ accessToken,
236
+ expiresAt,
237
+ refresh: null,
238
+ contentSha256: createHash("sha256").update(content, "utf8").digest("hex"),
239
+ });
240
+ }
241
+ if (auth.managed !== true || auth.provider !== "gmail") {
242
+ throw new Error("renewable OAuth credentials require a Ghostget-managed Gmail auth locator");
243
+ }
244
+ if (expiresAt === null) {
245
+ throw new Error("renewable OAuth credentials require an access-token expiry");
246
+ }
247
+ return Object.freeze({
248
+ schemaVersion,
249
+ accessToken,
250
+ expiresAt,
251
+ refresh: parseGoogleInstalledAppRefresh(parsed.refresh),
252
+ contentSha256: createHash("sha256").update(content, "utf8").digest("hex"),
253
+ });
254
+ }
255
+
256
+ /**
257
+ * Load a canonical private token document. The locator remains secret-free;
258
+ * provider, subject, and scopes are repeated here to prevent a path mix-up
259
+ * from silently granting a different realm.
260
+ */
261
+ export function loadOAuthToken(
262
+ auth: OAuthTokenAuth,
263
+ now = new Date(),
264
+ minimumValidityMs = DEFAULT_MINIMUM_TOKEN_VALIDITY_MS,
265
+ ): LoadedOAuthToken {
266
+ if (
267
+ !Number.isSafeInteger(minimumValidityMs)
268
+ || minimumValidityMs < 0
269
+ ) {
270
+ throw new Error("OAuth token minimum validity budget must be a non-negative safe integer");
271
+ }
272
+ const nowMs = now.getTime();
273
+ if (!Number.isFinite(nowMs)) throw new Error("OAuth token validity reference time is invalid");
274
+ const credential = loadOAuthCredential(auth);
275
+ const expiresAt = credential.expiresAt;
276
+ if (
277
+ expiresAt !== null
278
+ && Date.parse(expiresAt) - nowMs <= minimumValidityMs
279
+ ) {
280
+ const budget = minimumValidityMs === DEFAULT_MINIMUM_TOKEN_VALIDITY_MS
281
+ ? "expires within 30 seconds"
282
+ : `does not remain valid for the required ${minimumValidityMs}ms budget`;
283
+ throw new Error(`the ${auth.provider} OAuth access token is expired or ${budget}; rotate the private token file`);
284
+ }
285
+ return { accessToken: credential.accessToken, expiresAt };
286
+ }
287
+
288
+ export function requireOAuthScopes(
289
+ auth: OAuthTokenAuth,
290
+ alternatives: readonly (readonly string[])[],
291
+ additional: readonly string[] = [],
292
+ ): void {
293
+ const available = new Set(auth.scopes);
294
+ const base = alternatives.find((candidate) => candidate.every((scope) => available.has(scope)));
295
+ if (base === undefined) {
296
+ throw new Error(`OAuth locator ${auth.id} lacks one complete required ${auth.provider} scope set`);
297
+ }
298
+ const missing = additional.filter((scope) => !available.has(scope));
299
+ if (missing.length > 0) throw new Error(`OAuth locator ${auth.id} lacks required scope(s): ${missing.join(", ")}`);
300
+ }
301
+
302
+ export type ProviderFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
303
+
304
+ export type ProviderResponse = {
305
+ readonly status: number;
306
+ readonly headers: Headers;
307
+ readonly body: unknown;
308
+ };
309
+
310
+ export type ProviderResponseMediaType = "application/json";
311
+
312
+ type ProviderResponseReader = {
313
+ readonly read: () => Promise<unknown>;
314
+ readonly cancel: (reason?: unknown) => Promise<void>;
315
+ readonly releaseLock: () => void;
316
+ };
317
+
318
+ async function settlesWithin<T>(
319
+ operation: Promise<T>,
320
+ maximumMs: number,
321
+ ): Promise<PromiseSettledResult<T> | null> {
322
+ let timer: ReturnType<typeof setTimeout> | undefined;
323
+ try {
324
+ return await Promise.race([
325
+ operation.then(
326
+ (value): PromiseFulfilledResult<T> => ({
327
+ status: "fulfilled",
328
+ value,
329
+ }),
330
+ (reason: unknown): PromiseRejectedResult => ({
331
+ status: "rejected",
332
+ reason,
333
+ }),
334
+ ),
335
+ new Promise<null>((resolve) => {
336
+ timer = setTimeout(() => resolve(null), maximumMs);
337
+ }),
338
+ ]);
339
+ } finally {
340
+ if (timer !== undefined) clearTimeout(timer);
341
+ }
342
+ }
343
+
344
+ function combineCleanupFailures(
345
+ current: Error | null,
346
+ next: Error,
347
+ ): Error {
348
+ return current === null
349
+ ? next
350
+ : new AggregateError(
351
+ [current, next],
352
+ "multiple official provider response cleanup checks failed",
353
+ );
354
+ }
355
+
356
+ function responseCleanupVerificationError(
357
+ primaryFailure: unknown,
358
+ cleanupFailure: Error,
359
+ ): AggregateError {
360
+ const primary = primaryFailure instanceof Error
361
+ ? primaryFailure
362
+ : new Error("official provider response processing failed", {
363
+ cause: primaryFailure,
364
+ });
365
+ return new AggregateError(
366
+ [primary, cleanupFailure],
367
+ `${primary.message}; official provider response cleanup could not be verified`,
368
+ );
369
+ }
370
+
371
+ async function cancellationCleanupFailure(
372
+ cancellation: Promise<unknown>,
373
+ concurrentOperations: readonly Promise<unknown>[],
374
+ diagnostic: string,
375
+ ): Promise<{
376
+ readonly failure: Error | null;
377
+ readonly settled: boolean;
378
+ }> {
379
+ const quiescence = Promise.allSettled([
380
+ ...concurrentOperations,
381
+ cancellation,
382
+ ]).then((results) => {
383
+ const cancellationResult = results.at(-1);
384
+ if (cancellationResult?.status === "rejected") {
385
+ throw cancellationResult.reason;
386
+ }
387
+ });
388
+ const settled = await settlesWithin(
389
+ quiescence,
390
+ PROVIDER_RESPONSE_CLEANUP_JOIN_MS,
391
+ );
392
+ if (settled !== null && settled.status === "fulfilled") {
393
+ return { failure: null, settled: true };
394
+ }
395
+ return {
396
+ failure: new Error(
397
+ diagnostic,
398
+ settled?.status === "rejected"
399
+ ? { cause: settled.reason }
400
+ : undefined,
401
+ ),
402
+ settled: settled !== null,
403
+ };
404
+ }
405
+
406
+ async function cancelResponseBody(
407
+ body: ReadableStream<Uint8Array> | null,
408
+ ): Promise<Error | null> {
409
+ if (body === null) return null;
410
+ const cancellation = Promise.resolve().then(() =>
411
+ body.cancel("official provider response was rejected")
412
+ );
413
+ return (await cancellationCleanupFailure(
414
+ cancellation,
415
+ [],
416
+ "official provider response body cleanup did not settle after cancellation",
417
+ )).failure;
418
+ }
419
+
420
+ function releaseReaderAfterPendingRead(
421
+ reader: ProviderResponseReader,
422
+ pendingRead: Promise<unknown>,
423
+ ): void {
424
+ const release = (): void => {
425
+ try {
426
+ reader.releaseLock();
427
+ } catch {
428
+ // The caller already failed closed because synchronous cleanup could not
429
+ // be verified. This late path only makes eventual lock release possible.
430
+ }
431
+ };
432
+ void pendingRead.then(release, release);
433
+ }
434
+
435
+ async function joinLateResponseCleanup(
436
+ fetchOperation: Promise<Response>,
437
+ deadlineError: OperationDeadlineError,
438
+ ): Promise<void> {
439
+ const cleanup = fetchOperation.then(
440
+ async (response) => {
441
+ await response.body?.cancel("official provider request deadline expired");
442
+ },
443
+ () => undefined,
444
+ );
445
+ const settled = await settlesWithin(
446
+ cleanup,
447
+ PROVIDER_RESPONSE_CLEANUP_JOIN_MS,
448
+ );
449
+ if (settled !== null && settled.status === "fulfilled") return;
450
+ const cleanupError = new Error(
451
+ "official provider response cleanup did not settle after cancellation",
452
+ settled?.status === "rejected"
453
+ ? { cause: settled.reason }
454
+ : undefined,
455
+ );
456
+ throw new AggregateError(
457
+ [deadlineError, cleanupError],
458
+ `${deadlineError.message}; official provider response cleanup could not be verified`,
459
+ );
460
+ }
461
+
462
+ function hasOversizedContentLength(response: Response, maximumBytes: number): boolean {
463
+ const declared = response.headers.get("content-length");
464
+ if (declared === null || !/^[0-9]+$/u.test(declared)) return false;
465
+ return BigInt(declared) > BigInt(maximumBytes);
466
+ }
467
+
468
+ async function boundedResponseText(
469
+ response: Response,
470
+ maximumBytes: number,
471
+ deadline: OperationDeadline | null,
472
+ ): Promise<string> {
473
+ const body = response.body;
474
+ if (body === null) {
475
+ deadline?.throwIfUnavailable(PROVIDER_OPERATION_LABEL);
476
+ return "";
477
+ }
478
+ let reader: ProviderResponseReader | null = null;
479
+ let pendingRead: Promise<unknown> | null = null;
480
+ let pendingReadSettled = true;
481
+ let deferReaderRelease = false;
482
+ let failed = false;
483
+ let failure: unknown;
484
+ let cleanupFailure: Error | null = null;
485
+ let text: string | undefined;
486
+ const output = new BoundedByteBuffer(maximumBytes);
487
+ try {
488
+ deadline?.throwIfUnavailable(PROVIDER_OPERATION_LABEL);
489
+ if (hasOversizedContentLength(response, maximumBytes)) {
490
+ throw new Error(`provider response exceeds ${maximumBytes} bytes`);
491
+ }
492
+ const activeReader: ProviderResponseReader = body.getReader();
493
+ reader = activeReader;
494
+ for (;;) {
495
+ pendingReadSettled = false;
496
+ pendingRead = activeReader.read();
497
+ void pendingRead.then(
498
+ () => {
499
+ pendingReadSettled = true;
500
+ },
501
+ () => {
502
+ pendingReadSettled = true;
503
+ },
504
+ );
505
+ const next = deadline === null
506
+ ? await pendingRead
507
+ : await deadline.run(
508
+ () => pendingRead as Promise<unknown>,
509
+ PROVIDER_OPERATION_LABEL,
510
+ );
511
+ pendingRead = null;
512
+ if (!isRecord(next) || typeof next.done !== "boolean") {
513
+ throw new Error("provider response returned an invalid body read result");
514
+ }
515
+ if (next.done) break;
516
+ const chunk = next.value;
517
+ if (!(chunk instanceof Uint8Array)) throw new Error("provider response returned an invalid body chunk");
518
+ if (!output.append(chunk)) throw new Error(`provider response exceeds ${maximumBytes} bytes`);
519
+ }
520
+ text = new TextDecoder("utf-8", { fatal: true }).decode(output.toUint8Array());
521
+ deadline?.throwIfUnavailable(PROVIDER_OPERATION_LABEL);
522
+ } catch (error) {
523
+ failed = true;
524
+ failure = error;
525
+ if (reader === null) {
526
+ cleanupFailure = await cancelResponseBody(body);
527
+ } else {
528
+ const activeReader = reader;
529
+ const cancellation = Promise.resolve().then(() =>
530
+ activeReader.cancel("official provider response processing stopped")
531
+ );
532
+ const cleanup = await cancellationCleanupFailure(
533
+ cancellation,
534
+ pendingRead === null ? [] : [pendingRead],
535
+ "official provider response reader cleanup did not settle after cancellation",
536
+ );
537
+ cleanupFailure = cleanup.failure;
538
+ deferReaderRelease = !cleanup.settled
539
+ && pendingRead !== null
540
+ && !pendingReadSettled;
541
+ }
542
+ } finally {
543
+ if (reader !== null) {
544
+ if (deferReaderRelease && pendingRead !== null) {
545
+ releaseReaderAfterPendingRead(reader, pendingRead);
546
+ } else {
547
+ try {
548
+ reader.releaseLock();
549
+ } catch (error) {
550
+ cleanupFailure = combineCleanupFailures(
551
+ cleanupFailure,
552
+ new Error(
553
+ "official provider response reader lock could not be released",
554
+ { cause: error },
555
+ ),
556
+ );
557
+ }
558
+ }
559
+ }
560
+ }
561
+ if (failed) {
562
+ if (cleanupFailure !== null) {
563
+ throw responseCleanupVerificationError(failure, cleanupFailure);
564
+ }
565
+ throw failure;
566
+ }
567
+ if (cleanupFailure !== null) {
568
+ throw new Error(
569
+ "official provider response cleanup could not be verified",
570
+ { cause: cleanupFailure },
571
+ );
572
+ }
573
+ if (text === undefined) {
574
+ throw new Error("official provider response processing did not produce text");
575
+ }
576
+ return text;
577
+ }
578
+
579
+ function safePath(url: URL): string {
580
+ return `${url.pathname}${url.search === "" ? "" : "?…"}`;
581
+ }
582
+
583
+ export class ProviderHttpClient {
584
+ readonly #fetch: ProviderFetch;
585
+ readonly #operationDeadline: OperationDeadline | null;
586
+ readonly #legacyDeadlineMs: number | null;
587
+ readonly #maximumBytes: number;
588
+
589
+ constructor(
590
+ fetch_: ProviderFetch,
591
+ timeoutOrDeadline: number | OperationDeadline,
592
+ maximumBytes: number,
593
+ ) {
594
+ this.#fetch = fetch_;
595
+ this.#operationDeadline = timeoutOrDeadline instanceof OperationDeadline
596
+ ? timeoutOrDeadline
597
+ : null;
598
+ this.#legacyDeadlineMs = typeof timeoutOrDeadline === "number"
599
+ ? Date.now() + timeoutOrDeadline
600
+ : null;
601
+ this.#maximumBytes = maximumBytes;
602
+ }
603
+
604
+ /** Remaining operation-wide budget shared by requests and provider polling. */
605
+ remainingTimeMs(): number {
606
+ return this.#operationDeadline?.remainingTimeMs()
607
+ ?? Math.max(0, (this.#legacyDeadlineMs ?? 0) - Date.now());
608
+ }
609
+
610
+ /** Fail synchronously when CPU-bound response projection has exhausted the shared deadline. */
611
+ throwIfUnavailable(): void {
612
+ this.#operationDeadline?.throwIfUnavailable(PROVIDER_OPERATION_LABEL);
613
+ if (this.remainingTimeMs() < 1) {
614
+ throw new Error("official provider operation timed out during response projection");
615
+ }
616
+ }
617
+
618
+ async request(
619
+ urlValue: string | URL,
620
+ init: RequestInit,
621
+ expectedStatuses: readonly number[],
622
+ allowedHosts: readonly string[],
623
+ maximumBytes = this.#maximumBytes,
624
+ responseMediaType?: ProviderResponseMediaType,
625
+ ): Promise<ProviderResponse> {
626
+ if (
627
+ !Number.isSafeInteger(maximumBytes)
628
+ || maximumBytes < 1
629
+ || maximumBytes > this.#maximumBytes
630
+ ) {
631
+ throw new Error("official provider request response limit must be a positive safe integer within the client ceiling");
632
+ }
633
+ if (responseMediaType !== undefined && responseMediaType !== "application/json") {
634
+ throw new Error("official provider request has an unsupported response media-type policy");
635
+ }
636
+ const url = new URL(urlValue);
637
+ if (
638
+ url.protocol !== "https:"
639
+ || url.username !== ""
640
+ || url.password !== ""
641
+ || url.port !== ""
642
+ || !allowedHosts.includes(url.hostname)
643
+ ) throw new Error("official provider request attempted an unapproved origin");
644
+ this.#operationDeadline?.throwIfUnavailable(PROVIDER_OPERATION_LABEL);
645
+ const remaining = this.remainingTimeMs();
646
+ if (remaining < 1) throw new Error("official provider operation timed out before its next request");
647
+ let response: Response;
648
+ let fetchOperation: Promise<Response> | undefined;
649
+ try {
650
+ response = this.#operationDeadline === null
651
+ ? await this.#fetch(url, {
652
+ ...init,
653
+ redirect: "error",
654
+ signal: AbortSignal.timeout(remaining),
655
+ })
656
+ : await this.#operationDeadline.run(
657
+ (signal) => {
658
+ fetchOperation = Promise.resolve().then(() =>
659
+ this.#fetch(url, {
660
+ ...init,
661
+ redirect: "error",
662
+ signal,
663
+ })
664
+ );
665
+ return fetchOperation;
666
+ },
667
+ PROVIDER_OPERATION_LABEL,
668
+ );
669
+ } catch (error) {
670
+ if (error instanceof OperationDeadlineError) {
671
+ if (fetchOperation !== undefined) {
672
+ await joinLateResponseCleanup(fetchOperation, error);
673
+ }
674
+ throw error;
675
+ }
676
+ throw new Error(`official provider request did not return a response for ${init.method ?? "GET"} ${safePath(url)}`, { cause: error });
677
+ }
678
+ if (!expectedStatuses.includes(response.status)) {
679
+ const failure = new Error(`official provider returned HTTP ${response.status} for ${init.method ?? "GET"} ${safePath(url)}`);
680
+ const cleanupFailure = await cancelResponseBody(response.body);
681
+ if (cleanupFailure !== null) {
682
+ throw responseCleanupVerificationError(failure, cleanupFailure);
683
+ }
684
+ throw failure;
685
+ }
686
+ if (responseMediaType === "application/json") {
687
+ const contentType = response.headers.get("content-type");
688
+ if (
689
+ contentType === null
690
+ || contentType.length > 256
691
+ || !/^application\/json(?:\s*;\s*charset\s*=\s*(?:utf-8|"utf-8"))?\s*$/iu.test(contentType)
692
+ ) {
693
+ const failure = new Error(
694
+ `official provider returned an unsupported response media type for ${init.method ?? "GET"} ${safePath(url)}`,
695
+ );
696
+ const cleanupFailure = await cancelResponseBody(response.body);
697
+ if (cleanupFailure !== null) {
698
+ throw responseCleanupVerificationError(failure, cleanupFailure);
699
+ }
700
+ throw failure;
701
+ }
702
+ }
703
+ const text = await boundedResponseText(
704
+ response,
705
+ maximumBytes,
706
+ this.#operationDeadline,
707
+ );
708
+ if (text === "") return { status: response.status, headers: response.headers, body: null };
709
+ try {
710
+ const body = JSON.parse(text) as unknown;
711
+ this.#operationDeadline?.throwIfUnavailable(PROVIDER_OPERATION_LABEL);
712
+ return { status: response.status, headers: response.headers, body };
713
+ } catch (error) {
714
+ throw new Error(`official provider returned malformed JSON for ${init.method ?? "GET"} ${safePath(url)}`, { cause: error });
715
+ }
716
+ }
717
+ }
718
+
719
+ export function bearerHeaders(accessToken: string, extra: Readonly<Record<string, string>> = {}): Headers {
720
+ const headers = new Headers(extra);
721
+ headers.set("Authorization", `Bearer ${accessToken}`);
722
+ return headers;
723
+ }