@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,794 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ import type {
4
+ ArticleDraftDocument,
5
+ ArticleDraftDocumentV2,
6
+ } from "../article-draft-document";
7
+ import type { BoundArticleDraftImage } from "../article-draft-images";
8
+ import type { GhostgetAuth } from "../auth";
9
+ import {
10
+ PreservedBrowserArtifactsError,
11
+ browserResultData,
12
+ createBrowserSession,
13
+ type BrowserSession,
14
+ type CreateBrowserSessionOptions,
15
+ } from "../browser";
16
+ import { canonicalJson, jsonScriptLiteral } from "../canonical-json";
17
+ import type { GhostgetManifest } from "../model";
18
+ import type {
19
+ WebSessionCleanupResourcePublisher,
20
+ WebSessionOperationDeadline,
21
+ } from "../web-session-execution";
22
+ import {
23
+ LINKEDIN_ARTICLE_PAGE_MAX_CHARACTERS,
24
+ LINKEDIN_ARTICLE_INLINE_IMAGE_UPLOAD_PATH,
25
+ LINKEDIN_FIRST_PARTY_ARTICLES_PATH,
26
+ buildLinkedInArticleCoverPatch,
27
+ buildLinkedInArticleContentPatch,
28
+ buildLinkedInArticleContentPatchV2,
29
+ buildLinkedInArticleCreateBody,
30
+ buildLinkedInArticleTitlePatch,
31
+ linkedInArticleDraftEditUrl,
32
+ linkedInArticleDraftEnvelopeFromCodePayloads,
33
+ linkedInArticleDraftEntityUrl,
34
+ normalizeLinkedInArticleImageUploadRegistration,
35
+ } from "./linkedin-web";
36
+
37
+ const LINKEDIN_ORIGIN = "https://www.linkedin.com";
38
+ const LINKEDIN_ARTICLE_NEW_URL = `${LINKEDIN_ORIGIN}/article/new/`;
39
+ const LINKEDIN_ARTICLE_AUTOSAVE_PEM_METADATA =
40
+ "Voyager - Article Creator=autosave-article";
41
+ const MAX_BROWSER_OUTPUT_BYTES = 2 * 1024 * 1024;
42
+ const LINKEDIN_IMAGE_STAGING_COMMANDS_PER_BATCH = 16;
43
+
44
+ const articleBrowserManifest: GhostgetManifest = Object.freeze({
45
+ schemaVersion: 4,
46
+ id: "linkedin-article-runtime",
47
+ version: "1.0.0",
48
+ displayName: "LinkedIn native Article runtime",
49
+ surfaceId: "linkedin",
50
+ origins: Object.freeze([LINKEDIN_ORIGIN]),
51
+ browserDomains: Object.freeze(["www.linkedin.com", "static.licdn.com"]),
52
+ operations: Object.freeze({}),
53
+ });
54
+
55
+ export type LinkedInArticleBrowserTransport = {
56
+ readonly currentIdentityResponse: () => Promise<unknown>;
57
+ readonly prepareCreateDraft: () => Promise<void>;
58
+ readonly createDraft: (profileUrn: string, title: string) => Promise<string | null>;
59
+ readonly readDraftResponse: (draftId: string) => Promise<unknown>;
60
+ readonly updateTitle: (draftId: string, title: string) => Promise<void>;
61
+ readonly updateContent: (
62
+ draftId: string,
63
+ document: ArticleDraftDocument,
64
+ ) => Promise<void>;
65
+ readonly uploadInlineImage?: (
66
+ draftId: string,
67
+ image: BoundArticleDraftImage,
68
+ ) => Promise<string>;
69
+ readonly uploadCoverImage?: (
70
+ draftId: string,
71
+ image: BoundArticleDraftImage,
72
+ ) => Promise<string>;
73
+ readonly updateCover?: (
74
+ draftId: string,
75
+ assetUrn: string,
76
+ ) => Promise<void>;
77
+ readonly updateContentV2?: (
78
+ draftId: string,
79
+ document: ArticleDraftDocumentV2,
80
+ imageAssetUrns: readonly string[],
81
+ ) => Promise<void>;
82
+ readonly close: () => Promise<void>;
83
+ };
84
+
85
+ export type LinkedInArticleBrowserDependencies = {
86
+ readonly createBrowserSession: typeof createBrowserSession;
87
+ };
88
+
89
+ type RequestBinding = {
90
+ readonly method: "GET" | "POST";
91
+ readonly path: string;
92
+ readonly referrer: string;
93
+ readonly body: string | null;
94
+ readonly pageInstance: string | null;
95
+ readonly pemMetadata: "article-autosave" | null;
96
+ readonly track: string | null;
97
+ readonly response: "json" | "status" | "created" | "page";
98
+ };
99
+
100
+ type LinkedInArticlePageBindings = {
101
+ readonly pageInstance: string;
102
+ readonly track: string;
103
+ };
104
+
105
+ const LINKEDIN_PAGE_INSTANCE_PATTERN =
106
+ /^urn:li:page:[A-Za-z0-9_:-]{1,128};[A-Za-z0-9+/=_-]{1,512}$/u;
107
+
108
+ function linkedInPageInstance(value: unknown): string {
109
+ if (typeof value !== "string" || !LINKEDIN_PAGE_INSTANCE_PATTERN.test(value)) {
110
+ throw new Error("LinkedIn Article page omitted its bounded page-instance binding");
111
+ }
112
+ return value;
113
+ }
114
+
115
+ function boundedString(value: unknown, label: string, maximum: number): string {
116
+ if (
117
+ typeof value !== "string"
118
+ || value.length < 1
119
+ || value.length > maximum
120
+ || /[\0\r\n]/u.test(value)
121
+ ) throw new Error(`${label} changed its reviewed bound`);
122
+ return value;
123
+ }
124
+
125
+ function isRecord(value: unknown): value is Readonly<Record<string, unknown>> {
126
+ return typeof value === "object" && value !== null && !Array.isArray(value);
127
+ }
128
+
129
+ function exactKeys(
130
+ value: Readonly<Record<string, unknown>>,
131
+ expected: readonly string[],
132
+ label: string,
133
+ ): void {
134
+ if (Object.keys(value).sort().join(",") !== [...expected].sort().join(",")) {
135
+ throw new Error(`${label} returned an unexpected result shape`);
136
+ }
137
+ }
138
+
139
+ function linkedInArticleTrack(value: unknown): string {
140
+ const track = boundedString(value, "LinkedIn Article x-li-track binding", 4_096);
141
+ let parsed: unknown;
142
+ try {
143
+ parsed = JSON.parse(track) as unknown;
144
+ } catch {
145
+ throw new Error("LinkedIn Article x-li-track binding changed shape");
146
+ }
147
+ if (!isRecord(parsed)) {
148
+ throw new Error("LinkedIn Article x-li-track binding changed shape");
149
+ }
150
+ exactKeys(parsed, [
151
+ "clientVersion",
152
+ "deviceFormFactor",
153
+ "displayDensity",
154
+ "displayHeight",
155
+ "displayWidth",
156
+ "mpName",
157
+ "mpVersion",
158
+ "osName",
159
+ "timezone",
160
+ "timezoneOffset",
161
+ ], "LinkedIn Article x-li-track binding");
162
+ const clientVersion = boundedString(
163
+ parsed.clientVersion,
164
+ "LinkedIn Article x-li-track clientVersion",
165
+ 64,
166
+ );
167
+ if (!/^[0-9]+(?:\.[0-9]+){1,7}$/u.test(clientVersion) || parsed.mpVersion !== clientVersion) {
168
+ throw new Error("LinkedIn Article x-li-track version changed shape");
169
+ }
170
+ boundedString(parsed.osName, "LinkedIn Article x-li-track osName", 32);
171
+ const timezone = boundedString(
172
+ parsed.timezone,
173
+ "LinkedIn Article x-li-track timezone",
174
+ 128,
175
+ );
176
+ if (!/^[A-Za-z0-9_+.-]+(?:\/[A-Za-z0-9_+.-]+)+$/u.test(timezone)) {
177
+ throw new Error("LinkedIn Article x-li-track timezone changed shape");
178
+ }
179
+ if (parsed.deviceFormFactor !== "DESKTOP" || parsed.mpName !== "voyager-web") {
180
+ throw new Error("LinkedIn Article x-li-track client binding changed shape");
181
+ }
182
+ if (
183
+ !Number.isInteger(parsed.timezoneOffset)
184
+ || (parsed.timezoneOffset as number) < -24
185
+ || (parsed.timezoneOffset as number) > 24
186
+ || typeof parsed.displayDensity !== "number"
187
+ || !Number.isFinite(parsed.displayDensity)
188
+ || parsed.displayDensity < 0.5
189
+ || parsed.displayDensity > 10
190
+ || !Number.isInteger(parsed.displayWidth)
191
+ || (parsed.displayWidth as number) < 1
192
+ || (parsed.displayWidth as number) > 20_000
193
+ || !Number.isInteger(parsed.displayHeight)
194
+ || (parsed.displayHeight as number) < 1
195
+ || (parsed.displayHeight as number) > 20_000
196
+ ) throw new Error("LinkedIn Article x-li-track display binding changed shape");
197
+ return track;
198
+ }
199
+
200
+ function linkedInArticlePageBindings(
201
+ value: unknown,
202
+ expectedPageName: "d_flagship3_publishing_post_new" | "d_flagship3_publishing_post_edit",
203
+ ): LinkedInArticlePageBindings {
204
+ if (!isRecord(value) || !Array.isArray(value.requests) || value.requests.length > 10_000) {
205
+ throw new Error("LinkedIn Article network observation changed shape");
206
+ }
207
+ let selected: LinkedInArticlePageBindings | null = null;
208
+ for (const item of value.requests) {
209
+ if (!isRecord(item) || !isRecord(item.headers)) continue;
210
+ if (
211
+ item.method !== "GET"
212
+ || item.status !== 200
213
+ || typeof item.url !== "string"
214
+ || item.url.length > 64 * 1_024
215
+ ) continue;
216
+ let url: URL;
217
+ try {
218
+ url = new URL(item.url);
219
+ } catch {
220
+ continue;
221
+ }
222
+ if (
223
+ url.origin !== LINKEDIN_ORIGIN
224
+ || url.username !== ""
225
+ || url.password !== ""
226
+ || !url.pathname.startsWith("/voyager/api/")
227
+ ) continue;
228
+ const pageInstanceValue = item.headers["x-li-page-instance"];
229
+ if (typeof pageInstanceValue !== "string") continue;
230
+ const name = /^urn:li:page:([A-Za-z0-9_:-]{1,128});/u.exec(pageInstanceValue)?.[1];
231
+ if (name !== expectedPageName) continue;
232
+ const candidate = Object.freeze({
233
+ pageInstance: linkedInPageInstance(pageInstanceValue),
234
+ track: linkedInArticleTrack(item.headers["x-li-track"]),
235
+ });
236
+ // Agent-browser's bounded request inventory is chronological and may
237
+ // retain an earlier instance of this same editor after a navigation retry.
238
+ // The final matching request is the binding for the currently visible
239
+ // editor; older matching instances must not make an otherwise exact
240
+ // current-page binding ambiguous.
241
+ selected = candidate;
242
+ }
243
+ if (selected === null) {
244
+ throw new Error("LinkedIn Article page omitted its bounded page-instance binding");
245
+ }
246
+ return selected;
247
+ }
248
+
249
+ function articleReadbackResponseCategory(
250
+ status: unknown,
251
+ contentType: unknown,
252
+ body: unknown,
253
+ ): string {
254
+ const bodyShape = isRecord(body)
255
+ ? `:${Object.keys(body).sort().join("+") || "empty"}`
256
+ : typeof contentType === "string" && contentType.includes("json")
257
+ ? ":json-error"
258
+ : typeof contentType === "string" && contentType.startsWith("text/")
259
+ ? ":text-error"
260
+ : ":opaque-error";
261
+ if (status === 401 || status === 403) return "session-rejected";
262
+ if (status === 400 || status === 422) return `request-rejected-${status}${bodyShape}`;
263
+ if (status === 404) return "contract-route-not-found";
264
+ if (status === 410) return "contract-route-retired";
265
+ if (status === 409) return "provider-conflict";
266
+ if (status === 429) return "provider-throttled";
267
+ if (typeof status === "number" && status >= 500) return "provider-unavailable";
268
+ if (status !== 200) return "status-drift";
269
+ if (
270
+ contentType !== "application/vnd.linkedin.normalized+json+2.1"
271
+ && contentType !== "application/json"
272
+ ) return "content-type-drift";
273
+ return "response-drift";
274
+ }
275
+
276
+ function articleEditUrl(draftId: string): string {
277
+ return linkedInArticleDraftEditUrl(draftId).href;
278
+ }
279
+
280
+ function exactRequestPath(url: URL): string {
281
+ if (
282
+ url.origin !== LINKEDIN_ORIGIN
283
+ || url.username !== ""
284
+ || url.password !== ""
285
+ || url.hash !== ""
286
+ ) throw new Error("LinkedIn Article browser request escaped its reviewed origin");
287
+ return `${url.pathname}${url.search}`;
288
+ }
289
+
290
+ function requestEvaluationSource(binding: RequestBinding): string {
291
+ const bound = jsonScriptLiteral(binding);
292
+ return `(async()=>{const input=${bound};if(location.origin!=="${LINKEDIN_ORIGIN}")throw new Error("unexpected LinkedIn origin");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");const headers=input.response==="page"?{accept:"text/html"}:{accept:"application/vnd.linkedin.normalized+json+2.1","csrf-token":csrf,"x-li-lang":"en_US","x-restli-protocol-version":"2.0.0"};if(input.body!==null){if(!/^urn:li:page:[A-Za-z0-9_:-]{1,128};[A-Za-z0-9+/=_-]{1,512}$/.test(input.pageInstance||""))throw new Error("missing LinkedIn browser page instance");if(typeof input.track!=="string"||input.track.length<1||input.track.length>4096||/[\\0\\r\\n]/u.test(input.track))throw new Error("missing LinkedIn browser track binding");if(input.pemMetadata!==null&&input.pemMetadata!=="article-autosave")throw new Error("invalid LinkedIn browser PEM binding");headers["x-li-page-instance"]=input.pageInstance;if(input.pemMetadata==="article-autosave")headers["x-li-pem-metadata"]="${LINKEDIN_ARTICLE_AUTOSAVE_PEM_METADATA}";headers["x-li-track"]=input.track;headers["content-type"]="application/json; charset=UTF-8"}else if(input.pemMetadata!==null)throw new Error("invalid LinkedIn browser PEM binding");const response=await fetch(input.path,{body:input.body===null?undefined:input.body,credentials:"include",headers,method:input.method,redirect:"error",referrer:input.referrer});const contentType=(response.headers.get("content-type")||"").split(";",1)[0].trim().toLowerCase();if(input.response==="page"){const payloads=[];if(response.status===200&&contentType==="text/html"){if(response.body===null)throw new Error("missing LinkedIn Article page body");const reader=response.body.getReader();const decoder=new TextDecoder();const chunks=[];let bytes=0;while(true){const part=await reader.read();if(part.done)break;bytes+=part.value.byteLength;if(bytes>${LINKEDIN_ARTICLE_PAGE_MAX_CHARACTERS}){await reader.cancel();throw new Error("LinkedIn Article page exceeded its reviewed bound")}chunks.push(decoder.decode(part.value,{stream:true}))}chunks.push(decoder.decode());const html=chunks.join("");const id=new RegExp("^/article/edit/([0-9]{1,32})/$","u").exec(input.path)?.[1];if(typeof id!=="string")throw new Error("invalid LinkedIn Article page path");const urn="urn:li:fsd_firstPartyArticle:"+id;for(const match of html.matchAll(/<code\\b([^>]*)>([\\s\\S]*?)<\\/code>/giu)){const attributes=match[1]||"";const body=match[2]||"";if(!body.includes(urn))continue;payloads.push({attributes,body});if(payloads.length>20)throw new Error("LinkedIn Article page returned too many matching payloads")}}return{contentType,payloads,status:response.status}}if(input.response==="json"){let body=null;if(contentType==="application/vnd.linkedin.normalized+json+2.1"||contentType==="application/json")body=await response.json();return{body,contentType,status:response.status}}if(input.response==="created")return{contentType,responseId:response.headers.get("x-restli-id"),status:response.status};return{contentType,status:response.status}})()`;
293
+ }
294
+
295
+ function evaluationResult(
296
+ record: Readonly<Record<string, unknown>>,
297
+ ): Readonly<Record<string, unknown>> {
298
+ const data = browserResultData(record);
299
+ if (!isRecord(data) || typeof data.origin !== "string" || !isRecord(data.result)) {
300
+ throw new Error("LinkedIn Article browser returned a malformed evaluation envelope");
301
+ }
302
+ let origin: URL;
303
+ try {
304
+ origin = new URL(data.origin);
305
+ } catch {
306
+ throw new Error("LinkedIn Article browser returned a malformed evaluation envelope");
307
+ }
308
+ if (
309
+ origin.origin !== LINKEDIN_ORIGIN
310
+ || origin.username !== ""
311
+ || origin.password !== ""
312
+ ) throw new Error("LinkedIn Article browser returned a malformed evaluation envelope");
313
+ return data.result;
314
+ }
315
+
316
+ function linkedInArticleImageUploadEvaluationSource(input: {
317
+ readonly expectedBase64Length: number;
318
+ readonly expectedByteLength: number;
319
+ readonly expectedChunkCount: number;
320
+ readonly key: string;
321
+ readonly mediaType: string;
322
+ readonly referrer: string;
323
+ readonly uploadHeaders: Readonly<Record<string, string>>;
324
+ readonly uploadUrl: string;
325
+ }): string {
326
+ const bound = jsonScriptLiteral(input);
327
+ return `(async()=>{const input=${bound};if(location.origin!=="${LINKEDIN_ORIGIN}")throw new Error("unexpected LinkedIn origin");if(!Number.isSafeInteger(input.expectedBase64Length)||input.expectedBase64Length<1||!Number.isSafeInteger(input.expectedByteLength)||input.expectedByteLength<1||!Number.isSafeInteger(input.expectedChunkCount)||input.expectedChunkCount<1||input.expectedChunkCount>256)throw new Error("invalid LinkedIn image byte binding");const chunks=globalThis[input.key];delete globalThis[input.key];if(!Array.isArray(chunks)||chunks.length!==input.expectedChunkCount)throw new Error("missing bounded LinkedIn image bytes");let encoded="";for(const chunk of chunks){if(typeof chunk!=="string"||chunk.length<1||chunk.length>49152||!/^[A-Za-z0-9+/]*={0,2}$/.test(chunk))throw new Error("invalid LinkedIn image bytes");encoded+=chunk}if(encoded.length!==input.expectedBase64Length)throw new Error("LinkedIn image changed encoded size");const binary=atob(encoded);encoded="";if(binary.length!==input.expectedByteLength)throw new Error("LinkedIn image changed size");const bytes=new Uint8Array(binary.length);for(let index=0;index<binary.length;index+=1)bytes[index]=binary.charCodeAt(index);const image=new Blob([bytes],{type:input.mediaType});if(image.size!==input.expectedByteLength||image.type!==input.mediaType)throw new Error("LinkedIn image blob changed shape");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");const upload=await fetch(input.uploadUrl,{body:image,credentials:"include",headers:{...input.uploadHeaders,"content-type":input.mediaType,"csrf-token":csrf},method:"PUT",redirect:"error",referrer:input.referrer});return{uploadStatus:upload.status}})()`;
328
+ }
329
+
330
+ async function stageLinkedInArticleImageBytes(
331
+ session: BrowserSession,
332
+ key: string,
333
+ image: BoundArticleDraftImage,
334
+ timeoutMs: number,
335
+ ): Promise<void> {
336
+ const init = `(async()=>{const key=${jsonScriptLiteral(key)};if(Object.hasOwn(globalThis,key))throw new Error("LinkedIn image staging key collision");globalThis[key]=[];return true})()`;
337
+ await session.runBatch([["eval", init]], timeoutMs, MAX_BROWSER_OUTPUT_BYTES);
338
+ const encoded = Buffer.from(image.bytes).toString("base64");
339
+ try {
340
+ const commands: (readonly string[])[] = [];
341
+ for (let offset = 0; offset < encoded.length; offset += 48 * 1_024) {
342
+ const chunk = encoded.slice(offset, offset + 48 * 1_024);
343
+ const source = `(async()=>{const key=${jsonScriptLiteral(key)};const chunks=globalThis[key];if(!Array.isArray(chunks)||chunks.length>=256)throw new Error("LinkedIn image staging changed shape");chunks.push(${jsonScriptLiteral(chunk)});return true})()`;
344
+ commands.push(["eval", source]);
345
+ }
346
+ for (
347
+ let offset = 0;
348
+ offset < commands.length;
349
+ offset += LINKEDIN_IMAGE_STAGING_COMMANDS_PER_BATCH
350
+ ) {
351
+ await session.runBatch(
352
+ commands.slice(offset, offset + LINKEDIN_IMAGE_STAGING_COMMANDS_PER_BATCH),
353
+ timeoutMs,
354
+ MAX_BROWSER_OUTPUT_BYTES,
355
+ );
356
+ }
357
+ } catch (error) {
358
+ try {
359
+ await session.runBatch(
360
+ [["eval", `(async()=>{delete globalThis[${jsonScriptLiteral(key)}];return true})()`]],
361
+ timeoutMs,
362
+ MAX_BROWSER_OUTPUT_BYTES,
363
+ );
364
+ } catch {
365
+ // Browser finalization remains the authoritative private-artifact cleanup.
366
+ }
367
+ throw error;
368
+ }
369
+ }
370
+
371
+ async function finalizeBrowserSession(session: BrowserSession): Promise<void> {
372
+ const failures: unknown[] = [];
373
+ let closeVerified = false;
374
+ try {
375
+ await session.close();
376
+ closeVerified = true;
377
+ } catch (error) {
378
+ failures.push(error);
379
+ }
380
+ let cleanupVerified = false;
381
+ try {
382
+ await session.cleanup();
383
+ cleanupVerified = true;
384
+ } catch (error) {
385
+ failures.push(error);
386
+ }
387
+ if (closeVerified && 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 Article browser finalization failed; private artifacts were preserved",
400
+ session.recoveryHandle ?? "session=linkedin-article-runtime;artifacts=unknown",
401
+ new AggregateError(failures, "LinkedIn Article browser finalization failed"),
402
+ cleanupEvidence,
403
+ );
404
+ }
405
+
406
+ export async function createLinkedInArticleBrowserTransport(
407
+ auth: GhostgetAuth,
408
+ options: {
409
+ readonly timeoutMs: number;
410
+ readonly operationDeadline?: WebSessionOperationDeadline;
411
+ readonly publishCleanupResource?: WebSessionCleanupResourcePublisher;
412
+ readonly dependencies?: Partial<LinkedInArticleBrowserDependencies>;
413
+ },
414
+ ): Promise<LinkedInArticleBrowserTransport> {
415
+ const createSession = options.dependencies?.createBrowserSession ?? createBrowserSession;
416
+ const sessionOptions: CreateBrowserSessionOptions = {
417
+ allowCodeOwnedEvaluation: true,
418
+ allowCodeOwnedNetworkObservation: true,
419
+ headed: true,
420
+ maxOutputBytes: MAX_BROWSER_OUTPUT_BYTES,
421
+ timeoutMs: options.timeoutMs,
422
+ ...(options.operationDeadline === undefined
423
+ ? {}
424
+ : { operationDeadline: options.operationDeadline }),
425
+ ...(options.publishCleanupResource === undefined
426
+ ? {}
427
+ : { publishCleanupResource: options.publishCleanupResource }),
428
+ };
429
+ const session = await createSession(articleBrowserManifest, auth, sessionOptions);
430
+ let closed = false;
431
+ let articleBindings: LinkedInArticlePageBindings | null = null;
432
+ let activeEditor: { readonly kind: "new" } | { readonly kind: "edit"; readonly draftId: string } | null = null;
433
+ const run = async (binding: RequestBinding): Promise<Readonly<Record<string, unknown>>> => {
434
+ if (closed) throw new Error("LinkedIn Article browser transport is closed");
435
+ const records = await session.runBatch(
436
+ [["eval", requestEvaluationSource(binding)]],
437
+ options.operationDeadline?.remainingTimeMs() ?? options.timeoutMs,
438
+ MAX_BROWSER_OUTPUT_BYTES,
439
+ );
440
+ const first = records[0];
441
+ if (first === undefined) throw new Error("LinkedIn Article browser omitted its response");
442
+ return evaluationResult(first);
443
+ };
444
+ const open = async (url: URL): Promise<void> => {
445
+ if (
446
+ url.origin !== LINKEDIN_ORIGIN
447
+ || url.username !== ""
448
+ || url.password !== ""
449
+ || url.hash !== ""
450
+ ) throw new Error("LinkedIn Article browser navigation escaped its reviewed origin");
451
+ await session.runBatch(
452
+ [["open", url.href]],
453
+ options.operationDeadline?.remainingTimeMs() ?? options.timeoutMs,
454
+ MAX_BROWSER_OUTPUT_BYTES,
455
+ );
456
+ };
457
+ const openEditor = async (
458
+ url: URL,
459
+ expectedPageName: "d_flagship3_publishing_post_new" | "d_flagship3_publishing_post_edit",
460
+ editor: { readonly kind: "new" } | { readonly kind: "edit"; readonly draftId: string },
461
+ ): Promise<void> => {
462
+ for (let attempt = 0; attempt < 2; attempt += 1) {
463
+ try {
464
+ await open(url);
465
+ await session.runBatch(
466
+ [["wait", "5000"]],
467
+ Math.min(options.operationDeadline?.remainingTimeMs() ?? options.timeoutMs, 20_000),
468
+ MAX_BROWSER_OUTPUT_BYTES,
469
+ );
470
+ const records = await session.runBatch(
471
+ [["network", "requests", "--filter", "/voyager/api/"]],
472
+ Math.min(options.operationDeadline?.remainingTimeMs() ?? options.timeoutMs, 30_000),
473
+ MAX_BROWSER_OUTPUT_BYTES,
474
+ );
475
+ const first = records[0];
476
+ if (first === undefined) {
477
+ throw new Error("LinkedIn Article page-binding observation omitted its response");
478
+ }
479
+ articleBindings = linkedInArticlePageBindings(
480
+ browserResultData(first),
481
+ expectedPageName,
482
+ );
483
+ activeEditor = editor;
484
+ return;
485
+ } catch (error) {
486
+ articleBindings = null;
487
+ activeEditor = null;
488
+ if (attempt === 1) throw error;
489
+ }
490
+ }
491
+ };
492
+
493
+ try {
494
+ await session.runBatch(
495
+ [["open", `${LINKEDIN_ORIGIN}/feed/`]],
496
+ options.operationDeadline?.remainingTimeMs() ?? options.timeoutMs,
497
+ MAX_BROWSER_OUTPUT_BYTES,
498
+ );
499
+ // A freshly cloned browser profile can return from navigation before the
500
+ // signed-in feed has installed its CSRF cookie in the page context. Wait
501
+ // once before the exact current-member probe; this remains wholly before
502
+ // the durable dispatch boundary and never retries a mutation.
503
+ await session.runBatch(
504
+ [["wait", "2000"]],
505
+ Math.min(options.operationDeadline?.remainingTimeMs() ?? options.timeoutMs, 10_000),
506
+ MAX_BROWSER_OUTPUT_BYTES,
507
+ );
508
+ } catch (error) {
509
+ try {
510
+ await finalizeBrowserSession(session);
511
+ } catch (cleanupError) {
512
+ throw cleanupError;
513
+ }
514
+ throw error;
515
+ }
516
+
517
+ const uploadArticleImage = async (
518
+ draftId: string,
519
+ image: BoundArticleDraftImage,
520
+ mediaUploadType: "PUBLISHING_COVER_IMAGE" | "PUBLISHING_INLINE_IMAGE",
521
+ label: string,
522
+ ): Promise<string> => {
523
+ if (
524
+ activeEditor?.kind !== "edit"
525
+ || activeEditor.draftId !== draftId
526
+ || articleBindings === null
527
+ ) throw new Error(`${label} upload omitted its exact editor binding`);
528
+ const registrationResult = await run({
529
+ method: "POST",
530
+ path: LINKEDIN_ARTICLE_INLINE_IMAGE_UPLOAD_PATH,
531
+ referrer: articleEditUrl(draftId),
532
+ body: canonicalJson({
533
+ fileSize: image.bytes.byteLength,
534
+ filename: image.filename,
535
+ mediaUploadType,
536
+ }),
537
+ pageInstance: articleBindings.pageInstance,
538
+ pemMetadata: null,
539
+ track: articleBindings.track,
540
+ response: "json",
541
+ });
542
+ exactKeys(
543
+ registrationResult,
544
+ ["body", "contentType", "status"],
545
+ `${label} registration request`,
546
+ );
547
+ if (
548
+ registrationResult.status !== 200
549
+ || (registrationResult.contentType !== "application/vnd.linkedin.normalized+json+2.1"
550
+ && registrationResult.contentType !== "application/json")
551
+ ) throw new Error(`${label} registration returned an unreviewed response`);
552
+ const registration = normalizeLinkedInArticleImageUploadRegistration(
553
+ registrationResult.body,
554
+ );
555
+ const key = `__ghostgetLinkedInArticleImage_${randomUUID().replaceAll("-", "")}`;
556
+ const timeoutMs = options.operationDeadline?.remainingTimeMs() ?? options.timeoutMs;
557
+ const encodedLength = Buffer.from(image.bytes).toString("base64").length;
558
+ const chunkCount = Math.ceil(encodedLength / (48 * 1_024));
559
+ await stageLinkedInArticleImageBytes(session, key, image, timeoutMs);
560
+ let transfer: Readonly<Record<string, unknown>>;
561
+ try {
562
+ const records = await session.runBatch(
563
+ [["eval", linkedInArticleImageUploadEvaluationSource({
564
+ expectedBase64Length: encodedLength,
565
+ expectedByteLength: image.bytes.byteLength,
566
+ expectedChunkCount: chunkCount,
567
+ key,
568
+ mediaType: image.mediaType,
569
+ referrer: articleEditUrl(draftId),
570
+ uploadHeaders: registration.uploadHeaders,
571
+ uploadUrl: registration.uploadUrl,
572
+ })]],
573
+ options.operationDeadline?.remainingTimeMs() ?? options.timeoutMs,
574
+ MAX_BROWSER_OUTPUT_BYTES,
575
+ );
576
+ const first = records[0];
577
+ if (first === undefined) throw new Error(`${label} upload omitted its response`);
578
+ transfer = evaluationResult(first);
579
+ } catch (error) {
580
+ try {
581
+ await session.runBatch(
582
+ [["eval", `(async()=>{delete globalThis[${jsonScriptLiteral(key)}];return true})()`]],
583
+ options.operationDeadline?.remainingTimeMs() ?? options.timeoutMs,
584
+ MAX_BROWSER_OUTPUT_BYTES,
585
+ );
586
+ } catch {
587
+ // Browser finalization remains the authoritative private-artifact cleanup.
588
+ }
589
+ throw error;
590
+ }
591
+ exactKeys(transfer, ["uploadStatus"], `${label} upload`);
592
+ // The reviewed editor proceeds from the signed transfer's 201 response
593
+ // directly to the Article autosave. Exact acceptance remains gated by the
594
+ // later private Article response and independent readback.
595
+ if (transfer.uploadStatus !== 201) {
596
+ throw new Error(`${label} upload returned an unreviewed response`);
597
+ }
598
+ return registration.assetUrn;
599
+ };
600
+
601
+ const transport: LinkedInArticleBrowserTransport = {
602
+ currentIdentityResponse: async () => {
603
+ const result = await run({
604
+ method: "GET",
605
+ path: "/voyager/api/me",
606
+ referrer: `${LINKEDIN_ORIGIN}/feed/`,
607
+ body: null,
608
+ pageInstance: null,
609
+ pemMetadata: null,
610
+ track: null,
611
+ response: "json",
612
+ });
613
+ exactKeys(result, ["body", "contentType", "status"], "LinkedIn current-member browser request");
614
+ if (
615
+ result.status !== 200
616
+ || (result.contentType !== "application/vnd.linkedin.normalized+json+2.1"
617
+ && result.contentType !== "application/json")
618
+ ) throw new Error("LinkedIn current-member browser request returned an unreviewed response");
619
+ return result.body;
620
+ },
621
+ prepareCreateDraft: () => openEditor(
622
+ new URL(LINKEDIN_ARTICLE_NEW_URL),
623
+ "d_flagship3_publishing_post_new",
624
+ Object.freeze({ kind: "new" }),
625
+ ),
626
+ createDraft: async (profileUrn, title) => {
627
+ if (activeEditor?.kind !== "new" || articleBindings === null) {
628
+ throw new Error("LinkedIn Article create omitted its exact editor binding");
629
+ }
630
+ const result = await run({
631
+ method: "POST",
632
+ path: `${LINKEDIN_FIRST_PARTY_ARTICLES_PATH}/`,
633
+ referrer: LINKEDIN_ARTICLE_NEW_URL,
634
+ body: canonicalJson(buildLinkedInArticleCreateBody(profileUrn, title)),
635
+ pageInstance: articleBindings.pageInstance,
636
+ pemMetadata: "article-autosave",
637
+ track: articleBindings.track,
638
+ response: "created",
639
+ });
640
+ exactKeys(result, ["contentType", "responseId", "status"], "LinkedIn Article create browser request");
641
+ if (result.status !== 201) {
642
+ throw new Error("LinkedIn Article create browser request returned an unreviewed response");
643
+ }
644
+ if (result.responseId !== null && typeof result.responseId !== "string") {
645
+ throw new Error("LinkedIn Article create browser response returned an invalid identifier");
646
+ }
647
+ return result.responseId as string | null;
648
+ },
649
+ readDraftResponse: async (draftId) => {
650
+ const editUrl = linkedInArticleDraftEditUrl(draftId);
651
+ if (activeEditor?.kind !== "edit" || activeEditor.draftId !== draftId) {
652
+ await openEditor(
653
+ editUrl,
654
+ "d_flagship3_publishing_post_edit",
655
+ Object.freeze({ kind: "edit", draftId }),
656
+ );
657
+ }
658
+ const result = await run({
659
+ method: "GET",
660
+ path: exactRequestPath(editUrl),
661
+ referrer: editUrl.href,
662
+ body: null,
663
+ pageInstance: null,
664
+ pemMetadata: null,
665
+ track: null,
666
+ response: "page",
667
+ });
668
+ exactKeys(
669
+ result,
670
+ ["contentType", "payloads", "status"],
671
+ "LinkedIn Article readback browser request",
672
+ );
673
+ if (result.status !== 200 || result.contentType !== "text/html") {
674
+ throw new Error(
675
+ `LinkedIn Article readback browser request returned an unreviewed response (${articleReadbackResponseCategory(result.status, result.contentType, null)})`,
676
+ );
677
+ }
678
+ return linkedInArticleDraftEnvelopeFromCodePayloads(
679
+ result.payloads,
680
+ draftId,
681
+ );
682
+ },
683
+ updateTitle: async (draftId, title) => {
684
+ if (
685
+ activeEditor?.kind !== "edit"
686
+ || activeEditor.draftId !== draftId
687
+ || articleBindings === null
688
+ ) throw new Error("LinkedIn Article title update omitted its exact editor binding");
689
+ const result = await run({
690
+ method: "POST",
691
+ path: exactRequestPath(linkedInArticleDraftEntityUrl(draftId)),
692
+ referrer: articleEditUrl(draftId),
693
+ body: canonicalJson(buildLinkedInArticleTitlePatch(title)),
694
+ pageInstance: articleBindings.pageInstance,
695
+ pemMetadata: "article-autosave",
696
+ track: articleBindings.track,
697
+ response: "status",
698
+ });
699
+ exactKeys(result, ["contentType", "status"], "LinkedIn Article title browser request");
700
+ if (result.status !== 200) {
701
+ throw new Error(
702
+ `LinkedIn Article title browser request returned an unreviewed response (${articleReadbackResponseCategory(result.status, result.contentType, null)})`,
703
+ );
704
+ }
705
+ },
706
+ updateContent: async (draftId, document) => {
707
+ if (
708
+ activeEditor?.kind !== "edit"
709
+ || activeEditor.draftId !== draftId
710
+ || articleBindings === null
711
+ ) throw new Error("LinkedIn Article content update omitted its exact editor binding");
712
+ const result = await run({
713
+ method: "POST",
714
+ path: exactRequestPath(linkedInArticleDraftEntityUrl(draftId)),
715
+ referrer: articleEditUrl(draftId),
716
+ body: canonicalJson(buildLinkedInArticleContentPatch(document)),
717
+ pageInstance: articleBindings.pageInstance,
718
+ pemMetadata: "article-autosave",
719
+ track: articleBindings.track,
720
+ response: "status",
721
+ });
722
+ exactKeys(result, ["contentType", "status"], "LinkedIn Article content browser request");
723
+ if (result.status !== 200) {
724
+ throw new Error(
725
+ `LinkedIn Article content browser request returned an unreviewed response (${articleReadbackResponseCategory(result.status, result.contentType, null)})`,
726
+ );
727
+ }
728
+ },
729
+ uploadInlineImage: (draftId, image) => uploadArticleImage(
730
+ draftId,
731
+ image,
732
+ "PUBLISHING_INLINE_IMAGE",
733
+ "LinkedIn Article inline image",
734
+ ),
735
+ uploadCoverImage: (draftId, image) => uploadArticleImage(
736
+ draftId,
737
+ image,
738
+ "PUBLISHING_COVER_IMAGE",
739
+ "LinkedIn Article cover image",
740
+ ),
741
+ updateCover: async (draftId, assetUrn) => {
742
+ if (
743
+ activeEditor?.kind !== "edit"
744
+ || activeEditor.draftId !== draftId
745
+ || articleBindings === null
746
+ ) throw new Error("LinkedIn Article cover update omitted its exact editor binding");
747
+ const result = await run({
748
+ method: "POST",
749
+ path: exactRequestPath(linkedInArticleDraftEntityUrl(draftId)),
750
+ referrer: articleEditUrl(draftId),
751
+ body: canonicalJson(buildLinkedInArticleCoverPatch(assetUrn)),
752
+ pageInstance: articleBindings.pageInstance,
753
+ pemMetadata: "article-autosave",
754
+ track: articleBindings.track,
755
+ response: "status",
756
+ });
757
+ exactKeys(result, ["contentType", "status"], "LinkedIn Article cover browser request");
758
+ if (result.status !== 200) {
759
+ throw new Error(
760
+ `LinkedIn Article cover browser request returned an unreviewed response (${articleReadbackResponseCategory(result.status, result.contentType, null)})`,
761
+ );
762
+ }
763
+ },
764
+ updateContentV2: async (draftId, document, imageAssetUrns) => {
765
+ if (
766
+ activeEditor?.kind !== "edit"
767
+ || activeEditor.draftId !== draftId
768
+ || articleBindings === null
769
+ ) throw new Error("LinkedIn Article content update omitted its exact editor binding");
770
+ const result = await run({
771
+ method: "POST",
772
+ path: exactRequestPath(linkedInArticleDraftEntityUrl(draftId)),
773
+ referrer: articleEditUrl(draftId),
774
+ body: canonicalJson(buildLinkedInArticleContentPatchV2(document, imageAssetUrns)),
775
+ pageInstance: articleBindings.pageInstance,
776
+ pemMetadata: "article-autosave",
777
+ track: articleBindings.track,
778
+ response: "status",
779
+ });
780
+ exactKeys(result, ["contentType", "status"], "LinkedIn Article content browser request");
781
+ if (result.status !== 200) {
782
+ throw new Error(
783
+ `LinkedIn Article content browser request returned an unreviewed response (${articleReadbackResponseCategory(result.status, result.contentType, null)})`,
784
+ );
785
+ }
786
+ },
787
+ close: async () => {
788
+ if (closed) return;
789
+ closed = true;
790
+ await finalizeBrowserSession(session);
791
+ },
792
+ };
793
+ return Object.freeze(transport);
794
+ }