@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,690 @@
1
+ /**
2
+ * Network-inert Facebook Group feed projection.
3
+ *
4
+ * The source HTML comes from a direct authenticated GET of
5
+ * `/groups/{numeric-id}/`. Ordinary provider access logs may record the GET,
6
+ * but ghostget sends no client-interaction or mutation request and never retries
7
+ * an indeterminate read. This module parses inert JSON only; it never executes
8
+ * page JavaScript or performs a request.
9
+ */
10
+ import { assertMetaCometReadActor } from "./meta-bootstrap";
11
+ import {
12
+ isReviewedFacebookGroupRelayResultEnvelope,
13
+ parseFacebookViewerId,
14
+ parseMetaJsonScripts,
15
+ } from "./meta-web";
16
+
17
+ type JsonRecord = Record<string, unknown>;
18
+
19
+ type IndexedEdge = {
20
+ readonly index: number;
21
+ readonly edge: JsonRecord;
22
+ };
23
+
24
+ export type FacebookGroupFeedPost = {
25
+ readonly id: string;
26
+ readonly creation_time: number;
27
+ readonly message: string | null;
28
+ readonly actors: readonly {
29
+ readonly id: string | null;
30
+ readonly name: string | null;
31
+ }[];
32
+ };
33
+
34
+ export type FacebookGroupFeed = {
35
+ readonly feed: "group";
36
+ readonly group_id: string;
37
+ readonly posts: readonly FacebookGroupFeedPost[];
38
+ readonly provider_has_next_page: boolean;
39
+ readonly next_cursor: string | null;
40
+ readonly continuation_supported: boolean;
41
+ readonly truncated: boolean;
42
+ readonly complete: boolean;
43
+ };
44
+
45
+ const MAX_TREE_NODES = 250_000;
46
+ const MAX_TREE_DEPTH = 40;
47
+ const MAX_CONTAINER_ENTRIES = 10_000;
48
+ const MAX_PROVIDER_EDGES = 500;
49
+ const MAX_ACTORS = 20;
50
+ const FACEBOOK_GROUP_STREAM_KEY_PATTERN =
51
+ /^adp_CometGroupDiscussionRootSuccessQueryRelayPreloader_[A-Za-z0-9_]{1,192}$/u;
52
+ const FACEBOOK_GROUP_STREAM_RESULT_PATH = Object.freeze([
53
+ "require",
54
+ "[]",
55
+ "[]",
56
+ "[]",
57
+ "__bbox",
58
+ "require",
59
+ "[]",
60
+ "[]",
61
+ "[]",
62
+ "__bbox",
63
+ "result",
64
+ ]);
65
+
66
+ function isRecord(value: unknown): value is JsonRecord {
67
+ return typeof value === "object" && value !== null && !Array.isArray(value);
68
+ }
69
+
70
+ function isUnknownArray(value: unknown): value is readonly unknown[] {
71
+ return Array.isArray(value);
72
+ }
73
+
74
+ function record(value: unknown, label: string): JsonRecord {
75
+ if (!isRecord(value)) throw new Error(`${label} must be an object`);
76
+ return value;
77
+ }
78
+
79
+ function exactKeys(value: JsonRecord, expected: readonly string[], label: string): void {
80
+ const actual = Object.keys(value).sort();
81
+ const wanted = [...expected].sort();
82
+ if (
83
+ actual.length !== wanted.length
84
+ || actual.some((key, index) => key !== wanted[index])
85
+ ) throw new Error(`${label} changed its reviewed fields`);
86
+ }
87
+
88
+ function boundedString(
89
+ value: unknown,
90
+ label: string,
91
+ maximum: number,
92
+ minimum = 1,
93
+ ): string {
94
+ if (
95
+ typeof value !== "string"
96
+ || value.length < minimum
97
+ || value.length > maximum
98
+ || /[\0]/u.test(value)
99
+ ) throw new Error(`${label} must be a bounded string`);
100
+ return value;
101
+ }
102
+
103
+ function optionalString(
104
+ value: unknown,
105
+ label: string,
106
+ maximum: number,
107
+ ): string | null {
108
+ if (value === undefined || value === null) return null;
109
+ return boundedString(value, label, maximum, 0);
110
+ }
111
+
112
+ function decimalId(value: unknown, label: string, maximum = 32): string {
113
+ const id = boundedString(value, label, maximum);
114
+ if (!/^[0-9]+$/u.test(id) || id === "0") {
115
+ throw new Error(`${label} must be a stable decimal ID`);
116
+ }
117
+ return id;
118
+ }
119
+
120
+ function boundedInteger(value: unknown, label: string): number {
121
+ if (
122
+ !Number.isSafeInteger(value)
123
+ || (value as number) < 0
124
+ ) throw new Error(`${label} must be a non-negative safe integer`);
125
+ return value as number;
126
+ }
127
+
128
+ function exactBoolean(value: unknown, label: string): boolean {
129
+ if (typeof value !== "boolean") throw new Error(`${label} must be boolean`);
130
+ return value;
131
+ }
132
+
133
+ function walk(
134
+ roots: readonly unknown[],
135
+ visit: (value: unknown, path: readonly string[]) => void,
136
+ ): void {
137
+ const stack: {
138
+ readonly value: unknown;
139
+ readonly depth: number;
140
+ readonly path: readonly string[];
141
+ }[] = roots
142
+ .map((value) => ({
143
+ value,
144
+ depth: 0,
145
+ path: Object.freeze([]),
146
+ }))
147
+ .reverse();
148
+ let nodes = 0;
149
+ while (stack.length > 0) {
150
+ const item = stack.pop();
151
+ if (item === undefined) break;
152
+ nodes += 1;
153
+ if (nodes > MAX_TREE_NODES) {
154
+ throw new Error("Facebook Group bootstrap exceeded its reviewed structural bound");
155
+ }
156
+ visit(item.value, item.path);
157
+ if (item.depth >= MAX_TREE_DEPTH) {
158
+ if (
159
+ (Array.isArray(item.value) && item.value.length > 0)
160
+ || (isRecord(item.value) && Object.keys(item.value).length > 0)
161
+ ) {
162
+ throw new Error("Facebook Group bootstrap exceeded its reviewed depth bound");
163
+ }
164
+ continue;
165
+ }
166
+ if (Array.isArray(item.value)) {
167
+ if (item.value.length > MAX_CONTAINER_ENTRIES) {
168
+ throw new Error("Facebook Group bootstrap contained an oversized array");
169
+ }
170
+ for (let index = item.value.length - 1; index >= 0; index -= 1) {
171
+ stack.push({
172
+ value: item.value[index],
173
+ depth: item.depth + 1,
174
+ path: Object.freeze([...item.path, "[]"]),
175
+ });
176
+ }
177
+ continue;
178
+ }
179
+ if (!isRecord(item.value)) continue;
180
+ const entries = Object.entries(item.value);
181
+ if (entries.length > MAX_CONTAINER_ENTRIES) {
182
+ throw new Error("Facebook Group bootstrap contained an oversized object");
183
+ }
184
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
185
+ const entry = entries[index];
186
+ if (entry === undefined) continue;
187
+ stack.push({
188
+ value: entry[1],
189
+ depth: item.depth + 1,
190
+ path: Object.freeze([...item.path, entry[0]]),
191
+ });
192
+ }
193
+ }
194
+ }
195
+
196
+ function pathEquals(
197
+ value: readonly string[],
198
+ expected: readonly string[],
199
+ ): boolean {
200
+ return value.length === expected.length
201
+ && value.every((segment, index) => segment === expected[index]);
202
+ }
203
+
204
+ function reviewedFacebookGroupStreamKey(
205
+ roots: readonly unknown[],
206
+ path: readonly string[],
207
+ value: unknown,
208
+ ): string | null {
209
+ if (
210
+ !pathEquals(path, FACEBOOK_GROUP_STREAM_RESULT_PATH)
211
+ || !isReviewedFacebookGroupRelayResultEnvelope(roots, path, value)
212
+ ) return null;
213
+
214
+ const keys = new Set<string>();
215
+ for (const root of roots) {
216
+ if (!isRecord(root) || !isUnknownArray(root.require)) continue;
217
+ for (const scheduled of root.require) {
218
+ if (
219
+ !isUnknownArray(scheduled)
220
+ || scheduled.length !== 4
221
+ || scheduled[0] !== "ScheduledServerJS"
222
+ || scheduled[1] !== "handle"
223
+ || scheduled[2] !== null
224
+ || !isUnknownArray(scheduled[3])
225
+ ) continue;
226
+ for (const scheduledPayload of scheduled[3]) {
227
+ if (
228
+ !isRecord(scheduledPayload)
229
+ || !isRecord(scheduledPayload.__bbox)
230
+ || !isUnknownArray(scheduledPayload.__bbox.require)
231
+ ) continue;
232
+ for (const streamValue of scheduledPayload.__bbox.require) {
233
+ if (
234
+ !isUnknownArray(streamValue)
235
+ || streamValue.length !== 4
236
+ || streamValue[0] !== "RelayPrefetchedStreamCache"
237
+ || streamValue[1] !== "next"
238
+ || !isUnknownArray(streamValue[2])
239
+ || streamValue[2].length !== 0
240
+ || !isUnknownArray(streamValue[3])
241
+ || streamValue[3].length !== 2
242
+ || typeof streamValue[3][0] !== "string"
243
+ || !FACEBOOK_GROUP_STREAM_KEY_PATTERN.test(streamValue[3][0])
244
+ ) continue;
245
+ const payload = streamValue[3][1];
246
+ if (
247
+ isRecord(payload)
248
+ && isRecord(payload.__bbox)
249
+ && payload.__bbox.result === value
250
+ ) keys.add(streamValue[3][0]);
251
+ }
252
+ }
253
+ }
254
+ }
255
+ if (keys.size !== 1) {
256
+ throw new Error("Facebook Group result did not bind exactly one reviewed preloader key");
257
+ }
258
+ return [...keys][0] as string;
259
+ }
260
+
261
+ function assertEmptyProviderErrors(value: JsonRecord, label: string): void {
262
+ if (!Object.hasOwn(value, "errors")) return;
263
+ if (!Array.isArray(value.errors)) {
264
+ throw new Error(`${label}.errors must be an array`);
265
+ }
266
+ if (value.errors.length > 0) {
267
+ throw new Error(`${label} contained provider errors`);
268
+ }
269
+ }
270
+
271
+ function assertExpectedGroup(
272
+ value: unknown,
273
+ expectedGroupId: string,
274
+ label: string,
275
+ requireTypename: boolean,
276
+ ): JsonRecord {
277
+ const group = record(value, label);
278
+ if (requireTypename && group.__typename !== "Group") {
279
+ throw new Error(`${label} must be a typed Group`);
280
+ }
281
+ if (
282
+ group.__typename !== undefined
283
+ && group.__typename !== "Group"
284
+ ) throw new Error(`${label} changed its Group typename`);
285
+ if (decimalId(group.id, `${label}.id`) !== expectedGroupId) {
286
+ throw new Error(`${label} changed the requested Group target`);
287
+ }
288
+ return group;
289
+ }
290
+
291
+ function assertTypedGroupRoot(group: JsonRecord, expectedGroupId: string): void {
292
+ if (group.__typename === "Group") {
293
+ assertExpectedGroup(group, expectedGroupId, "Facebook Group feed root", true);
294
+ return;
295
+ }
296
+ if (group.__typename !== undefined) {
297
+ throw new Error("Facebook Group feed root changed its Group typename");
298
+ }
299
+ // The observed Comet group-feed root omits __typename but carries this
300
+ // exact access-gated typed Group record under the same root.
301
+ assertExpectedGroup(
302
+ group.if_viewer_can_see_content,
303
+ expectedGroupId,
304
+ "Facebook Group feed root access marker",
305
+ true,
306
+ );
307
+ }
308
+
309
+ function validatePlaceholder(
310
+ value: unknown,
311
+ index: number,
312
+ expectedGroupId: string,
313
+ ): string | null {
314
+ if (value === null) return null;
315
+ const edge = record(value, `Facebook Group placeholder edge[${index}]`);
316
+ exactKeys(edge, ["cursor", "node"], `Facebook Group placeholder edge[${index}]`);
317
+ const cursor = boundedString(
318
+ edge.cursor,
319
+ `Facebook Group placeholder edge[${index}].cursor`,
320
+ 4_096,
321
+ );
322
+ const node = record(edge.node, `Facebook Group placeholder edge[${index}].node`);
323
+ if (
324
+ node.__typename !== "GroupsSectionHeaderUnit"
325
+ || node.__isFeedUnit !== "GroupsSectionHeaderUnit"
326
+ ) {
327
+ throw new Error(`Facebook Group placeholder edge[${index}] changed its header-unit markers`);
328
+ }
329
+ const target = record(
330
+ node.target_group,
331
+ `Facebook Group placeholder edge[${index}].target_group`,
332
+ );
333
+ if (
334
+ target.__typename !== undefined
335
+ && target.__typename !== "Group"
336
+ ) throw new Error(`Facebook Group placeholder edge[${index}] changed its Group typename`);
337
+ if (
338
+ decimalId(
339
+ target.id,
340
+ `Facebook Group placeholder edge[${index}].target_group.id`,
341
+ ) !== expectedGroupId
342
+ ) throw new Error(`Facebook Group placeholder edge[${index}] changed the requested Group target`);
343
+ return cursor;
344
+ }
345
+
346
+ function rootPlaceholders(
347
+ roots: readonly unknown[],
348
+ expectedGroupId: string,
349
+ ): {
350
+ readonly count: number;
351
+ readonly cursors: readonly string[];
352
+ readonly streamKey: string;
353
+ } {
354
+ const candidates: {
355
+ readonly group: JsonRecord;
356
+ readonly streamKey: string;
357
+ }[] = [];
358
+ walk(roots, (value, path) => {
359
+ const streamKey = reviewedFacebookGroupStreamKey(roots, path, value);
360
+ if (
361
+ isRecord(value)
362
+ && streamKey !== null
363
+ ) {
364
+ assertEmptyProviderErrors(value, "Facebook Group reviewed Relay envelope");
365
+ }
366
+ if (!isRecord(value) || !isRecord(value.data)) return;
367
+ const group = value.data.group;
368
+ if (!isRecord(group) || !isRecord(group.group_feed)) return;
369
+ if (streamKey === null) {
370
+ throw new Error("Facebook Group feed root appeared outside its reviewed Relay envelope");
371
+ }
372
+ assertEmptyProviderErrors(value, "Facebook Group feed envelope");
373
+ candidates.push(Object.freeze({ group, streamKey }));
374
+ });
375
+ if (candidates.length !== 1) {
376
+ throw new Error("Facebook Group response did not contain exactly one group-feed root");
377
+ }
378
+ const candidate = candidates[0];
379
+ if (candidate === undefined) throw new Error("Facebook Group feed root disappeared");
380
+ const { group, streamKey } = candidate;
381
+ if (decimalId(group.id, "Facebook Group feed root.id") !== expectedGroupId) {
382
+ throw new Error("Facebook Group feed root changed the requested Group target");
383
+ }
384
+ assertTypedGroupRoot(group, expectedGroupId);
385
+ const feed = record(group.group_feed, "Facebook Group feed root.group_feed");
386
+ exactKeys(feed, ["edges"], "Facebook Group feed root.group_feed");
387
+ if (!Array.isArray(feed.edges) || feed.edges.length > MAX_PROVIDER_EDGES) {
388
+ throw new Error("Facebook Group placeholder edges must be a bounded array");
389
+ }
390
+ const cursors: string[] = [];
391
+ const seenCursors = new Set<string>();
392
+ for (const [index, edge] of feed.edges.entries()) {
393
+ const cursor = validatePlaceholder(edge, index, expectedGroupId);
394
+ if (cursor === null) continue;
395
+ if (seenCursors.has(cursor)) {
396
+ throw new Error("Facebook Group placeholders contained a duplicate edge cursor");
397
+ }
398
+ seenCursors.add(cursor);
399
+ cursors.push(cursor);
400
+ }
401
+ return {
402
+ count: feed.edges.length,
403
+ cursors: Object.freeze(cursors),
404
+ streamKey,
405
+ };
406
+ }
407
+
408
+ function stream(
409
+ roots: readonly unknown[],
410
+ placeholderCount: number,
411
+ expectedStreamKey: string,
412
+ ): {
413
+ readonly edges: readonly IndexedEdge[];
414
+ readonly hasNextPage: boolean;
415
+ readonly endCursor: string;
416
+ } {
417
+ const indexed = new Map<number, JsonRecord>();
418
+ const pageInfoCandidates: JsonRecord[] = [];
419
+ let finalFragments = 0;
420
+ let streamPhase: "before-root" | "edges" | "final" = "before-root";
421
+
422
+ walk(roots, (value, pathToValue) => {
423
+ const streamKey = reviewedFacebookGroupStreamKey(
424
+ roots,
425
+ pathToValue,
426
+ value,
427
+ );
428
+ if (
429
+ isRecord(value)
430
+ && isRecord(value.data)
431
+ && isRecord(value.data.group)
432
+ && isRecord(value.data.group.group_feed)
433
+ && streamKey !== null
434
+ ) {
435
+ if (streamKey !== expectedStreamKey) {
436
+ throw new Error("Facebook Group stream changed its bound preloader key");
437
+ }
438
+ if (streamPhase !== "before-root") {
439
+ throw new Error("Facebook Group stream emitted its initial root out of order");
440
+ }
441
+ streamPhase = "edges";
442
+ }
443
+ if (!isRecord(value) || !Array.isArray(value.path)) return;
444
+ const path = value.path;
445
+ if (path[0] !== "group" || path[1] !== "group_feed") return;
446
+ if (streamKey === null) {
447
+ throw new Error("Facebook Group stream patch appeared outside its reviewed envelope");
448
+ }
449
+ if (streamKey !== expectedStreamKey) {
450
+ throw new Error("Facebook Group stream changed its bound preloader key");
451
+ }
452
+ if (streamPhase !== "edges") {
453
+ throw new Error("Facebook Group stream patch appeared outside its reviewed order");
454
+ }
455
+ assertEmptyProviderErrors(value, "Facebook Group stream envelope");
456
+ const extensions = record(value.extensions, "Facebook Group stream extensions");
457
+ if (typeof extensions.is_final !== "boolean") {
458
+ throw new Error("Facebook Group stream extensions.is_final must be boolean");
459
+ }
460
+ if (extensions.is_final) finalFragments += 1;
461
+
462
+ if (
463
+ path.length === 4
464
+ && path[2] === "edges"
465
+ && typeof path[3] === "number"
466
+ ) {
467
+ const index = path[3];
468
+ if (
469
+ !Number.isSafeInteger(index)
470
+ || index < placeholderCount
471
+ || index >= MAX_PROVIDER_EDGES
472
+ ) throw new Error("Facebook Group stream used an out-of-bounds edge index");
473
+ if (extensions.is_final) {
474
+ throw new Error("Facebook Group edge fragment unexpectedly ended the stream");
475
+ }
476
+ if (indexed.has(index)) {
477
+ throw new Error("Facebook Group stream contained a duplicate edge index");
478
+ }
479
+ const edge = record(value.data, `Facebook Group streamed edge[${index}]`);
480
+ exactKeys(edge, ["cursor", "node"], `Facebook Group streamed edge[${index}]`);
481
+ indexed.set(index, edge);
482
+ return;
483
+ }
484
+
485
+ if (path.length === 2) {
486
+ if (!extensions.is_final) {
487
+ throw new Error("Facebook Group page-info fragment was not final");
488
+ }
489
+ streamPhase = "final";
490
+ const data = record(value.data, "Facebook Group final stream data");
491
+ exactKeys(data, ["page_info"], "Facebook Group final stream data");
492
+ pageInfoCandidates.push(
493
+ record(data.page_info, "Facebook Group final page_info"),
494
+ );
495
+ return;
496
+ }
497
+
498
+ throw new Error("Facebook Group stream used an unsupported group-feed path");
499
+ });
500
+
501
+ if (
502
+ pageInfoCandidates.length !== 1
503
+ || finalFragments !== 1
504
+ ) {
505
+ throw new Error("Facebook Group response did not contain exactly one final page-info fragment");
506
+ }
507
+ const edges = [...indexed.entries()]
508
+ .sort(([left], [right]) => left - right)
509
+ .map(([index, edge]) => Object.freeze({ index, edge }));
510
+ if (edges.some((edge, index) => edge.index !== placeholderCount + index)) {
511
+ throw new Error("Facebook Group stream edge indices were not contiguous");
512
+ }
513
+
514
+ const pageInfo = pageInfoCandidates[0];
515
+ if (pageInfo === undefined) throw new Error("Facebook Group final page_info disappeared");
516
+ exactKeys(
517
+ pageInfo,
518
+ ["end_cursor", "has_next_page"],
519
+ "Facebook Group final page_info",
520
+ );
521
+ return {
522
+ edges: Object.freeze(edges),
523
+ hasNextPage: exactBoolean(
524
+ pageInfo.has_next_page,
525
+ "Facebook Group page_info.has_next_page",
526
+ ),
527
+ endCursor: boundedString(
528
+ pageInfo.end_cursor,
529
+ "Facebook Group page_info.end_cursor",
530
+ 4_096,
531
+ ),
532
+ };
533
+ }
534
+
535
+ function actors(
536
+ value: unknown,
537
+ label: string,
538
+ ): readonly FacebookGroupFeedPost["actors"][number][] {
539
+ if (value === undefined || value === null) return Object.freeze([]);
540
+ if (!Array.isArray(value) || value.length > MAX_ACTORS) {
541
+ throw new Error(`${label} must be a bounded actor array`);
542
+ }
543
+ const seen = new Set<string>();
544
+ const projected = value.map((actorValue, index) => {
545
+ const actor = record(actorValue, `${label}[${index}]`);
546
+ const id = actor.id === undefined || actor.id === null
547
+ ? null
548
+ : decimalId(actor.id, `${label}[${index}].id`);
549
+ const name = optionalString(actor.name, `${label}[${index}].name`, 512);
550
+ if (id === null && name === null) {
551
+ throw new Error(`${label}[${index}] omitted both reviewed actor fields`);
552
+ }
553
+ if (id !== null && seen.has(id)) {
554
+ throw new Error(`${label} contained a duplicate actor ID`);
555
+ }
556
+ if (id !== null) seen.add(id);
557
+ return Object.freeze({ id, name });
558
+ });
559
+ return Object.freeze(projected);
560
+ }
561
+
562
+ function sameActors(
563
+ left: readonly FacebookGroupFeedPost["actors"][number][],
564
+ right: readonly FacebookGroupFeedPost["actors"][number][],
565
+ ): boolean {
566
+ return left.length === right.length
567
+ && left.every((actor, index) => {
568
+ const candidate = right[index];
569
+ return candidate !== undefined
570
+ && actor.id === candidate.id
571
+ && actor.name === candidate.name;
572
+ });
573
+ }
574
+
575
+ function projectPost(
576
+ edge: IndexedEdge,
577
+ expectedGroupId: string,
578
+ ): FacebookGroupFeedPost {
579
+ const label = `Facebook Group streamed edge[${edge.index}]`;
580
+ boundedString(edge.edge.cursor, `${label}.cursor`, 4_096);
581
+ const node = record(edge.edge.node, `${label}.node`);
582
+ if (node.__typename !== "Story" || node.__isFeedUnit !== "Story") {
583
+ throw new Error(`${label}.node changed its Story markers`);
584
+ }
585
+ const id = decimalId(node.post_id, `${label}.node.post_id`);
586
+ const nodeId = boundedString(node.id, `${label}.node.id`, 256);
587
+ assertExpectedGroup(
588
+ node.to,
589
+ expectedGroupId,
590
+ `${label}.node.to`,
591
+ true,
592
+ );
593
+ const creationTime = boundedInteger(
594
+ node.creation_time,
595
+ `${label}.node.creation_time`,
596
+ );
597
+ const outerActors = actors(node.actors, `${label}.node.actors`);
598
+
599
+ const sections = record(node.comet_sections, `${label}.node.comet_sections`);
600
+ const content = record(sections.content, `${label}.node.comet_sections.content`);
601
+ const story = record(content.story, `${label}.node.comet_sections.content.story`);
602
+ if (
603
+ boundedString(story.id, `${label}.story.id`, 256) !== nodeId
604
+ || decimalId(story.post_id, `${label}.story.post_id`) !== id
605
+ ) throw new Error(`${label} changed its nested Story identity`);
606
+ const target = record(story.target_group, `${label}.story.target_group`);
607
+ if (
608
+ target.__typename !== undefined
609
+ && target.__typename !== "Group"
610
+ ) throw new Error(`${label}.story.target_group changed its Group typename`);
611
+ if (decimalId(target.id, `${label}.story.target_group.id`) !== expectedGroupId) {
612
+ throw new Error(`${label}.story changed the requested Group target`);
613
+ }
614
+ const innerActors = actors(story.actors, `${label}.story.actors`);
615
+ if (
616
+ outerActors.length > 0
617
+ && innerActors.length > 0
618
+ && !sameActors(outerActors, innerActors)
619
+ ) throw new Error(`${label} changed its nested Story actors`);
620
+
621
+ const messageRecord = story.message === undefined || story.message === null
622
+ ? null
623
+ : record(story.message, `${label}.story.message`);
624
+ const message = messageRecord === null
625
+ ? null
626
+ : optionalString(messageRecord.text, `${label}.story.message.text`, 20_000);
627
+ return Object.freeze({
628
+ id,
629
+ creation_time: creationTime,
630
+ message,
631
+ actors: outerActors.length > 0 ? outerActors : innerActors,
632
+ });
633
+ }
634
+
635
+ export function normalizeFacebookGroupFeedHtml(
636
+ html: unknown,
637
+ expectedViewerId: string,
638
+ expectedGroupId: string,
639
+ limit: number,
640
+ ): FacebookGroupFeed {
641
+ const viewerId = decimalId(expectedViewerId, "expected Facebook viewer ID");
642
+ const groupId = decimalId(expectedGroupId, "expected Facebook Group ID");
643
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 30) {
644
+ throw new Error("Facebook Group feed limit must be an integer between 1 and 30");
645
+ }
646
+ const roots = parseMetaJsonScripts(html);
647
+ if (parseFacebookViewerId(html) !== viewerId) {
648
+ throw new Error("Facebook Group feed response changed its bound viewer");
649
+ }
650
+ assertMetaCometReadActor(roots, viewerId);
651
+ const placeholders = rootPlaceholders(roots, groupId);
652
+ const providerPage = stream(
653
+ roots,
654
+ placeholders.count,
655
+ placeholders.streamKey,
656
+ );
657
+ const seenPostIds = new Set<string>();
658
+ const seenCursors = new Set(placeholders.cursors);
659
+ const posts = providerPage.edges.map((edge) => {
660
+ const cursor = boundedString(
661
+ edge.edge.cursor,
662
+ `Facebook Group streamed edge[${edge.index}].cursor`,
663
+ 4_096,
664
+ );
665
+ if (seenCursors.has(cursor)) {
666
+ throw new Error("Facebook Group feed contained a duplicate edge cursor");
667
+ }
668
+ seenCursors.add(cursor);
669
+ const post = projectPost(edge, groupId);
670
+ if (seenPostIds.has(post.id)) {
671
+ throw new Error("Facebook Group feed contained a duplicate post ID");
672
+ }
673
+ seenPostIds.add(post.id);
674
+ return post;
675
+ });
676
+ const truncated = posts.length > limit;
677
+ return Object.freeze({
678
+ feed: "group",
679
+ group_id: groupId,
680
+ posts: Object.freeze(posts.slice(0, limit)),
681
+ provider_has_next_page: providerPage.hasNextPage,
682
+ // This reviewed contract is first-page-only. The provider cursor proves
683
+ // completeness of the assembled page but is not an executable request
684
+ // capability and must never escape as one.
685
+ next_cursor: null,
686
+ continuation_supported: false,
687
+ truncated,
688
+ complete: !providerPage.hasNextPage && !truncated,
689
+ });
690
+ }