@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,683 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import type { GhostgetAuth } from "../auth";
4
+ import {
5
+ PreservedBrowserArtifactsError,
6
+ browserResultData,
7
+ createBrowserSession,
8
+ runCommand,
9
+ type BrowserSession,
10
+ type CommandResult,
11
+ type CreateBrowserSessionOptions,
12
+ } from "../browser";
13
+ import type { GhostgetManifest } from "../model";
14
+ import {
15
+ buildLinkedInProfileContactInfoGraphqlPath,
16
+ type LinkedInContactInfoJsonInput,
17
+ } from "./linkedin-web-contact";
18
+ import type {
19
+ WebSessionCleanupResourcePublisher,
20
+ WebSessionOperationDeadline,
21
+ } from "../web-session-execution";
22
+ import { jsonScriptLiteral } from "../canonical-json";
23
+
24
+ const LINKEDIN_ORIGIN = "https://www.linkedin.com";
25
+ const LINKEDIN_FEED_URL = `${LINKEDIN_ORIGIN}/feed/`;
26
+ const LINKEDIN_PRE_COOKIE_REALM_URL = `${LINKEDIN_ORIGIN}/robots.txt`;
27
+ const LINKEDIN_INITIAL_ROOT_BATCH = JSON.stringify([
28
+ ["open", LINKEDIN_ORIGIN],
29
+ ]);
30
+ const LINKEDIN_INITIAL_BLANK_BATCH = JSON.stringify([
31
+ ["open", "about:blank"],
32
+ ]);
33
+ const LINKEDIN_INITIAL_REALM_BATCH = JSON.stringify([
34
+ ["open", LINKEDIN_PRE_COOKIE_REALM_URL],
35
+ ]);
36
+ const LINKEDIN_CONNECTIONS_URL =
37
+ `${LINKEDIN_ORIGIN}/mynetwork/invite-connect/connections/`;
38
+ const MAX_IDENTITY_BYTES = 2 * 1024 * 1024;
39
+ const MAX_STATS_PAGE_BYTES = 8 * 1024 * 1024;
40
+ const BROWSER_ENVELOPE_BYTES = 64 * 1024;
41
+
42
+ const profileBrowserManifest: GhostgetManifest = Object.freeze({
43
+ schemaVersion: 4,
44
+ id: "linkedin-profile-runtime",
45
+ version: "1.0.0",
46
+ displayName: "LinkedIn profile stats runtime",
47
+ surfaceId: "linkedin",
48
+ origins: Object.freeze([LINKEDIN_ORIGIN]),
49
+ browserDomains: Object.freeze(["www.linkedin.com", "static.licdn.com"]),
50
+ operations: Object.freeze({}),
51
+ });
52
+
53
+ export type LinkedInProfileBrowserTransport = {
54
+ readonly currentIdentityResponse: () => Promise<unknown>;
55
+ readonly readProfileHtml: (profileUrl: string) => Promise<string>;
56
+ readonly readConnectionsHtml: (profileUrl: string) => Promise<string>;
57
+ readonly readContactInfoJson: (input: LinkedInContactInfoJsonInput) => Promise<unknown>;
58
+ readonly readOrganizationHtml: (organizationUrl: string) => Promise<string>;
59
+ readonly close: () => Promise<void>;
60
+ };
61
+
62
+ export type LinkedInProfileBrowserDependencies = {
63
+ readonly createBrowserSession: typeof createBrowserSession;
64
+ readonly runCommand: typeof runCommand;
65
+ readonly settleContext: () => Promise<void>;
66
+ };
67
+
68
+ type BrowserReadBinding = {
69
+ readonly kind: "html" | "json";
70
+ readonly maxBytes: number;
71
+ readonly path: string;
72
+ readonly referrer: string;
73
+ };
74
+
75
+ type BrowserReadState = "ready" | "identity" | "profile" | "complete";
76
+
77
+ const LINKEDIN_RESPONSE_MEDIA_TYPE =
78
+ /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/u;
79
+
80
+ export type LinkedInProfileBrowserFailureCategory =
81
+ | "authwall"
82
+ | "body-envelope"
83
+ | "bootstrap"
84
+ | "browser-envelope"
85
+ | "browser-command"
86
+ | "execution-context"
87
+ | "identity-json"
88
+ | "output-bound"
89
+ | "provider-fetch"
90
+ | "response-envelope"
91
+ | "response-rejected"
92
+ | "session-cookie"
93
+ | "startup";
94
+
95
+ export class LinkedInProfileBrowserFailure extends Error {
96
+ readonly category: LinkedInProfileBrowserFailureCategory;
97
+
98
+ constructor(category: LinkedInProfileBrowserFailureCategory, message: string) {
99
+ super(message);
100
+ this.name = "LinkedInProfileBrowserFailure";
101
+ this.category = category;
102
+ }
103
+ }
104
+
105
+ export class LinkedInProfileBrowserResponseRejectedError
106
+ extends LinkedInProfileBrowserFailure {
107
+ readonly status: number;
108
+ readonly contentType: string;
109
+
110
+ constructor(status: number, contentType: string) {
111
+ super(
112
+ "response-rejected",
113
+ "LinkedIn stats browser request returned a reviewed rejection",
114
+ );
115
+ this.name = "LinkedInProfileBrowserResponseRejectedError";
116
+ if (
117
+ !Number.isSafeInteger(status)
118
+ || status < 100
119
+ || status > 599
120
+ || contentType.length > 128
121
+ || (contentType !== "" && !LINKEDIN_RESPONSE_MEDIA_TYPE.test(contentType))
122
+ ) throw new Error("LinkedIn stats browser returned a malformed response category");
123
+ this.status = status;
124
+ this.contentType = contentType === "" ? "missing" : contentType;
125
+ }
126
+ }
127
+
128
+ function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
129
+ return typeof value === "object" && value !== null && !Array.isArray(value);
130
+ }
131
+
132
+ function exactKeys(
133
+ value: Readonly<Record<string, unknown>>,
134
+ expected: readonly string[],
135
+ label: string,
136
+ ): void {
137
+ if (Object.keys(value).sort().join(",") !== [...expected].sort().join(",")) {
138
+ throw new Error(`${label} returned an unexpected result shape`);
139
+ }
140
+ }
141
+
142
+ function exactLinkedInUrl(
143
+ value: string,
144
+ kind: "organization" | "profile",
145
+ ): URL {
146
+ let url: URL;
147
+ try {
148
+ url = new URL(value);
149
+ } catch {
150
+ throw new Error(`LinkedIn ${kind} browser target is invalid`);
151
+ }
152
+ const pathAllowed = kind === "profile"
153
+ ? /^\/in\/[A-Za-z0-9_-]{1,256}\/$/u.test(url.pathname)
154
+ : /^\/company\/[a-z0-9][a-z0-9-]{0,255}\/$/u.test(url.pathname);
155
+ if (
156
+ url.origin !== LINKEDIN_ORIGIN
157
+ || url.username !== ""
158
+ || url.password !== ""
159
+ || url.search !== ""
160
+ || url.hash !== ""
161
+ || !pathAllowed
162
+ ) throw new Error(`LinkedIn ${kind} browser target escaped its reviewed route`);
163
+ return url;
164
+ }
165
+
166
+ function browserReadEvaluationSource(binding: BrowserReadBinding): string {
167
+ const bound = jsonScriptLiteral(binding);
168
+ return `(async()=>{const input=${bound};if(location.origin!=="${LINKEDIN_ORIGIN}")throw new Error("unexpected LinkedIn origin");if((input.kind!=="json"&&input.kind!=="html")||!Number.isSafeInteger(input.maxBytes)||input.maxBytes<1||input.maxBytes>${MAX_STATS_PAGE_BYTES})throw new Error("invalid LinkedIn stats browser request binding");const expected=new URL(input.path,"${LINKEDIN_ORIGIN}");if(expected.origin!=="${LINKEDIN_ORIGIN}"||expected.username!==""||expected.password!==""||expected.hash!==""||expected.href!=="${LINKEDIN_ORIGIN}"+input.path)throw new Error("invalid LinkedIn stats browser path binding");const headers=input.kind==="json"?{accept:"application/vnd.linkedin.normalized+json+2.1","x-li-lang":"en_US","x-requested-with":"XMLHttpRequest","x-restli-protocol-version":"2.0.0"}:{accept:"text/html"};if(input.kind==="json"){const raw=document.cookie.split("; ").find((part)=>part.startsWith("JSESSIONID="));if(typeof raw!=="string")throw new Error("missing LinkedIn browser CSRF cookie");const csrf=decodeURIComponent(raw.slice("JSESSIONID=".length)).replace(/^\"|\"$/g,"");if(!/^ajax:[A-Za-z0-9_-]{1,512}$/.test(csrf))throw new Error("invalid LinkedIn browser CSRF cookie");headers["csrf-token"]=csrf}const response=await fetch(input.path,{credentials:"include",headers,method:"GET",redirect:"error",referrer:input.referrer});const responseUrl=new URL(response.url);if(responseUrl.origin!=="${LINKEDIN_ORIGIN}"||responseUrl.username!==""||responseUrl.password!==""||responseUrl.hash!==""||responseUrl.href!==expected.href)throw new Error("LinkedIn stats browser response escaped its exact route");const contentType=(response.headers.get("content-type")||"").split(";",1)[0].trim().toLowerCase();const contentTypeAllowed=input.kind==="json"?(contentType==="application/vnd.linkedin.normalized+json+2.1"||contentType==="application/json"):contentType==="text/html";if(response.status!==200||!contentTypeAllowed){response.body?.cancel();return{authWall:false,bodyBase64:null,bodyBytes:0,bodySha256:null,contentType,status:response.status}}if(response.body===null)throw new Error("LinkedIn stats browser response omitted its body");const reader=response.body.getReader();const chunks=[];let bytes=0;while(true){const part=await reader.read();if(part.done)break;bytes+=part.value.byteLength;if(bytes>input.maxBytes){await reader.cancel();throw new Error("LinkedIn stats browser response exceeded its reviewed byte bound")}chunks.push(part.value)}const body=new Uint8Array(bytes);let cursor=0;for(const chunk of chunks){body.set(chunk,cursor);cursor+=chunk.byteLength}const text=new TextDecoder("utf-8",{fatal:true}).decode(body);const authWall=input.kind==="html"&&/(?:id|data-test-id)=[\"']authwall[\"']|name=[\"']loginCsrfParam[\"']|<form[^>]+(?:login|sign-in)/iu.test(text);if(authWall)return{authWall:true,bodyBase64:null,bodyBytes:0,bodySha256:null,contentType,status:response.status};const digest=Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256",body)),(value)=>value.toString(16).padStart(2,"0")).join("");let binary="";for(let offset=0;offset<body.length;offset+=32768)binary+=String.fromCharCode(...body.subarray(offset,Math.min(offset+32768,body.length)));return{authWall:false,bodyBase64:btoa(binary),bodyBytes:body.byteLength,bodySha256:digest,contentType,status:response.status}})()`;
169
+ }
170
+
171
+ function encodedBodyBound(bytes: number): number {
172
+ return Math.ceil(bytes / 3) * 4;
173
+ }
174
+
175
+ function decodedBody(
176
+ result: Readonly<Record<string, unknown>>,
177
+ maximumBytes: number,
178
+ ): string {
179
+ if (
180
+ typeof result.bodyBase64 !== "string"
181
+ || result.bodyBase64.length > encodedBodyBound(maximumBytes)
182
+ || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(
183
+ result.bodyBase64,
184
+ )
185
+ || !Number.isSafeInteger(result.bodyBytes)
186
+ || (result.bodyBytes as number) < 0
187
+ || (result.bodyBytes as number) > maximumBytes
188
+ || typeof result.bodySha256 !== "string"
189
+ || !/^[a-f0-9]{64}$/u.test(result.bodySha256)
190
+ ) throw new LinkedInProfileBrowserFailure(
191
+ "body-envelope",
192
+ "LinkedIn stats browser body envelope changed shape",
193
+ );
194
+ const bytes = Buffer.from(result.bodyBase64, "base64");
195
+ if (
196
+ bytes.byteLength !== result.bodyBytes
197
+ || bytes.toString("base64") !== result.bodyBase64
198
+ || createHash("sha256").update(bytes).digest("hex") !== result.bodySha256
199
+ ) throw new LinkedInProfileBrowserFailure(
200
+ "body-envelope",
201
+ "LinkedIn stats browser body envelope failed integrity verification",
202
+ );
203
+ try {
204
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
205
+ } catch {
206
+ throw new LinkedInProfileBrowserFailure(
207
+ "body-envelope",
208
+ "LinkedIn stats browser body was not valid UTF-8",
209
+ );
210
+ }
211
+ }
212
+
213
+ function browserEvaluationResult(
214
+ value: Readonly<Record<string, unknown>>,
215
+ ): Readonly<Record<string, unknown>> {
216
+ const data = browserResultData(value as Record<string, unknown>);
217
+ if (!isRecord(data) || typeof data.origin !== "string" || !isRecord(data.result)) {
218
+ throw new LinkedInProfileBrowserFailure(
219
+ "response-envelope",
220
+ "LinkedIn stats browser returned a malformed evaluation envelope",
221
+ );
222
+ }
223
+ let origin: URL;
224
+ try {
225
+ origin = new URL(data.origin);
226
+ } catch {
227
+ throw new LinkedInProfileBrowserFailure(
228
+ "response-envelope",
229
+ "LinkedIn stats browser returned a malformed evaluation envelope",
230
+ );
231
+ }
232
+ if (
233
+ origin.origin !== LINKEDIN_ORIGIN
234
+ || origin.username !== ""
235
+ || origin.password !== ""
236
+ ) throw new LinkedInProfileBrowserFailure(
237
+ "response-envelope",
238
+ "LinkedIn stats browser returned a malformed evaluation envelope",
239
+ );
240
+ return data.result;
241
+ }
242
+
243
+ function hasNoDefaultExecutionContext(error: unknown): boolean {
244
+ let current: unknown = error;
245
+ for (let depth = 0; depth < 8 && current !== undefined; depth += 1) {
246
+ if (
247
+ current instanceof Error
248
+ && /(?:cannot find|no) default execution context/iu.test(current.message)
249
+ ) return true;
250
+ current = current instanceof Error ? current.cause : undefined;
251
+ }
252
+ return false;
253
+ }
254
+
255
+ function hasUnexpectedLinkedInOrigin(error: unknown): boolean {
256
+ let current: unknown = error;
257
+ for (let depth = 0; depth < 8 && current !== undefined; depth += 1) {
258
+ if (
259
+ current instanceof Error
260
+ && /(?:^|: )unexpected LinkedIn origin(?:$|[\r\n])/u.test(current.message)
261
+ ) return true;
262
+ current = current instanceof Error ? current.cause : undefined;
263
+ }
264
+ return false;
265
+ }
266
+
267
+ function classifiedBrowserCommandFailure(
268
+ error: unknown,
269
+ ): LinkedInProfileBrowserFailure {
270
+ const message = error instanceof Error && error.message.length <= 1_500
271
+ ? error.message
272
+ : "";
273
+ if (
274
+ /(?:^|: )(?:missing|invalid) LinkedIn browser CSRF cookie$/u.test(message)
275
+ ) {
276
+ return new LinkedInProfileBrowserFailure(
277
+ "session-cookie",
278
+ "LinkedIn stats browser could not establish its reviewed CSRF cookie",
279
+ );
280
+ }
281
+ if (message.includes("Failed to fetch")) {
282
+ return new LinkedInProfileBrowserFailure(
283
+ "provider-fetch",
284
+ "LinkedIn stats browser could not complete its first-party fetch",
285
+ );
286
+ }
287
+ if (message.endsWith("LinkedIn stats browser response escaped its exact route")) {
288
+ return new LinkedInProfileBrowserFailure(
289
+ "response-envelope",
290
+ "LinkedIn stats browser response escaped its exact route",
291
+ );
292
+ }
293
+ if (
294
+ message.includes("process output exceeded")
295
+ || message.includes("response exceeded its reviewed byte bound")
296
+ ) {
297
+ return new LinkedInProfileBrowserFailure(
298
+ "output-bound",
299
+ "LinkedIn stats browser exceeded a reviewed output bound",
300
+ );
301
+ }
302
+ if (
303
+ message.includes("malformed batch")
304
+ || message.includes("malformed batch entry")
305
+ || message.includes("did not return JSON")
306
+ || message.includes("command omitted its result")
307
+ ) {
308
+ return new LinkedInProfileBrowserFailure(
309
+ "browser-envelope",
310
+ "LinkedIn stats browser command returned a malformed envelope",
311
+ );
312
+ }
313
+ return new LinkedInProfileBrowserFailure(
314
+ "browser-command",
315
+ "LinkedIn stats browser command failed before a reviewed response",
316
+ );
317
+ }
318
+
319
+ function contextSettlementRejected(result: CommandResult): boolean {
320
+ return result.exitCode !== 0
321
+ && /Failed to install browser network controls:[^\r\n]{0,256}Cannot find default execution context/u.test(
322
+ `${result.stderr}\n${result.stdout}`,
323
+ );
324
+ }
325
+
326
+ function commandWasAborted(options: Parameters<typeof runCommand>[1]): boolean {
327
+ return options.signal?.aborted === true;
328
+ }
329
+
330
+ function linkedInProfileBrowserCommandRunner(
331
+ execute: typeof runCommand,
332
+ settleContext: () => Promise<void>,
333
+ authKind: GhostgetAuth["kind"],
334
+ ): typeof runCommand {
335
+ let initialBatchPending = true;
336
+ return async (command, options) => {
337
+ const rewroteInitialRoot = initialBatchPending
338
+ && options.stdin === LINKEDIN_INITIAL_ROOT_BATCH;
339
+ const rewroteInitialBlank = initialBatchPending
340
+ && authKind === "browser-profile"
341
+ && options.stdin === LINKEDIN_INITIAL_BLANK_BATCH;
342
+ const executionOptions = rewroteInitialRoot || rewroteInitialBlank
343
+ ? { ...options, stdin: LINKEDIN_INITIAL_REALM_BATCH }
344
+ : options;
345
+ initialBatchPending = false;
346
+ const first = await execute(command, executionOptions);
347
+ if (
348
+ !rewroteInitialRoot
349
+ || authKind === "browser-profile"
350
+ || !contextSettlementRejected(first)
351
+ || commandWasAborted(executionOptions)
352
+ ) return first;
353
+ // Only a root navigation from a cookie-importing auth kind is safe to
354
+ // repeat: it occurs before any cookies are imported. Browser profiles may
355
+ // already contain cookies when their initial about:blank is rewritten, so
356
+ // that navigation and every later command remain single-attempt.
357
+ await settleContext();
358
+ if (commandWasAborted(executionOptions)) return first;
359
+ return execute(command, executionOptions);
360
+ };
361
+ }
362
+
363
+ function settleLinkedInBrowserContext(): Promise<void> {
364
+ return new Promise((resolve) => {
365
+ setTimeout(resolve, 500);
366
+ });
367
+ }
368
+
369
+ async function finalizeBrowserSession(session: BrowserSession): Promise<void> {
370
+ const failures: unknown[] = [];
371
+ let closeVerified = false;
372
+ try {
373
+ await session.close();
374
+ closeVerified = true;
375
+ } catch (error) {
376
+ failures.push(error);
377
+ }
378
+ let cleanupVerified = false;
379
+ try {
380
+ await session.cleanup();
381
+ cleanupVerified = true;
382
+ } catch (error) {
383
+ failures.push(error);
384
+ }
385
+ // BrowserSession cleanup can resolve only after close was acknowledged or
386
+ // the same pinned resource was independently proved quiescent.
387
+ if (cleanupVerified) return;
388
+ const cleanupEvidence = (
389
+ closeVerified
390
+ && !cleanupVerified
391
+ && session.cleanupResourceIdentity !== undefined
392
+ )
393
+ ? Object.freeze({
394
+ kind: "agent-browser-closed-artifacts-v1" as const,
395
+ resource: session.cleanupResourceIdentity,
396
+ })
397
+ : undefined;
398
+ throw new PreservedBrowserArtifactsError(
399
+ "LinkedIn stats browser finalization failed; private artifacts were preserved",
400
+ session.recoveryHandle ?? "session=linkedin-profile-runtime;artifacts=unknown",
401
+ new AggregateError(failures, "LinkedIn stats browser finalization failed"),
402
+ cleanupEvidence,
403
+ );
404
+ }
405
+
406
+ export async function createLinkedInProfileBrowserTransport(
407
+ auth: GhostgetAuth,
408
+ options: {
409
+ readonly timeoutMs: number;
410
+ readonly maxOutputBytes: number;
411
+ readonly operationDeadline?: WebSessionOperationDeadline;
412
+ readonly publishCleanupResource?: WebSessionCleanupResourcePublisher;
413
+ readonly dependencies?: Partial<LinkedInProfileBrowserDependencies>;
414
+ },
415
+ ): Promise<LinkedInProfileBrowserTransport> {
416
+ if (
417
+ !Number.isSafeInteger(options.maxOutputBytes)
418
+ || options.maxOutputBytes < 1
419
+ || options.maxOutputBytes > MAX_STATS_PAGE_BYTES
420
+ ) throw new Error("LinkedIn stats browser output bound is invalid");
421
+ const createSession = options.dependencies?.createBrowserSession
422
+ ?? createBrowserSession;
423
+ const browserOutputBytes = encodedBodyBound(options.maxOutputBytes)
424
+ + BROWSER_ENVELOPE_BYTES;
425
+ const sessionOptions: CreateBrowserSessionOptions = {
426
+ allowCodeOwnedEvaluation: true,
427
+ headed: true,
428
+ maxOutputBytes: browserOutputBytes,
429
+ timeoutMs: options.timeoutMs,
430
+ dependencies: {
431
+ runCommand: linkedInProfileBrowserCommandRunner(
432
+ options.dependencies?.runCommand ?? runCommand,
433
+ options.dependencies?.settleContext ?? settleLinkedInBrowserContext,
434
+ auth.kind,
435
+ ),
436
+ },
437
+ ...(options.operationDeadline === undefined
438
+ ? {}
439
+ : { operationDeadline: options.operationDeadline }),
440
+ ...(options.publishCleanupResource === undefined
441
+ ? {}
442
+ : { publishCleanupResource: options.publishCleanupResource }),
443
+ };
444
+ let session: BrowserSession;
445
+ try {
446
+ session = await createSession(profileBrowserManifest, auth, sessionOptions);
447
+ } catch (error) {
448
+ if (error instanceof PreservedBrowserArtifactsError) throw error;
449
+ options.operationDeadline?.throwIfUnavailable(
450
+ "LinkedIn stats browser startup",
451
+ );
452
+ throw new LinkedInProfileBrowserFailure(
453
+ "startup",
454
+ "LinkedIn stats browser could not start its contained session",
455
+ );
456
+ }
457
+ let closed = false;
458
+ let state: BrowserReadState = "ready";
459
+
460
+ const remainingTimeMs = (): number =>
461
+ options.operationDeadline?.remainingTimeMs() ?? options.timeoutMs;
462
+ const run = async (
463
+ binding: BrowserReadBinding,
464
+ ): Promise<Readonly<Record<string, unknown>>> => {
465
+ if (closed) throw new Error("LinkedIn stats browser transport is closed");
466
+ const source = browserReadEvaluationSource(binding);
467
+ let records: readonly Readonly<Record<string, unknown>>[];
468
+ try {
469
+ records = await session.runBatch(
470
+ [["eval", source]],
471
+ remainingTimeMs(),
472
+ Math.min(
473
+ encodedBodyBound(binding.maxBytes) + BROWSER_ENVELOPE_BYTES,
474
+ browserOutputBytes,
475
+ ),
476
+ );
477
+ } catch (error) {
478
+ if (error instanceof PreservedBrowserArtifactsError) throw error;
479
+ if (error instanceof LinkedInProfileBrowserFailure) throw error;
480
+ options.operationDeadline?.throwIfUnavailable(
481
+ "LinkedIn stats browser operation",
482
+ );
483
+ if (hasNoDefaultExecutionContext(error)) {
484
+ throw new LinkedInProfileBrowserFailure(
485
+ "execution-context",
486
+ "LinkedIn stats browser lost its reviewed execution context",
487
+ );
488
+ }
489
+ if (hasUnexpectedLinkedInOrigin(error)) {
490
+ throw new LinkedInProfileBrowserFailure(
491
+ "bootstrap",
492
+ "LinkedIn stats browser was not on its reviewed signed-in origin",
493
+ );
494
+ }
495
+ throw classifiedBrowserCommandFailure(error);
496
+ }
497
+ const first = records[0];
498
+ if (first === undefined) {
499
+ throw new LinkedInProfileBrowserFailure(
500
+ "response-envelope",
501
+ "LinkedIn stats browser omitted its response",
502
+ );
503
+ }
504
+ const result = browserEvaluationResult(first);
505
+ try {
506
+ exactKeys(
507
+ result,
508
+ [
509
+ "authWall",
510
+ "bodyBase64",
511
+ "bodyBytes",
512
+ "bodySha256",
513
+ "contentType",
514
+ "status",
515
+ ],
516
+ "LinkedIn stats browser request",
517
+ );
518
+ } catch {
519
+ throw new LinkedInProfileBrowserFailure(
520
+ "response-envelope",
521
+ "LinkedIn stats browser request returned an unexpected result shape",
522
+ );
523
+ }
524
+ const expectedContentTypes = binding.kind === "json"
525
+ ? ["application/vnd.linkedin.normalized+json+2.1", "application/json"]
526
+ : ["text/html"];
527
+ if (
528
+ typeof result.status !== "number"
529
+ || !Number.isSafeInteger(result.status)
530
+ || result.status < 100
531
+ || result.status > 599
532
+ || typeof result.contentType !== "string"
533
+ || result.contentType.length > 128
534
+ || (result.contentType !== "" && !LINKEDIN_RESPONSE_MEDIA_TYPE.test(result.contentType))
535
+ ) throw new LinkedInProfileBrowserFailure(
536
+ "response-envelope",
537
+ "LinkedIn stats browser returned a malformed response category",
538
+ );
539
+ if (result.authWall === true) {
540
+ if (
541
+ binding.kind !== "html"
542
+ || result.status !== 200
543
+ || result.contentType !== "text/html"
544
+ || result.bodyBase64 !== null
545
+ || result.bodyBytes !== 0
546
+ || result.bodySha256 !== null
547
+ ) throw new LinkedInProfileBrowserFailure(
548
+ "response-envelope",
549
+ "LinkedIn stats browser returned a malformed authwall envelope",
550
+ );
551
+ throw new LinkedInProfileBrowserFailure(
552
+ "authwall",
553
+ "LinkedIn stats browser reached the signed-out authwall",
554
+ );
555
+ }
556
+ if (result.authWall !== false) {
557
+ throw new LinkedInProfileBrowserFailure(
558
+ "response-envelope",
559
+ "LinkedIn stats browser returned a malformed authwall envelope",
560
+ );
561
+ }
562
+ if (
563
+ result.status !== 200
564
+ || !expectedContentTypes.includes(result.contentType)
565
+ ) {
566
+ if (
567
+ result.bodyBase64 !== null
568
+ || result.bodyBytes !== 0
569
+ || result.bodySha256 !== null
570
+ ) throw new LinkedInProfileBrowserFailure(
571
+ "response-envelope",
572
+ "LinkedIn stats browser returned a malformed rejection envelope",
573
+ );
574
+ throw new LinkedInProfileBrowserResponseRejectedError(
575
+ result.status,
576
+ result.contentType,
577
+ );
578
+ }
579
+ return result;
580
+ };
581
+
582
+ return Object.freeze({
583
+ currentIdentityResponse: async () => {
584
+ if (state !== "ready") {
585
+ throw new Error("LinkedIn stats browser identity read is out of order");
586
+ }
587
+ const result = await run({
588
+ kind: "json",
589
+ maxBytes: Math.min(MAX_IDENTITY_BYTES, options.maxOutputBytes),
590
+ path: "/voyager/api/me",
591
+ referrer: LINKEDIN_FEED_URL,
592
+ });
593
+ state = "identity";
594
+ const text = decodedBody(
595
+ result,
596
+ Math.min(MAX_IDENTITY_BYTES, options.maxOutputBytes),
597
+ );
598
+ try {
599
+ return JSON.parse(text) as unknown;
600
+ } catch {
601
+ throw new LinkedInProfileBrowserFailure(
602
+ "identity-json",
603
+ "LinkedIn stats browser identity response was not valid JSON",
604
+ );
605
+ }
606
+ },
607
+ readProfileHtml: async (profileUrl: string) => {
608
+ if (state !== "identity") {
609
+ throw new Error("LinkedIn stats browser profile read is out of order");
610
+ }
611
+ const target = exactLinkedInUrl(profileUrl, "profile");
612
+ const result = await run({
613
+ kind: "html",
614
+ maxBytes: options.maxOutputBytes,
615
+ path: target.pathname,
616
+ referrer: LINKEDIN_FEED_URL,
617
+ });
618
+ state = "profile";
619
+ return decodedBody(result, options.maxOutputBytes);
620
+ },
621
+ readConnectionsHtml: async (profileUrl: string) => {
622
+ if (state !== "profile") {
623
+ throw new Error("LinkedIn stats browser connections read is out of order");
624
+ }
625
+ const profile = exactLinkedInUrl(profileUrl, "profile");
626
+ const result = await run({
627
+ kind: "html",
628
+ maxBytes: options.maxOutputBytes,
629
+ path: new URL(LINKEDIN_CONNECTIONS_URL).pathname,
630
+ referrer: profile.href,
631
+ });
632
+ state = "complete";
633
+ return decodedBody(result, options.maxOutputBytes);
634
+ },
635
+ readContactInfoJson: async (input: LinkedInContactInfoJsonInput) => {
636
+ if (state !== "profile") {
637
+ throw new Error("LinkedIn stats browser Contact-info read is out of order");
638
+ }
639
+ const profile = exactLinkedInUrl(input.profileUrl, "profile");
640
+ const result = await run({
641
+ kind: "json",
642
+ maxBytes: Math.min(MAX_IDENTITY_BYTES, options.maxOutputBytes),
643
+ path: buildLinkedInProfileContactInfoGraphqlPath({
644
+ profileUrn: input.profileUrn,
645
+ queryId: input.queryId,
646
+ }),
647
+ referrer: profile.href,
648
+ });
649
+ state = "complete";
650
+ const text = decodedBody(
651
+ result,
652
+ Math.min(MAX_IDENTITY_BYTES, options.maxOutputBytes),
653
+ );
654
+ try {
655
+ return JSON.parse(text) as unknown;
656
+ } catch {
657
+ throw new LinkedInProfileBrowserFailure(
658
+ "identity-json",
659
+ "LinkedIn stats browser Contact-info response was not valid JSON",
660
+ );
661
+ }
662
+ },
663
+ readOrganizationHtml: async (organizationUrl: string) => {
664
+ if (state !== "identity") {
665
+ throw new Error("LinkedIn stats browser organization read is out of order");
666
+ }
667
+ const target = exactLinkedInUrl(organizationUrl, "organization");
668
+ const result = await run({
669
+ kind: "html",
670
+ maxBytes: options.maxOutputBytes,
671
+ path: target.pathname,
672
+ referrer: LINKEDIN_FEED_URL,
673
+ });
674
+ state = "complete";
675
+ return decodedBody(result, options.maxOutputBytes);
676
+ },
677
+ close: async () => {
678
+ if (closed) return;
679
+ closed = true;
680
+ await finalizeBrowserSession(session);
681
+ },
682
+ });
683
+ }