@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,864 @@
1
+ import {
2
+ LINKEDIN_GRAPHQL_PATH,
3
+ encodeRestliV2Value,
4
+ linkedInPersonalProfilePublicIdentifier,
5
+ normalizeLinkedInGraphqlEnvelope,
6
+ resolveLinkedInRegisteredQueryId,
7
+ type LinkedInWebJsonRecord,
8
+ } from "./linkedin-web";
9
+
10
+ export { LINKEDIN_GRAPHQL_PATH };
11
+
12
+ export const LINKEDIN_PROFILE_ACTIVITY_QUERY_PREFIX = "voyagerFeedDashProfileUpdates";
13
+ export const LINKEDIN_PROFILE_ACTIVITY_CURSOR_PREFIX = "linkedin-profile-activity-v1";
14
+ export const LINKEDIN_PROFILE_ACTIVITY_MAX_ITEMS = 100;
15
+ export const LINKEDIN_PROFILE_ACTIVITY_MAX_START = 100_000;
16
+
17
+ const LINKEDIN_ORIGIN = "https://www.linkedin.com";
18
+ const ACTIVITY_URN = /urn:li:activity:([0-9]{10,20})/u;
19
+ const PROFILE_URN = /^urn:li:fsd_profile:[A-Za-z0-9_-]{1,256}$/u;
20
+ const POST_URN = /^urn:li:(?:ugcPost|share|fsd_share):[A-Za-z0-9_(),.:%=-]{1,448}$/u;
21
+ const UPDATE_TYPE = "com.linkedin.voyager.dash.feed.Update";
22
+ const SOCIAL_COUNTS_TYPE = "com.linkedin.voyager.dash.feed.SocialActivityCounts";
23
+ const COLLECTION_TYPE = "com.linkedin.restli.common.CollectionResponse";
24
+ const MEDIA_FIELD_NAMES = Object.freeze([
25
+ "articleComponent",
26
+ "content",
27
+ "contentV2",
28
+ "documentComponent",
29
+ "imageComponent",
30
+ "linkedInVideoComponent",
31
+ "videoComponent",
32
+ ] as const);
33
+
34
+ export type LinkedInProfileActivityFeed = "home" | "profile-activity";
35
+
36
+ export type LinkedInProfileActivityTarget = {
37
+ readonly slug: string;
38
+ readonly profileUrl: string;
39
+ readonly activityUrl: string;
40
+ };
41
+
42
+ export type LinkedInProfileActivityCursor = {
43
+ readonly slug: string;
44
+ readonly start: number;
45
+ };
46
+
47
+ export type LinkedInProfileActivityPostKind = "original" | "reshare";
48
+
49
+ export type LinkedInProfileActivityPost = {
50
+ readonly activityUrn: string;
51
+ readonly postUrn: string | null;
52
+ readonly url: string;
53
+ readonly authorVanity: string | null;
54
+ readonly authorUrn: string | null;
55
+ readonly text: string;
56
+ readonly textComplete: boolean;
57
+ readonly createdAt: string | null;
58
+ readonly relativeTime: string | null;
59
+ readonly reactionCount: number | null;
60
+ readonly commentCount: number | null;
61
+ readonly repostCount: number | null;
62
+ readonly hasMedia: boolean;
63
+ readonly kind: LinkedInProfileActivityPostKind;
64
+ };
65
+
66
+ export type LinkedInProfileActivityPage = {
67
+ readonly schemaVersion: 1;
68
+ readonly provider: "linkedin";
69
+ readonly feed: "profile-activity";
70
+ readonly profile: {
71
+ readonly vanity: string;
72
+ readonly profileUrn: string;
73
+ readonly url: string;
74
+ };
75
+ readonly observedAt: string;
76
+ readonly posts: readonly LinkedInProfileActivityPost[];
77
+ readonly items: readonly {
78
+ readonly activity_urn: string;
79
+ readonly url: string;
80
+ }[];
81
+ readonly nextCursor: string | null;
82
+ readonly complete: boolean;
83
+ };
84
+
85
+ export type LinkedInProfileActivityBinding = {
86
+ readonly queryId: string;
87
+ readonly profileUrn: string;
88
+ };
89
+
90
+ function isRecord(value: unknown): value is LinkedInWebJsonRecord {
91
+ return typeof value === "object" && value !== null && !Array.isArray(value);
92
+ }
93
+
94
+ function record(value: unknown, label: string): LinkedInWebJsonRecord {
95
+ if (!isRecord(value)) throw new Error(`${label} must be an object`);
96
+ return value;
97
+ }
98
+
99
+ function boundedText(value: unknown, label: string, maximum: number): string {
100
+ if (
101
+ typeof value !== "string"
102
+ || value.length < 1
103
+ || value.length > maximum
104
+ || /[\0\r]/u.test(value)
105
+ ) throw new Error(`${label} must be a bounded string`);
106
+ return value;
107
+ }
108
+
109
+ function optionalBoundedText(
110
+ value: unknown,
111
+ label: string,
112
+ maximum: number,
113
+ ): string | null {
114
+ if (value === null || value === undefined) return null;
115
+ return boundedText(value, label, maximum);
116
+ }
117
+
118
+ function nonnegativeInteger(value: unknown, label: string): number {
119
+ if (!Number.isSafeInteger(value) || (value as number) < 0) {
120
+ throw new Error(`${label} must be a non-negative safe integer`);
121
+ }
122
+ return value as number;
123
+ }
124
+
125
+ function optionalNonnegativeInteger(
126
+ value: unknown,
127
+ label: string,
128
+ ): number | null {
129
+ if (value === null || value === undefined) return null;
130
+ return nonnegativeInteger(value, label);
131
+ }
132
+
133
+ export function linkedInProfileActivityFeed(value: unknown): LinkedInProfileActivityFeed {
134
+ if (value !== "home" && value !== "profile-activity") {
135
+ throw new Error("LinkedIn feed must be home or profile-activity");
136
+ }
137
+ return value;
138
+ }
139
+
140
+ export function linkedInProfileActivityTarget(input: {
141
+ readonly profile_url?: unknown;
142
+ readonly vanity?: unknown;
143
+ }): LinkedInProfileActivityTarget {
144
+ const fromUrl = input.profile_url === undefined
145
+ ? null
146
+ : linkedInProfileActivityTargetFromUrl(input.profile_url);
147
+ const fromVanity = input.vanity === undefined
148
+ ? null
149
+ : linkedInProfileActivityTargetFromVanity(input.vanity);
150
+ if (fromUrl === null && fromVanity === null) {
151
+ throw new Error("LinkedIn profile-activity feed requires profile_url or vanity");
152
+ }
153
+ if (fromUrl !== null && fromVanity !== null && fromUrl.slug !== fromVanity.slug) {
154
+ throw new Error("LinkedIn profile_url and vanity named different profiles");
155
+ }
156
+ return fromUrl ?? fromVanity!;
157
+ }
158
+
159
+ export function linkedInProfileActivityTargetFromVanity(
160
+ value: unknown,
161
+ ): LinkedInProfileActivityTarget {
162
+ const slug = linkedInPersonalProfilePublicIdentifier(value);
163
+ return Object.freeze({
164
+ slug,
165
+ profileUrl: `${LINKEDIN_ORIGIN}/in/${slug}/`,
166
+ activityUrl: `${LINKEDIN_ORIGIN}/in/${slug}/recent-activity/all/`,
167
+ });
168
+ }
169
+
170
+ export function linkedInProfileActivityTargetFromUrl(
171
+ value: unknown,
172
+ ): LinkedInProfileActivityTarget {
173
+ const raw = boundedText(value, "LinkedIn profile activity URL", 2_048);
174
+ let url: URL;
175
+ try {
176
+ url = new URL(raw);
177
+ } catch {
178
+ throw new Error("LinkedIn profile activity URL must be an absolute URL");
179
+ }
180
+ if (
181
+ url.origin !== LINKEDIN_ORIGIN
182
+ || url.username !== ""
183
+ || url.password !== ""
184
+ || url.search !== ""
185
+ || url.hash !== ""
186
+ ) throw new Error("LinkedIn profile activity URL must use the exact public LinkedIn origin");
187
+ const match = /^\/in\/([A-Za-z0-9][A-Za-z0-9_-]{1,99})(?:\/recent-activity(?:\/all)?)?\/?$/u
188
+ .exec(url.pathname);
189
+ if (match?.[1] === undefined) {
190
+ throw new Error("LinkedIn profile activity URL has an unsupported path");
191
+ }
192
+ return linkedInProfileActivityTargetFromVanity(match[1]);
193
+ }
194
+
195
+ export function linkedInProfileActivityProfileUrn(value: unknown): string {
196
+ const urn = boundedText(value, "LinkedIn profile activity profile URN", 512);
197
+ if (!PROFILE_URN.test(urn)) {
198
+ throw new Error("LinkedIn profile activity profile URN is invalid");
199
+ }
200
+ return urn;
201
+ }
202
+
203
+ export function linkedInProfileActivityQueryId(value: unknown): string {
204
+ return resolveLinkedInRegisteredQueryId(
205
+ LINKEDIN_PROFILE_ACTIVITY_QUERY_PREFIX,
206
+ [value],
207
+ );
208
+ }
209
+
210
+ export function encodeLinkedInProfileActivityCursor(
211
+ cursor: LinkedInProfileActivityCursor,
212
+ ): string {
213
+ const slug = linkedInPersonalProfilePublicIdentifier(cursor.slug);
214
+ const start = nonnegativeInteger(cursor.start, "LinkedIn profile-activity cursor start");
215
+ if (start > LINKEDIN_PROFILE_ACTIVITY_MAX_START) {
216
+ throw new Error("LinkedIn profile-activity cursor start exceeded its reviewed bound");
217
+ }
218
+ return `${LINKEDIN_PROFILE_ACTIVITY_CURSOR_PREFIX}:${slug}:${start}`;
219
+ }
220
+
221
+ export function parseLinkedInProfileActivityCursor(
222
+ value: unknown,
223
+ expectedSlug: string,
224
+ ): LinkedInProfileActivityCursor {
225
+ if (value === undefined) {
226
+ return Object.freeze({
227
+ slug: linkedInPersonalProfilePublicIdentifier(expectedSlug),
228
+ start: 0,
229
+ });
230
+ }
231
+ const raw = boundedText(value, "LinkedIn profile-activity cursor", 4_096);
232
+ const match = new RegExp(
233
+ `^${LINKEDIN_PROFILE_ACTIVITY_CURSOR_PREFIX}:([A-Za-z0-9][A-Za-z0-9_-]{1,99}):(0|[1-9][0-9]{0,6})$`,
234
+ "u",
235
+ ).exec(raw);
236
+ if (match?.[1] === undefined || match[2] === undefined) {
237
+ throw new Error("LinkedIn profile-activity cursor is invalid");
238
+ }
239
+ const slug = linkedInPersonalProfilePublicIdentifier(match[1]);
240
+ const expected = linkedInPersonalProfilePublicIdentifier(expectedSlug);
241
+ if (slug !== expected) {
242
+ throw new Error("LinkedIn profile-activity cursor does not match the requested profile");
243
+ }
244
+ const start = nonnegativeInteger(Number(match[2]), "LinkedIn profile-activity cursor start");
245
+ if (start > LINKEDIN_PROFILE_ACTIVITY_MAX_START) {
246
+ throw new Error("LinkedIn profile-activity cursor start exceeded its reviewed bound");
247
+ }
248
+ return Object.freeze({ slug, start });
249
+ }
250
+
251
+ export function linkedInProfileActivityPageUrl(input: {
252
+ readonly queryId: unknown;
253
+ readonly profileUrn: unknown;
254
+ readonly count: number;
255
+ readonly start: number;
256
+ }): URL {
257
+ const queryId = linkedInProfileActivityQueryId(input.queryId);
258
+ const profileUrn = linkedInProfileActivityProfileUrn(input.profileUrn);
259
+ const count = nonnegativeInteger(input.count, "LinkedIn profile-activity count");
260
+ const start = nonnegativeInteger(input.start, "LinkedIn profile-activity start");
261
+ if (count < 1 || count > LINKEDIN_PROFILE_ACTIVITY_MAX_ITEMS) {
262
+ throw new Error("LinkedIn profile-activity count must be an integer between 1 and 100");
263
+ }
264
+ if (start > LINKEDIN_PROFILE_ACTIVITY_MAX_START) {
265
+ throw new Error("LinkedIn profile-activity start exceeded its reviewed bound");
266
+ }
267
+ const variables = `(count:${count},start:${start},profileUrn:${encodeRestliV2Value(profileUrn)})`;
268
+ return new URL(
269
+ `${LINKEDIN_ORIGIN}${LINKEDIN_GRAPHQL_PATH}?includeWebMetadata=true&queryId=${encodeURIComponent(queryId)}&variables=${variables}`,
270
+ );
271
+ }
272
+
273
+ export function assertLinkedInProfileActivityRequest(
274
+ requestValue: {
275
+ readonly url: string | URL;
276
+ readonly method: string;
277
+ },
278
+ expected: {
279
+ readonly queryId: unknown;
280
+ readonly profileUrn: unknown;
281
+ readonly count: number;
282
+ readonly start: number;
283
+ },
284
+ ): URL {
285
+ const url = requestValue.url instanceof URL
286
+ ? new URL(requestValue.url.href)
287
+ : new URL(requestValue.url);
288
+ if (
289
+ requestValue.method !== "GET"
290
+ || url.origin !== LINKEDIN_ORIGIN
291
+ || url.pathname !== LINKEDIN_GRAPHQL_PATH
292
+ || url.username !== ""
293
+ || url.password !== ""
294
+ || url.hash !== ""
295
+ ) throw new Error("LinkedIn profile-activity request escaped its exact reviewed route");
296
+ const queryNames = [...url.searchParams.keys()];
297
+ if (
298
+ queryNames.length !== 3
299
+ || queryNames[0] !== "includeWebMetadata"
300
+ || queryNames[1] !== "queryId"
301
+ || queryNames[2] !== "variables"
302
+ || url.searchParams.getAll("includeWebMetadata").length !== 1
303
+ || url.searchParams.getAll("queryId").length !== 1
304
+ || url.searchParams.getAll("variables").length !== 1
305
+ ) throw new Error("LinkedIn profile-activity request query shape is invalid");
306
+ const expectedUrl = linkedInProfileActivityPageUrl(expected);
307
+ if (
308
+ url.searchParams.get("includeWebMetadata") !== expectedUrl.searchParams.get("includeWebMetadata")
309
+ || url.searchParams.get("queryId") !== expectedUrl.searchParams.get("queryId")
310
+ || url.searchParams.get("variables") !== expectedUrl.searchParams.get("variables")
311
+ || url.search !== expectedUrl.search
312
+ ) throw new Error("LinkedIn profile-activity request binding changed");
313
+ return url;
314
+ }
315
+
316
+ export function linkedInProfileActivityVariables(
317
+ value: unknown,
318
+ ): { readonly profileUrn: string; readonly count: number | null; readonly start: number | null } {
319
+ const raw = boundedText(value, "LinkedIn profile-activity variables", 4_096);
320
+ const match = /^\((.*)\)$/u.exec(raw);
321
+ if (match?.[1] === undefined) {
322
+ throw new Error("LinkedIn profile-activity variables must be a Rest.li tuple");
323
+ }
324
+ const fields = new Map<string, string>();
325
+ for (const part of match[1].split(",")) {
326
+ const separator = part.indexOf(":");
327
+ if (separator < 1) throw new Error("LinkedIn profile-activity variables field is malformed");
328
+ const key = part.slice(0, separator);
329
+ const fieldValue = part.slice(separator + 1);
330
+ if (!/^[A-Za-z][A-Za-z0-9]{0,63}$/u.test(key) || fieldValue.length < 1) {
331
+ throw new Error("LinkedIn profile-activity variables field is malformed");
332
+ }
333
+ if (fields.has(key)) throw new Error("LinkedIn profile-activity variables repeated a field");
334
+ fields.set(key, fieldValue);
335
+ }
336
+ const profileUrn = linkedInProfileActivityProfileUrn(fields.get("profileUrn"));
337
+ const count = fields.has("count")
338
+ ? nonnegativeInteger(Number(fields.get("count")), "LinkedIn profile-activity variables count")
339
+ : null;
340
+ const start = fields.has("start")
341
+ ? nonnegativeInteger(Number(fields.get("start")), "LinkedIn profile-activity variables start")
342
+ : null;
343
+ return Object.freeze({ profileUrn, count, start });
344
+ }
345
+
346
+ export function resolveLinkedInProfileActivityBinding(
347
+ candidates: readonly {
348
+ readonly method?: unknown;
349
+ readonly status?: unknown;
350
+ readonly url?: unknown;
351
+ }[],
352
+ ): LinkedInProfileActivityBinding {
353
+ if (candidates.length > 4_096) {
354
+ throw new Error("LinkedIn profile-activity observation exceeded its reviewed bound");
355
+ }
356
+ const queryIds = new Set<string>();
357
+ const profileUrns = new Set<string>();
358
+ for (const candidate of candidates) {
359
+ if (candidate.method !== "GET" || candidate.status !== 200) continue;
360
+ if (typeof candidate.url !== "string" || candidate.url.length > 64 * 1_024) continue;
361
+ let url: URL;
362
+ try {
363
+ url = new URL(candidate.url);
364
+ } catch {
365
+ continue;
366
+ }
367
+ if (
368
+ url.origin !== LINKEDIN_ORIGIN
369
+ || url.pathname !== LINKEDIN_GRAPHQL_PATH
370
+ || url.username !== ""
371
+ || url.password !== ""
372
+ || url.hash !== ""
373
+ ) continue;
374
+ const queryId = url.searchParams.get("queryId");
375
+ const variables = url.searchParams.get("variables");
376
+ if (queryId === null || variables === null) continue;
377
+ try {
378
+ queryIds.add(linkedInProfileActivityQueryId(queryId));
379
+ profileUrns.add(linkedInProfileActivityVariables(variables).profileUrn);
380
+ } catch {
381
+ continue;
382
+ }
383
+ }
384
+ if (queryIds.size === 0 || profileUrns.size === 0) {
385
+ throw new Error("LinkedIn registered query voyagerFeedDashProfileUpdates was not found");
386
+ }
387
+ if (queryIds.size !== 1) {
388
+ throw new Error("LinkedIn registered query voyagerFeedDashProfileUpdates is ambiguous");
389
+ }
390
+ if (profileUrns.size !== 1) {
391
+ throw new Error("LinkedIn profile-activity observation bound more than one profile URN");
392
+ }
393
+ return Object.freeze({
394
+ queryId: [...queryIds][0]!,
395
+ profileUrn: [...profileUrns][0]!,
396
+ });
397
+ }
398
+
399
+ function activityUrnFromValue(value: unknown): string | null {
400
+ if (typeof value !== "string" || value.length > 4_096) return null;
401
+ const match = ACTIVITY_URN.exec(value);
402
+ return match?.[0] ?? null;
403
+ }
404
+
405
+ function uniqueOptionalText(
406
+ values: readonly unknown[],
407
+ label: string,
408
+ maximum: number,
409
+ ): string | null {
410
+ const unique = [...new Set(
411
+ values
412
+ .filter((value) => value !== undefined && value !== null && value !== "")
413
+ .map((value) => boundedText(value, label, maximum)),
414
+ )];
415
+ if (unique.length === 0) return null;
416
+ if (unique.length !== 1) throw new Error(`${label} was ambiguous`);
417
+ return unique[0]!;
418
+ }
419
+
420
+ function textFromViewModel(value: unknown, label: string): string | null {
421
+ if (value === null || value === undefined) return null;
422
+ const model = record(value, label);
423
+ if (isRecord(model.text)) {
424
+ return optionalBoundedText(model.text.text, `${label}.text.text`, 12_000);
425
+ }
426
+ return optionalBoundedText(model.text, `${label}.text`, 12_000);
427
+ }
428
+
429
+ function vanityFromNavigation(value: unknown): string | null {
430
+ if (typeof value !== "string" || value.length > 2_048) return null;
431
+ let url: URL;
432
+ try {
433
+ url = new URL(value, LINKEDIN_ORIGIN);
434
+ } catch {
435
+ return null;
436
+ }
437
+ if (url.origin !== LINKEDIN_ORIGIN) return null;
438
+ const match = /^\/in\/([A-Za-z0-9][A-Za-z0-9_-]{1,99})\/?$/u.exec(url.pathname);
439
+ return match?.[1] === undefined
440
+ ? null
441
+ : linkedInPersonalProfilePublicIdentifier(match[1]);
442
+ }
443
+
444
+ function createdAtFromValue(value: unknown, label: string): string | null {
445
+ if (value === null || value === undefined) return null;
446
+ if (typeof value === "number") {
447
+ const milliseconds = nonnegativeInteger(value, label);
448
+ if (milliseconds < 1_000_000_000_000 || milliseconds > 4_102_444_800_000) {
449
+ throw new Error(`${label} is outside the reviewed timestamp window`);
450
+ }
451
+ return new Date(milliseconds).toISOString();
452
+ }
453
+ const raw = boundedText(value, label, 64);
454
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{3})?Z$/u.test(raw)) {
455
+ throw new Error(`${label} must be an exact UTC timestamp`);
456
+ }
457
+ const parsed = Date.parse(raw);
458
+ if (!Number.isFinite(parsed)) throw new Error(`${label} must be an exact UTC timestamp`);
459
+ return new Date(parsed).toISOString();
460
+ }
461
+
462
+ function relativeTimeFromActor(actor: LinkedInWebJsonRecord | null): string | null {
463
+ if (actor === null) return null;
464
+ const fromViewModel = isRecord(actor.subDescription)
465
+ ? textFromViewModel(actor.subDescription, "LinkedIn update actor.subDescription")
466
+ : null;
467
+ const fromText = typeof actor.subDescription === "string"
468
+ ? optionalBoundedText(actor.subDescription, "LinkedIn update actor.subDescription", 64)
469
+ : null;
470
+ const unique = [...new Set(
471
+ [fromViewModel, fromText].filter((value): value is string => value !== null),
472
+ )];
473
+ if (unique.length !== 1) return null;
474
+ const value = unique[0]!;
475
+ return /^(?:[1-9][0-9]?[smhdw]|[1-9][0-9]?mo|now|just now)$/iu.test(value)
476
+ ? value
477
+ : null;
478
+ }
479
+
480
+ function authorUrnFromActor(actor: LinkedInWebJsonRecord | null): string | null {
481
+ if (actor === null) return null;
482
+ for (const field of ["urn", "backendUrn", "entityUrn", "*profile"] as const) {
483
+ const value = actor[field];
484
+ if (typeof value === "string" && PROFILE_URN.test(value)) return value;
485
+ }
486
+ return null;
487
+ }
488
+
489
+ function postUrnFromUpdate(update: LinkedInWebJsonRecord): string | null {
490
+ const metadata = isRecord(update.metadata) ? update.metadata : null;
491
+ const socialDetail = isRecord(update.socialDetail) ? update.socialDetail : null;
492
+ const candidates = [
493
+ metadata?.shareUrn,
494
+ metadata?.backendUrn,
495
+ metadata?.ugcPostUrn,
496
+ socialDetail?.urn,
497
+ update.shareUrn,
498
+ update.ugcPostUrn,
499
+ ];
500
+ const urns = candidates.filter((value): value is string =>
501
+ typeof value === "string" && POST_URN.test(value));
502
+ return uniqueOptionalText(urns, "LinkedIn update post URN", 512);
503
+ }
504
+
505
+ function hasReviewedMedia(update: LinkedInWebJsonRecord): boolean {
506
+ return MEDIA_FIELD_NAMES.some((field) => {
507
+ const value = update[field];
508
+ return value !== undefined && value !== null && value !== false;
509
+ });
510
+ }
511
+
512
+ function commentaryTruncated(commentary: LinkedInWebJsonRecord | null): boolean {
513
+ if (commentary === null) return false;
514
+ if (commentary.truncated === true || commentary.textTruncated === true) return true;
515
+ const attributes = commentary.attributesV2;
516
+ return Array.isArray(attributes) && attributes.some((attribute) => {
517
+ if (!isRecord(attribute)) return false;
518
+ const type = attribute.$type;
519
+ return typeof type === "string" && /seeMore|SeeMore|truncated/iu.test(type);
520
+ });
521
+ }
522
+
523
+ function socialCountsByActivityUrn(
524
+ included: readonly LinkedInWebJsonRecord[],
525
+ ): ReadonlyMap<string, {
526
+ readonly reactionCount: number | null;
527
+ readonly commentCount: number | null;
528
+ readonly repostCount: number | null;
529
+ }> {
530
+ const counts = new Map<string, {
531
+ readonly reactionCount: number | null;
532
+ readonly commentCount: number | null;
533
+ readonly repostCount: number | null;
534
+ }>();
535
+ for (const entity of included) {
536
+ if (entity.$type !== SOCIAL_COUNTS_TYPE) continue;
537
+ const activityUrn = activityUrnFromValue(entity.urn);
538
+ if (activityUrn === null) continue;
539
+ const next = Object.freeze({
540
+ reactionCount: optionalNonnegativeInteger(entity.numLikes, "LinkedIn social numLikes"),
541
+ commentCount: optionalNonnegativeInteger(entity.numComments, "LinkedIn social numComments"),
542
+ repostCount: optionalNonnegativeInteger(entity.numShares, "LinkedIn social numShares"),
543
+ });
544
+ const prior = counts.get(activityUrn);
545
+ if (prior !== undefined) {
546
+ if (
547
+ prior.reactionCount !== next.reactionCount
548
+ || prior.commentCount !== next.commentCount
549
+ || prior.repostCount !== next.repostCount
550
+ ) throw new Error("LinkedIn social activity counts conflict for one activity URN");
551
+ continue;
552
+ }
553
+ counts.set(activityUrn, next);
554
+ }
555
+ return counts;
556
+ }
557
+
558
+ function projectUpdate(
559
+ update: LinkedInWebJsonRecord,
560
+ counts: ReadonlyMap<string, {
561
+ readonly reactionCount: number | null;
562
+ readonly commentCount: number | null;
563
+ readonly repostCount: number | null;
564
+ }>,
565
+ ): LinkedInProfileActivityPost {
566
+ if (update.$type !== UPDATE_TYPE) {
567
+ throw new Error("LinkedIn profile-activity element is not a feed Update");
568
+ }
569
+ const activityUrn = activityUrnFromValue(update.entityUrn)
570
+ ?? activityUrnFromValue(update.urn)
571
+ ?? activityUrnFromValue(isRecord(update.metadata) ? update.metadata.urn : null);
572
+ if (activityUrn === null) {
573
+ throw new Error("LinkedIn profile-activity update omitted its activity URN");
574
+ }
575
+ const commentary = isRecord(update.commentary) ? update.commentary : null;
576
+ const text = textFromViewModel(commentary, "LinkedIn update commentary") ?? "";
577
+ const actor = isRecord(update.actor) ? update.actor : null;
578
+ const navigation = actor === null
579
+ ? null
580
+ : vanityFromNavigation(
581
+ isRecord(actor.navigationContext)
582
+ ? actor.navigationContext.actionTarget
583
+ : actor.navigationUrl,
584
+ );
585
+ const kind: LinkedInProfileActivityPostKind = update.resharedUpdate === undefined
586
+ || update.resharedUpdate === null
587
+ ? "original"
588
+ : "reshare";
589
+ const engagement = counts.get(activityUrn);
590
+ const createdAt = createdAtFromValue(
591
+ update.publishedAt ?? update.createdAt ?? (isRecord(update.metadata) ? update.metadata.createdAt : null),
592
+ "LinkedIn update createdAt",
593
+ );
594
+ return Object.freeze({
595
+ activityUrn,
596
+ postUrn: postUrnFromUpdate(update),
597
+ url: `${LINKEDIN_ORIGIN}/feed/update/${activityUrn}/`,
598
+ authorVanity: navigation,
599
+ authorUrn: authorUrnFromActor(actor),
600
+ text,
601
+ textComplete: !commentaryTruncated(commentary),
602
+ createdAt,
603
+ relativeTime: relativeTimeFromActor(actor),
604
+ reactionCount: engagement?.reactionCount ?? null,
605
+ commentCount: engagement?.commentCount ?? null,
606
+ repostCount: engagement?.repostCount ?? null,
607
+ hasMedia: hasReviewedMedia(update),
608
+ kind,
609
+ });
610
+ }
611
+
612
+ function collectionResponses(
613
+ value: unknown,
614
+ depth: number,
615
+ seen: WeakSet<object>,
616
+ ): readonly LinkedInWebJsonRecord[] {
617
+ if (depth > 8 || value === null || typeof value !== "object") return [];
618
+ if (seen.has(value)) return [];
619
+ seen.add(value);
620
+ if (Array.isArray(value)) {
621
+ if (value.length > 256) throw new Error("LinkedIn profile-activity data exceeded its reviewed bound");
622
+ return value.flatMap((item) => collectionResponses(item, depth + 1, seen));
623
+ }
624
+ const candidate = record(value, "LinkedIn profile-activity data node");
625
+ const collections: LinkedInWebJsonRecord[] = [];
626
+ if (
627
+ candidate.$type === COLLECTION_TYPE
628
+ && Array.isArray(candidate["*elements"])
629
+ ) collections.push(candidate);
630
+ for (const nested of Object.values(candidate)) {
631
+ collections.push(...collectionResponses(nested, depth + 1, seen));
632
+ }
633
+ return collections;
634
+ }
635
+
636
+ function pagingHasNext(input: {
637
+ readonly start: number;
638
+ readonly count: number;
639
+ readonly total: number | null;
640
+ readonly hasNextLink: boolean;
641
+ }): boolean {
642
+ if (input.hasNextLink) return true;
643
+ return input.total !== null && input.total > 0 && input.start + input.count < input.total;
644
+ }
645
+
646
+ function profileActivityPageCursor(input: {
647
+ readonly paging: unknown;
648
+ readonly profileUrn: string;
649
+ readonly queryId: string;
650
+ readonly requestedCount: number;
651
+ readonly requestedStart: number;
652
+ readonly returnedCount: number;
653
+ }): { readonly known: boolean; readonly nextStart: number | null } {
654
+ if (input.paging === undefined) {
655
+ return Object.freeze({ known: false, nextStart: null });
656
+ }
657
+ const paging = record(input.paging, "LinkedIn profile-activity paging");
658
+ const start = nonnegativeInteger(
659
+ paging.start,
660
+ "LinkedIn profile-activity paging.start",
661
+ );
662
+ const count = nonnegativeInteger(
663
+ paging.count,
664
+ "LinkedIn profile-activity paging.count",
665
+ );
666
+ if (start !== input.requestedStart || count !== input.requestedCount) {
667
+ throw new Error("LinkedIn profile-activity paging did not bind the exact requested page");
668
+ }
669
+ if (input.returnedCount > count) {
670
+ throw new Error("LinkedIn profile-activity paging count contradicted the returned page");
671
+ }
672
+ const total = optionalNonnegativeInteger(
673
+ paging.total,
674
+ "LinkedIn profile-activity paging.total",
675
+ );
676
+ if (total !== null && total > 0) {
677
+ const expectedReturned = Math.min(count, Math.max(total - start, 0));
678
+ if (input.returnedCount !== expectedReturned) {
679
+ throw new Error("LinkedIn profile-activity page length contradicted its paging total");
680
+ }
681
+ }
682
+
683
+ let hasNextLink = false;
684
+ if (paging.links !== undefined) {
685
+ if (!Array.isArray(paging.links) || paging.links.length > 16) {
686
+ throw new Error("LinkedIn profile-activity paging links were invalid or exceeded their bound");
687
+ }
688
+ const seenRelations = new Set<string>();
689
+ for (let index = 0; index < paging.links.length; index += 1) {
690
+ const label = `LinkedIn profile-activity paging.links[${index}]`;
691
+ const link = record(paging.links[index], label);
692
+ const relation = boundedText(link.rel, `${label}.rel`, 100);
693
+ if (relation !== "next" && relation !== "prev") {
694
+ throw new Error("LinkedIn profile-activity paging returned an invalid relation");
695
+ }
696
+ if (seenRelations.has(relation)) {
697
+ throw new Error(`LinkedIn profile-activity paging repeated its ${relation} relation`);
698
+ }
699
+ seenRelations.add(relation);
700
+ const href = boundedText(link.href, `${label}.href`, 8_192);
701
+ if (/\s/u.test(href)) {
702
+ throw new Error("LinkedIn profile-activity paging returned an invalid URL");
703
+ }
704
+ const linkedStart = relation === "next"
705
+ ? start + count
706
+ : Math.max(0, start - count);
707
+ if (
708
+ !Number.isSafeInteger(linkedStart)
709
+ || linkedStart > LINKEDIN_PROFILE_ACTIVITY_MAX_START
710
+ || (relation === "next" && linkedStart <= start)
711
+ || (relation === "prev" && start === 0)
712
+ ) {
713
+ throw new Error(`LinkedIn profile-activity paging returned a contradictory ${relation} link`);
714
+ }
715
+ let linkedPage: URL;
716
+ try {
717
+ linkedPage = new URL(href, LINKEDIN_ORIGIN);
718
+ } catch {
719
+ throw new Error("LinkedIn profile-activity paging returned an invalid URL");
720
+ }
721
+ linkedInProfileActivityQueryId(linkedPage.searchParams.get("queryId"));
722
+ const expectedPage = linkedInProfileActivityPageUrl({
723
+ queryId: input.queryId,
724
+ profileUrn: input.profileUrn,
725
+ count,
726
+ start: linkedStart,
727
+ });
728
+ if (linkedPage.href !== expectedPage.href) {
729
+ throw new Error("LinkedIn profile-activity paging link changed the exact collection");
730
+ }
731
+ if (relation === "next") hasNextLink = true;
732
+ }
733
+ }
734
+
735
+ if (pagingHasNext({ start, count, total, hasNextLink })) {
736
+ if (input.returnedCount === 0) {
737
+ throw new Error("LinkedIn profile-activity page did not advance its cursor");
738
+ }
739
+ if (hasNextLink && input.returnedCount < count) {
740
+ throw new Error("LinkedIn profile-activity paging linked past a terminal short page");
741
+ }
742
+ if (total !== null && total > 0 && start + input.returnedCount >= total) {
743
+ throw new Error("LinkedIn profile-activity paging contradicted its terminal total");
744
+ }
745
+ return Object.freeze({ known: true, nextStart: start + count });
746
+ }
747
+ const known = paging.links !== undefined || total !== null;
748
+ return Object.freeze({ known, nextStart: null });
749
+ }
750
+
751
+ export function projectLinkedInProfileActivityPage(input: {
752
+ readonly response: unknown;
753
+ readonly target: LinkedInProfileActivityTarget;
754
+ readonly profileUrn: unknown;
755
+ readonly queryId: unknown;
756
+ readonly limit: number;
757
+ readonly start: number;
758
+ readonly observedAt: string;
759
+ }): LinkedInProfileActivityPage {
760
+ const profileUrn = linkedInProfileActivityProfileUrn(input.profileUrn);
761
+ const queryId = linkedInProfileActivityQueryId(input.queryId);
762
+ const limit = nonnegativeInteger(input.limit, "LinkedIn profile-activity limit");
763
+ const start = nonnegativeInteger(input.start, "LinkedIn profile-activity start");
764
+ if (limit < 1 || limit > LINKEDIN_PROFILE_ACTIVITY_MAX_ITEMS) {
765
+ throw new Error("LinkedIn profile-activity limit must be an integer between 1 and 100");
766
+ }
767
+ if (
768
+ !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u.test(input.observedAt)
769
+ || !Number.isFinite(Date.parse(input.observedAt))
770
+ ) throw new Error("LinkedIn profile-activity observedAt must be an exact UTC timestamp");
771
+ const normalized = normalizeLinkedInGraphqlEnvelope(input.response);
772
+ const collections = collectionResponses(normalized.data, 0, new WeakSet<object>());
773
+ if (collections.length !== 1) {
774
+ throw new Error("LinkedIn profile-activity response did not bind one collection");
775
+ }
776
+ const collection = collections[0]!;
777
+ const references = collection["*elements"];
778
+ if (!Array.isArray(references) || references.length > LINKEDIN_PROFILE_ACTIVITY_MAX_ITEMS) {
779
+ throw new Error("LinkedIn profile-activity collection exceeded its reviewed bound");
780
+ }
781
+ if (references.length > limit) {
782
+ throw new Error("LinkedIn profile-activity page exceeded the requested limit");
783
+ }
784
+ const counts = socialCountsByActivityUrn(normalized.included);
785
+ const posts = references.map((reference, index) => {
786
+ const urn = boundedText(reference, `LinkedIn profile-activity elements[${index}]`, 4_096);
787
+ const entity = normalized.entitiesByUrn.get(urn);
788
+ if (entity === undefined) {
789
+ throw new Error("LinkedIn profile-activity response omitted a referenced update");
790
+ }
791
+ return projectUpdate(entity, counts);
792
+ });
793
+ const cursor = profileActivityPageCursor({
794
+ paging: collection.paging,
795
+ profileUrn,
796
+ queryId,
797
+ requestedCount: limit,
798
+ requestedStart: start,
799
+ returnedCount: posts.length,
800
+ });
801
+ if (!cursor.known && posts.length === 0) {
802
+ throw new Error("LinkedIn profile-activity page did not advance its cursor");
803
+ }
804
+ const nextCursor = cursor.nextStart === null
805
+ ? null
806
+ : encodeLinkedInProfileActivityCursor({
807
+ slug: input.target.slug,
808
+ start: cursor.nextStart,
809
+ });
810
+ return Object.freeze({
811
+ schemaVersion: 1,
812
+ provider: "linkedin",
813
+ feed: "profile-activity",
814
+ profile: Object.freeze({
815
+ vanity: input.target.slug,
816
+ profileUrn,
817
+ url: input.target.profileUrl,
818
+ }),
819
+ observedAt: input.observedAt,
820
+ posts: Object.freeze(posts),
821
+ items: Object.freeze(posts.map((post) => Object.freeze({
822
+ activity_urn: post.activityUrn,
823
+ url: post.url,
824
+ }))),
825
+ nextCursor,
826
+ complete: cursor.known && nextCursor === null,
827
+ });
828
+ }
829
+
830
+ export function linkedInProfileActivityInputIssues(
831
+ input: Readonly<Record<string, unknown>>,
832
+ ): readonly string[] {
833
+ const issues: string[] = [];
834
+ if (input.feed === "home") {
835
+ if (input.profile_url !== undefined) {
836
+ issues.push("input.profile_url is not accepted for the capture-required home feed");
837
+ }
838
+ if (input.vanity !== undefined) {
839
+ issues.push("input.vanity is not accepted for the capture-required home feed");
840
+ }
841
+ return Object.freeze(issues);
842
+ }
843
+ if (input.feed !== "profile-activity") return Object.freeze(issues);
844
+ try {
845
+ linkedInProfileActivityTarget({
846
+ profile_url: input.profile_url,
847
+ vanity: input.vanity,
848
+ });
849
+ } catch (error) {
850
+ issues.push(error instanceof Error ? error.message : "LinkedIn profile-activity target is invalid");
851
+ }
852
+ if (input.cursor !== undefined) {
853
+ try {
854
+ const target = linkedInProfileActivityTarget({
855
+ profile_url: input.profile_url,
856
+ vanity: input.vanity,
857
+ });
858
+ parseLinkedInProfileActivityCursor(input.cursor, target.slug);
859
+ } catch (error) {
860
+ issues.push(error instanceof Error ? error.message : "LinkedIn profile-activity cursor is invalid");
861
+ }
862
+ }
863
+ return Object.freeze(issues);
864
+ }