@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,1242 @@
1
+ import { MAX_HAR_BYTES, MAX_HAR_ENTRIES } from "./har";
2
+ import { xWebQueryDescriptorEvidenceSnapshot } from "./providers/x-web";
3
+ import { readRegularFile, type PrivateDirectoryIdentity } from "./storage";
4
+
5
+ type JsonRecord = Record<string, unknown>;
6
+
7
+ export type InternalHarCandidate = {
8
+ readonly method: string;
9
+ readonly origin: string;
10
+ readonly path: string;
11
+ readonly operationType: "query" | "mutation" | "unknown";
12
+ readonly sampleCount: number;
13
+ readonly statuses: readonly number[];
14
+ readonly queryNames: readonly string[];
15
+ readonly headerNames: readonly string[];
16
+ readonly requestFieldPaths: readonly string[];
17
+ readonly responseFieldPaths: readonly string[];
18
+ /** Non-secret registered-operation revisions safe to bind into a contract. */
19
+ readonly revisions: readonly string[];
20
+ readonly reviewRequired: true;
21
+ };
22
+
23
+ export type InternalHarEvidence = {
24
+ readonly schemaVersion: 1;
25
+ readonly adapterId: string;
26
+ readonly targetOrigin: string;
27
+ readonly analyzedAt: string;
28
+ readonly observedEntries: number;
29
+ readonly candidates: readonly InternalHarCandidate[];
30
+ readonly warnings: readonly string[];
31
+ };
32
+
33
+ const isRecord = (value: unknown): value is JsonRecord =>
34
+ typeof value === "object" && value !== null && !Array.isArray(value);
35
+
36
+ export const INTERNAL_HAR_REVIEW_BOUNDS = Object.freeze({
37
+ maxArrayItems: 100,
38
+ maxFieldNameCharacters: 4_096,
39
+ maxFieldPaths: 1_000,
40
+ maxFormParameters: 1_000,
41
+ maxHeaderItems: 1_000,
42
+ maxHeaderNames: 200,
43
+ maxCandidates: 2_000,
44
+ maxJsonTextBytes: 2 * 1024 * 1024,
45
+ maxObjectEntries: 300,
46
+ maxPathCharacters: 4_096,
47
+ maxPathSegments: 128,
48
+ maxQueryCharacters: 2 * 1024 * 1024,
49
+ maxQueryIdCharacters: 256,
50
+ maxQueryParameters: 1_000,
51
+ maxRawStringCharacters: 2 * 1024 * 1024,
52
+ maxRevisions: 1_000,
53
+ maxTotalFieldPaths: 20_000,
54
+ maxTotalRevisions: 20_000,
55
+ maxTraversalDepth: 8,
56
+ maxLinkedInArticleTraversalDepth: 12,
57
+ maxUrlCharacters: 4_096 + (2 * 1024 * 1024) + 8_192,
58
+ } as const);
59
+
60
+ const sensitiveName = /(?:authorization|cookie|token|secret|password|passwd|session|credential|private|signature)/iu;
61
+ const safeFieldName = /^[A-Za-z_$][A-Za-z0-9_$.-]{0,127}$/u;
62
+ const safeGraphQlVariableFieldName = /^[_A-Za-z][_0-9A-Za-z]{0,127}$/u;
63
+ const safeHeaderName = /^[a-z0-9!#$%&'*+.^_`|~-]{1,128}$/u;
64
+
65
+ const reviewedHeaderNames: ReadonlySet<string> = new Set([
66
+ "accept",
67
+ "accept-language",
68
+ "authorization",
69
+ "content-type",
70
+ "cookie",
71
+ "csrf-token",
72
+ "origin",
73
+ "referer",
74
+ "user-agent",
75
+ "x-client-transaction-id",
76
+ "x-csrf-token",
77
+ "x-li-lang",
78
+ "x-restli-id",
79
+ "x-restli-method",
80
+ "x-restli-protocol-version",
81
+ "x-twitter-active-user",
82
+ "x-twitter-auth-type",
83
+ "x-twitter-client-language",
84
+ ] as const);
85
+
86
+ const reviewedTwitchGraphQlHeaderNames: ReadonlySet<string> = new Set([
87
+ "client-id",
88
+ "client-integrity",
89
+ ] as const);
90
+
91
+ /**
92
+ * HAR object keys are untrusted content too. In particular, normalized API
93
+ * responses commonly use account IDs, URNs, names, and slugs as map keys. A
94
+ * lexical identifier check cannot distinguish those values from schema field
95
+ * names, so only this small reviewed vocabulary may survive as a literal.
96
+ * Unknown keys retain their structural position as `:dynamic`.
97
+ */
98
+ const reviewedStructuralFieldNames: ReadonlySet<string> = new Set([
99
+ "body",
100
+ "byId",
101
+ "category",
102
+ "conversation_id",
103
+ "conversationUrn",
104
+ "conversations",
105
+ "count",
106
+ "cursor",
107
+ "data",
108
+ "edges",
109
+ "elements",
110
+ "encoded_event",
111
+ "entityUrn",
112
+ "entries",
113
+ "errors",
114
+ "extensions",
115
+ "features",
116
+ "fieldToggles",
117
+ "hasNextPage",
118
+ "hasPreviousPage",
119
+ "id",
120
+ "inbox_initial_state",
121
+ "items",
122
+ "limit",
123
+ "media",
124
+ "message",
125
+ "messages",
126
+ "messengerConversations",
127
+ "messengerMessages",
128
+ "metadata",
129
+ "name",
130
+ "nextCursor",
131
+ "nodes",
132
+ "operationName",
133
+ "pageInfo",
134
+ "paging",
135
+ "payload",
136
+ "previousCursor",
137
+ "public_key",
138
+ "queryId",
139
+ "recipient_ids",
140
+ "response",
141
+ "responsive_web_graphql_enabled",
142
+ "responsive_web_graphql_exclude_directive_enabled",
143
+ "result",
144
+ "results",
145
+ "screen_name",
146
+ "start",
147
+ "subject",
148
+ "text",
149
+ "total",
150
+ "variables",
151
+ "users",
152
+ "withAuxiliaryUserLabels",
153
+ ] as const);
154
+
155
+ /** Exact schema vocabulary reviewed only for Twitch's first-party GraphQL route. */
156
+ const reviewedTwitchGraphQlFieldNames: ReadonlySet<string> = new Set([
157
+ "channelLogin",
158
+ "currentUser",
159
+ "displayName",
160
+ "extensions",
161
+ "followers",
162
+ "id",
163
+ "login",
164
+ "operationName",
165
+ "persistedQuery",
166
+ "query",
167
+ "requestInfo",
168
+ "sha256Hash",
169
+ "skipSchedule",
170
+ "totalCount",
171
+ "user",
172
+ "variables",
173
+ "version",
174
+ ] as const);
175
+
176
+ const reviewedTwitchGraphQlOperationNames: ReadonlySet<string> = new Set([
177
+ "ChannelRoot_AboutPanel",
178
+ "TopNav_CurrentUser",
179
+ ] as const);
180
+
181
+ /** Exact schema vocabulary reviewed only for LinkedIn's native Article route. */
182
+ const reviewedLinkedInArticleFieldNames: ReadonlySet<string> = new Set([
183
+ "$set",
184
+ "$type",
185
+ "activityUrn",
186
+ "article",
187
+ "articleType",
188
+ "articleUrn",
189
+ "attributesV2",
190
+ "author",
191
+ "authors",
192
+ "content",
193
+ "contentHtml",
194
+ "caption",
195
+ "coverImage",
196
+ "coverMedia",
197
+ "coverMediaV2Union",
198
+ "createdAt",
199
+ "detailDataUnion",
200
+ "entity",
201
+ "fileSize",
202
+ "filename",
203
+ "firstPartyArticle",
204
+ "firstPartyArticleUrn",
205
+ "hyperlink",
206
+ "included",
207
+ "length",
208
+ "linkedInArticleUrn",
209
+ "mediaUploadType",
210
+ "originalImage",
211
+ "originalImageUrn",
212
+ "patch",
213
+ "permalink",
214
+ "profileUrn",
215
+ "publishedAt",
216
+ "q",
217
+ "state",
218
+ "textBlock",
219
+ "title",
220
+ "type",
221
+ "ugcPostUrn",
222
+ "updatedAt",
223
+ "version",
224
+ ] as const);
225
+
226
+ const reviewedPathSegments: ReadonlySet<string> = new Set([
227
+ "2",
228
+ "api",
229
+ "append",
230
+ "articles",
231
+ "chat",
232
+ "comments",
233
+ "conversation",
234
+ "conversations",
235
+ "events",
236
+ "feed",
237
+ "feeds",
238
+ "finalize",
239
+ "graphql",
240
+ "i",
241
+ "inbox",
242
+ "initialize",
243
+ "low_quality",
244
+ "media",
245
+ "message",
246
+ "messages",
247
+ "messaging",
248
+ "notifications",
249
+ "public_keys",
250
+ "publishing",
251
+ "reactions",
252
+ "replies",
253
+ "rest",
254
+ "timeline",
255
+ "trusted",
256
+ "untrusted",
257
+ "upload",
258
+ "users",
259
+ "voyager",
260
+ "voyagerPublishingDashFirstPartyArticles",
261
+ ] as const);
262
+
263
+ /** Fields whose object value is a provider-normalized map keyed by IDs/URNs. */
264
+ const reviewedDynamicMapFieldNames: ReadonlySet<string> = new Set([
265
+ "byId",
266
+ "conversations",
267
+ "entries",
268
+ "messages",
269
+ "objects",
270
+ "tweets",
271
+ "users",
272
+ ] as const);
273
+
274
+ /**
275
+ * Structural capture hints only. They let the sanitizer retain known
276
+ * registered-operation names and revisions from a private HAR; they are not
277
+ * request rules and cannot authorize dispatch. Observed LinkedIn web reads
278
+ * still bind their executable request rules in the operation registry.
279
+ */
280
+ const reviewedLinkedInRegisteredQueries: ReadonlyMap<string, ReadonlySet<string>> = new Map([
281
+ ["/voyager/api/graphql", new Set([
282
+ "voyagerFeedDashMainFeed",
283
+ "voyagerFeedDashProfileUpdates",
284
+ ])],
285
+ ["/voyager/api/voyagerMessagingGraphQL/graphql", new Set([
286
+ "messengerConversations",
287
+ "messengerMailboxCounts",
288
+ "messengerMessages",
289
+ ])],
290
+ ]);
291
+
292
+ const reviewedXGraphQlDescriptors: ReadonlyMap<
293
+ string,
294
+ { readonly operationType: "query" | "mutation"; readonly revision: string }
295
+ > = new Map(
296
+ xWebQueryDescriptorEvidenceSnapshot.descriptors.map((descriptor) => [
297
+ descriptor.operationName,
298
+ { operationType: descriptor.operationType, revision: descriptor.queryId },
299
+ ]),
300
+ );
301
+
302
+ const reviewedMetaRequestFieldNames: ReadonlySet<string> = new Set([
303
+ "variables",
304
+ "doc_id",
305
+ "fb_api_req_friendly_name",
306
+ "fb_api_caller_class",
307
+ "av",
308
+ "__user",
309
+ "__a",
310
+ "__req",
311
+ "__hs",
312
+ "dpr",
313
+ "__ccg",
314
+ "__rev",
315
+ "__s",
316
+ "__hsi",
317
+ "__dyn",
318
+ "__csr",
319
+ "__comet_req",
320
+ "fb_dtsg",
321
+ "jazoest",
322
+ "lsd",
323
+ "server_timestamps",
324
+ ] as const);
325
+
326
+ function isExactFacebookOrigin(url: URL): boolean {
327
+ return url.origin === "https://www.facebook.com";
328
+ }
329
+
330
+ export function isReviewedTwitchGraphQlRoute(url: URL): boolean {
331
+ return url.origin === "https://gql.twitch.tv" && url.pathname === "/gql";
332
+ }
333
+
334
+ function isTwitchPublicAccountFieldName(value: string): boolean {
335
+ return value === "channelLogin" || value === "login";
336
+ }
337
+
338
+ function isTwitchCredentialLikeFieldName(value: string): boolean {
339
+ if (isTwitchPublicAccountFieldName(value)) return false;
340
+ const compact = value.replaceAll(/[^A-Za-z0-9]/gu, "").toLowerCase();
341
+ return /(?:auth|authorization|cookie|credential|password|passwd|private|secret|session|signature|token)/u.test(compact);
342
+ }
343
+
344
+ function isReviewedTwitchStructuralContainer(value: string): boolean {
345
+ return !isTwitchCredentialLikeFieldName(value);
346
+ }
347
+
348
+ export function isReviewedLinkedInArticleRoute(url: URL): boolean {
349
+ if (url.origin !== "https://www.linkedin.com") return false;
350
+ const segments = url.pathname.split("/").filter(Boolean);
351
+ if (
352
+ segments[0] !== "voyager"
353
+ || segments[1] !== "api"
354
+ || segments[2] !== "voyagerPublishingDashFirstPartyArticles"
355
+ || (segments.length !== 3 && segments.length !== 4)
356
+ ) return false;
357
+ if (segments.length === 3) return true;
358
+ try {
359
+ return /^urn:li:fsd_firstPartyArticle:[0-9]{1,32}$/u.test(decodeURIComponent(segments[3] ?? ""));
360
+ } catch {
361
+ return false;
362
+ }
363
+ }
364
+
365
+ export function reviewedInternalFieldNameForUrl(url: URL, value: string): string {
366
+ if (
367
+ isReviewedTwitchGraphQlRoute(url)
368
+ && value.length <= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldNameCharacters
369
+ && safeFieldName.test(value)
370
+ && !isTwitchCredentialLikeFieldName(value)
371
+ && reviewedTwitchGraphQlFieldNames.has(value)
372
+ ) return value;
373
+ if (
374
+ isReviewedLinkedInArticleRoute(url)
375
+ && value.length <= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldNameCharacters
376
+ && safeFieldName.test(value)
377
+ && !sensitiveName.test(value)
378
+ && reviewedLinkedInArticleFieldNames.has(value)
379
+ ) return value;
380
+ return reviewedInternalFieldName(value);
381
+ }
382
+
383
+ function reviewedMetaRequestFieldName(url: URL, value: string): string {
384
+ return isExactFacebookOrigin(url)
385
+ && value.length <= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldNameCharacters
386
+ && safeFieldName.test(value)
387
+ && reviewedMetaRequestFieldNames.has(value)
388
+ ? value
389
+ : reviewedInternalFieldNameForUrl(url, value);
390
+ }
391
+
392
+ function isReviewedMetaStructuralContainer(value: string): boolean {
393
+ return !reviewedMetaRequestFieldNames.has(value) || value === "variables";
394
+ }
395
+
396
+ export function reviewedInternalFieldName(value: string): string {
397
+ return value.length <= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldNameCharacters
398
+ && safeFieldName.test(value)
399
+ && !sensitiveName.test(value)
400
+ && reviewedStructuralFieldNames.has(value)
401
+ ? value
402
+ : ":dynamic";
403
+ }
404
+
405
+ /**
406
+ * GraphQL top-level variable names are operation schema identifiers rather
407
+ * than caller values. Private review may retain one only for an exact
408
+ * first-party X GraphQL request; values and all nested unknown map keys remain
409
+ * redacted by the ordinary structural reviewer.
410
+ */
411
+ export function reviewedXGraphQlVariableFieldName(value: string): string {
412
+ return value.length <= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldNameCharacters
413
+ && safeGraphQlVariableFieldName.test(value)
414
+ && !sensitiveName.test(value)
415
+ ? value
416
+ : ":dynamic";
417
+ }
418
+
419
+ export function isReviewedInternalDynamicMapField(value: string): boolean {
420
+ return reviewedDynamicMapFieldNames.has(value);
421
+ }
422
+
423
+ function parseRoot(value: unknown): readonly unknown[] {
424
+ if (!isRecord(value) || !isRecord(value.log) || !Array.isArray(value.log.entries)) {
425
+ throw new Error("HAR must contain log.entries[]");
426
+ }
427
+ if (value.log.entries.length > MAX_HAR_ENTRIES) throw new Error(`HAR contains more than ${MAX_HAR_ENTRIES} entries`);
428
+ return value.log.entries;
429
+ }
430
+
431
+ export type BoundedInternalHarUrl = {
432
+ readonly url: URL;
433
+ readonly pathTruncated: boolean;
434
+ readonly queryTruncated: boolean;
435
+ };
436
+
437
+ function boundedRawQuery(value: string): { readonly value: string; readonly truncated: boolean } {
438
+ if (value.length > INTERNAL_HAR_REVIEW_BOUNDS.maxQueryCharacters) {
439
+ return { value: "", truncated: true };
440
+ }
441
+ let separators = 0;
442
+ for (let index = 0; index < value.length; index += 1) {
443
+ if (value[index] !== "&") continue;
444
+ separators += 1;
445
+ if (separators === INTERNAL_HAR_REVIEW_BOUNDS.maxQueryParameters) {
446
+ return { value: value.slice(0, index), truncated: true };
447
+ }
448
+ }
449
+ return { value, truncated: false };
450
+ }
451
+
452
+ function hasTooManyPathSegments(value: string): boolean {
453
+ let segments = 0;
454
+ let insideSegment = false;
455
+ for (const character of value) {
456
+ if (character === "/") {
457
+ insideSegment = false;
458
+ continue;
459
+ }
460
+ if (insideSegment) continue;
461
+ insideSegment = true;
462
+ segments += 1;
463
+ if (segments > INTERNAL_HAR_REVIEW_BOUNDS.maxPathSegments) return true;
464
+ }
465
+ return false;
466
+ }
467
+
468
+ /**
469
+ * Bound the raw URL before asking the platform URL parser to normalize or
470
+ * decode it. Oversized path and query components are replaced with inert
471
+ * placeholders, while a query with too many parameters retains only its
472
+ * bounded prefix.
473
+ */
474
+ export function parseBoundedInternalHarUrl(value: string): BoundedInternalHarUrl | null {
475
+ if (value.length > INTERNAL_HAR_REVIEW_BOUNDS.maxUrlCharacters) return null;
476
+ const fragmentIndex = value.indexOf("#");
477
+ const withoutFragment = fragmentIndex === -1 ? value : value.slice(0, fragmentIndex);
478
+ const queryIndex = withoutFragment.indexOf("?");
479
+ const withoutQuery = queryIndex === -1 ? withoutFragment : withoutFragment.slice(0, queryIndex);
480
+ const rawQuery = queryIndex === -1 ? "" : withoutFragment.slice(queryIndex + 1);
481
+ const schemeIndex = withoutQuery.indexOf("://");
482
+ const pathStart = schemeIndex === -1 ? -1 : withoutQuery.indexOf("/", schemeIndex + 3);
483
+ const rawPath = pathStart === -1 ? "" : withoutQuery.slice(pathStart);
484
+ const pathTruncated = rawPath.length > INTERNAL_HAR_REVIEW_BOUNDS.maxPathCharacters
485
+ || hasTooManyPathSegments(rawPath);
486
+ const boundedQuery = boundedRawQuery(rawQuery);
487
+ const boundedBase = pathTruncated && pathStart !== -1
488
+ ? `${withoutQuery.slice(0, pathStart)}/:oversized-path`
489
+ : withoutQuery;
490
+ const boundedValue = boundedQuery.value === "" ? boundedBase : `${boundedBase}?${boundedQuery.value}`;
491
+ try {
492
+ return {
493
+ url: new URL(boundedValue),
494
+ pathTruncated,
495
+ queryTruncated: boundedQuery.truncated,
496
+ };
497
+ } catch {
498
+ return null;
499
+ }
500
+ }
501
+
502
+ function reviewedXGraphQlRoute(url: URL): {
503
+ readonly revision: string;
504
+ readonly operation: string;
505
+ readonly reviewed: boolean;
506
+ readonly operationType: "query" | "mutation" | "unknown";
507
+ } | null {
508
+ if (url.origin !== "https://x.com") return null;
509
+ const parts = url.pathname.split("/").filter(Boolean);
510
+ if (parts.length !== 5 || parts[0] !== "i" || parts[1] !== "api" || parts[2] !== "graphql") return null;
511
+ const revision = parts[3];
512
+ const operation = parts[4];
513
+ if (revision === undefined || operation === undefined
514
+ || !/^[A-Za-z0-9_-]{20,64}$/u.test(revision)
515
+ || !/^[A-Z][A-Za-z0-9_]{2,100}$/u.test(operation)
516
+ || url.pathname !== `/i/api/graphql/${revision}/${operation}`) return null;
517
+ const descriptor = reviewedXGraphQlDescriptors.get(operation);
518
+ return {
519
+ revision,
520
+ operation,
521
+ reviewed: descriptor?.revision === revision,
522
+ operationType: descriptor?.revision === revision ? descriptor.operationType : "unknown",
523
+ };
524
+ }
525
+
526
+ function safePath(url: URL): string {
527
+ const { pathname } = url;
528
+ if (
529
+ pathname.length > INTERNAL_HAR_REVIEW_BOUNDS.maxPathCharacters
530
+ || hasTooManyPathSegments(pathname)
531
+ ) return "/:oversized-path";
532
+ if (
533
+ isExactFacebookOrigin(url)
534
+ && (
535
+ pathname === "/api/graphql/"
536
+ || pathname === "/ajax/qm/"
537
+ || pathname === "/data/manifest/"
538
+ )
539
+ ) return pathname;
540
+ if (isReviewedTwitchGraphQlRoute(url)) return "/gql";
541
+ const xGraphQl = reviewedXGraphQlRoute(url);
542
+ if (xGraphQl !== null) {
543
+ return xGraphQl.reviewed
544
+ ? `/i/api/graphql/:revision/${xGraphQl.operation}`
545
+ : "/i/api/graphql/:revision/:operation";
546
+ }
547
+ if (url.origin === "https://www.linkedin.com" && reviewedLinkedInRegisteredQueries.has(pathname)) {
548
+ return pathname;
549
+ }
550
+ const rawSegments = pathname.split("/");
551
+ let parameter = 0;
552
+ return rawSegments.map((raw) => {
553
+ let segment: string;
554
+ try {
555
+ segment = decodeURIComponent(raw);
556
+ } catch {
557
+ parameter += 1;
558
+ return `:segment${parameter}`;
559
+ }
560
+ if (segment === "") return "";
561
+ if (reviewedPathSegments.has(segment)) return segment;
562
+ parameter += 1;
563
+ return `:segment${parameter}`;
564
+ }).join("/");
565
+ }
566
+
567
+ function headerNames(value: unknown, url: URL): readonly string[] {
568
+ if (!Array.isArray(value)) return [];
569
+ const output = new Set<string>();
570
+ const count = Math.min(value.length, INTERNAL_HAR_REVIEW_BOUNDS.maxHeaderItems);
571
+ for (let index = 0; index < count; index += 1) {
572
+ const candidate: unknown = value[index];
573
+ if (!isRecord(candidate) || typeof candidate.name !== "string") continue;
574
+ if (candidate.name.length > INTERNAL_HAR_REVIEW_BOUNDS.maxFieldNameCharacters) continue;
575
+ const name = candidate.name.toLowerCase();
576
+ if (!safeHeaderName.test(name)) continue;
577
+ output.add(
578
+ reviewedHeaderNames.has(name)
579
+ || (isReviewedTwitchGraphQlRoute(url) && reviewedTwitchGraphQlHeaderNames.has(name))
580
+ ? name
581
+ : ":dynamic",
582
+ );
583
+ if (output.size >= INTERNAL_HAR_REVIEW_BOUNDS.maxHeaderNames) break;
584
+ }
585
+ return [...output].sort();
586
+ }
587
+
588
+ function addFieldPaths(
589
+ value: unknown,
590
+ output: Set<string>,
591
+ prefix = "",
592
+ depth = 0,
593
+ redactObjectKeys = false,
594
+ fieldNameReviewer: (value: string) => string = reviewedInternalFieldName,
595
+ shouldTraverseField: (value: string) => boolean = () => true,
596
+ maximumDepth: number = INTERNAL_HAR_REVIEW_BOUNDS.maxTraversalDepth,
597
+ ): void {
598
+ if (
599
+ depth >= maximumDepth
600
+ || output.size >= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldPaths
601
+ ) return;
602
+ if (Array.isArray(value)) {
603
+ const count = Math.min(value.length, INTERNAL_HAR_REVIEW_BOUNDS.maxArrayItems);
604
+ for (let index = 0; index < count; index += 1) {
605
+ addFieldPaths(
606
+ value[index],
607
+ output,
608
+ `${prefix}[]`,
609
+ depth + 1,
610
+ false,
611
+ fieldNameReviewer,
612
+ shouldTraverseField,
613
+ maximumDepth,
614
+ );
615
+ if (output.size >= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldPaths) return;
616
+ }
617
+ return;
618
+ }
619
+ if (!isRecord(value)) return;
620
+ let inspected = 0;
621
+ for (const key in value) {
622
+ if (!Object.prototype.hasOwnProperty.call(value, key)) continue;
623
+ if (inspected >= INTERNAL_HAR_REVIEW_BOUNDS.maxObjectEntries) return;
624
+ inspected += 1;
625
+ const child = value[key];
626
+ const renderedKey = redactObjectKeys ? ":dynamic" : fieldNameReviewer(key);
627
+ const path = prefix === "" ? renderedKey : `${prefix}.${renderedKey}`;
628
+ output.add(path);
629
+ if (!redactObjectKeys && !shouldTraverseField(key)) continue;
630
+ addFieldPaths(
631
+ child,
632
+ output,
633
+ path,
634
+ depth + 1,
635
+ !redactObjectKeys && isRecord(child) && reviewedDynamicMapFieldNames.has(key),
636
+ fieldNameReviewer,
637
+ shouldTraverseField,
638
+ maximumDepth,
639
+ );
640
+ if (output.size >= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldPaths) return;
641
+ }
642
+ }
643
+
644
+ function parsedJson(value: unknown): unknown {
645
+ if (
646
+ typeof value !== "string"
647
+ || value.length > INTERNAL_HAR_REVIEW_BOUNDS.maxRawStringCharacters
648
+ || Buffer.byteLength(value, "utf8") > INTERNAL_HAR_REVIEW_BOUNDS.maxJsonTextBytes
649
+ ) return null;
650
+ try {
651
+ return JSON.parse(value) as unknown;
652
+ } catch {
653
+ return null;
654
+ }
655
+ }
656
+
657
+ type BoundedFormParameters = {
658
+ readonly parameters: readonly JsonRecord[];
659
+ readonly truncated: boolean;
660
+ };
661
+
662
+ function boundedFormParameters(postData: JsonRecord): BoundedFormParameters | null {
663
+ if (Array.isArray(postData.params)) {
664
+ return {
665
+ parameters: postData.params
666
+ .slice(0, INTERNAL_HAR_REVIEW_BOUNDS.maxFormParameters)
667
+ .filter(isRecord),
668
+ truncated: postData.params.length > INTERNAL_HAR_REVIEW_BOUNDS.maxFormParameters,
669
+ };
670
+ }
671
+ const rawMime = postData.mimeType;
672
+ const mime = typeof rawMime === "string"
673
+ && rawMime.length <= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldNameCharacters
674
+ ? rawMime.toLowerCase()
675
+ : "";
676
+ const text = postData.text;
677
+ if (
678
+ !mime.includes("application/x-www-form-urlencoded")
679
+ || typeof text !== "string"
680
+ || text.length > INTERNAL_HAR_REVIEW_BOUNDS.maxRawStringCharacters
681
+ || Buffer.byteLength(text, "utf8") > INTERNAL_HAR_REVIEW_BOUNDS.maxJsonTextBytes
682
+ ) return null;
683
+ const output: JsonRecord[] = [];
684
+ for (const [name, value] of new URLSearchParams(text)) {
685
+ if (output.length >= INTERNAL_HAR_REVIEW_BOUNDS.maxFormParameters) {
686
+ return { parameters: output, truncated: true };
687
+ }
688
+ output.push({ name, value });
689
+ }
690
+ return { parameters: output, truncated: false };
691
+ }
692
+
693
+ function requestFields(request: JsonRecord, url: URL): readonly string[] {
694
+ if (!isRecord(request.postData)) return [];
695
+ let value: unknown = null;
696
+ const rawMime = request.postData.mimeType;
697
+ const mime = typeof rawMime === "string"
698
+ && rawMime.length <= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldNameCharacters
699
+ ? rawMime.toLowerCase()
700
+ : "";
701
+ const twitchTextPlainJson = request.method === "POST"
702
+ && isReviewedTwitchGraphQlRoute(url)
703
+ && mime.split(";", 1)[0]?.trim() === "text/plain";
704
+ if (mime.includes("json") || twitchTextPlainJson) value = parsedJson(request.postData.text);
705
+ else {
706
+ const form = boundedFormParameters(request.postData);
707
+ if (form === null) return [];
708
+ const fields = new Set<string>();
709
+ const count = Math.min(
710
+ form.parameters.length,
711
+ INTERNAL_HAR_REVIEW_BOUNDS.maxFormParameters,
712
+ );
713
+ for (let index = 0; index < count; index += 1) {
714
+ const candidate: unknown = form.parameters[index];
715
+ if (!isRecord(candidate) || typeof candidate.name !== "string") continue;
716
+ const renderedName = reviewedMetaRequestFieldName(url, candidate.name);
717
+ fields.add(renderedName);
718
+ if (
719
+ isExactFacebookOrigin(url)
720
+ && candidate.name === "variables"
721
+ && typeof candidate.value === "string"
722
+ ) {
723
+ const parsedVariables = parsedJson(candidate.value);
724
+ if (parsedVariables === null) continue;
725
+ const variableFields = new Set<string>();
726
+ addFieldPaths(
727
+ parsedVariables,
728
+ variableFields,
729
+ "",
730
+ 0,
731
+ false,
732
+ (fieldName) => reviewedMetaRequestFieldName(url, fieldName),
733
+ isReviewedMetaStructuralContainer,
734
+ );
735
+ for (const field of variableFields) fields.add(`variables.${field}`);
736
+ }
737
+ }
738
+ return [...fields].sort();
739
+ }
740
+ const fields = new Set<string>();
741
+ addFieldPaths(
742
+ value,
743
+ fields,
744
+ "",
745
+ 0,
746
+ false,
747
+ (fieldName) => reviewedMetaRequestFieldName(url, fieldName),
748
+ isExactFacebookOrigin(url)
749
+ ? isReviewedMetaStructuralContainer
750
+ : isReviewedTwitchGraphQlRoute(url)
751
+ ? isReviewedTwitchStructuralContainer
752
+ : undefined,
753
+ isReviewedLinkedInArticleRoute(url)
754
+ ? INTERNAL_HAR_REVIEW_BOUNDS.maxLinkedInArticleTraversalDepth
755
+ : INTERNAL_HAR_REVIEW_BOUNDS.maxTraversalDepth,
756
+ );
757
+ const xGraphQl = reviewedXGraphQlRoute(url);
758
+ if (
759
+ request.method === "POST"
760
+ && xGraphQl?.reviewed === true
761
+ && isRecord(value)
762
+ && isRecord(value.variables)
763
+ ) {
764
+ let inspected = 0;
765
+ for (const key in value.variables) {
766
+ if (!Object.prototype.hasOwnProperty.call(value.variables, key)) continue;
767
+ if (inspected >= INTERNAL_HAR_REVIEW_BOUNDS.maxObjectEntries) break;
768
+ inspected += 1;
769
+ fields.add(`variables.${reviewedXGraphQlVariableFieldName(key)}`);
770
+ if (fields.size >= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldPaths) break;
771
+ }
772
+ }
773
+ return [...fields].sort();
774
+ }
775
+
776
+ function queryStructuralFields(url: URL): readonly string[] {
777
+ const output = new Set<string>();
778
+ const fieldNameReviewer = (fieldName: string): string =>
779
+ reviewedMetaRequestFieldName(url, fieldName);
780
+ for (const name of ["variables", "features", "fieldToggles"] as const) {
781
+ const raw = url.searchParams.get(name);
782
+ if (raw === null || raw.length > INTERNAL_HAR_REVIEW_BOUNDS.maxRawStringCharacters) continue;
783
+ const parsed = parsedJson(raw);
784
+ if (parsed !== null) {
785
+ const fields = new Set<string>();
786
+ addFieldPaths(
787
+ parsed,
788
+ fields,
789
+ "",
790
+ 0,
791
+ false,
792
+ fieldNameReviewer,
793
+ isExactFacebookOrigin(url) ? isReviewedMetaStructuralContainer : undefined,
794
+ );
795
+ for (const field of fields) output.add(`query.${name}.${field}`);
796
+ continue;
797
+ }
798
+ if (name !== "variables") continue;
799
+ for (const match of raw.matchAll(/(?:^|[,(])([A-Za-z_$][A-Za-z0-9_$]{0,127}):/gu)) {
800
+ const field = match[1];
801
+ if (field !== undefined) output.add(`query.variables.${fieldNameReviewer(field)}`);
802
+ }
803
+ }
804
+ return [...output].sort();
805
+ }
806
+
807
+ function parsedMetaGraphQlDocuments(value: string): readonly unknown[] {
808
+ if (
809
+ value.length > INTERNAL_HAR_REVIEW_BOUNDS.maxRawStringCharacters
810
+ || Buffer.byteLength(value, "utf8") > INTERNAL_HAR_REVIEW_BOUNDS.maxJsonTextBytes
811
+ ) return [];
812
+ let payload = value.trimStart();
813
+ if (payload.startsWith("for (;;);")) payload = payload.slice("for (;;);".length).trimStart();
814
+ const whole = parsedJson(payload);
815
+ if (whole !== null) return [whole];
816
+ const lines = payload.split(/\r?\n/u);
817
+ if (lines.length > INTERNAL_HAR_REVIEW_BOUNDS.maxArrayItems) return [];
818
+ const documents: unknown[] = [];
819
+ for (const line of lines) {
820
+ const trimmed = line.trim();
821
+ if (trimmed === "") continue;
822
+ const parsed = parsedJson(trimmed);
823
+ if (parsed === null) return [];
824
+ documents.push(parsed);
825
+ }
826
+ return documents;
827
+ }
828
+
829
+ function responseFields(response: unknown, url: URL): readonly string[] {
830
+ if (!isRecord(response) || !isRecord(response.content)) return [];
831
+ const rawMime = response.content.mimeType;
832
+ const mime = typeof rawMime === "string"
833
+ && rawMime.length <= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldNameCharacters
834
+ ? rawMime.toLowerCase()
835
+ : "";
836
+ const facebookGraphQl = isExactFacebookOrigin(url) && url.pathname === "/api/graphql/";
837
+ if (
838
+ !mime.includes("json")
839
+ && !(
840
+ facebookGraphQl
841
+ && (
842
+ mime.includes("text/javascript")
843
+ || mime.includes("text/plain")
844
+ || mime.includes("text/html")
845
+ )
846
+ )
847
+ ) return [];
848
+ const fields = new Set<string>();
849
+ const documents = facebookGraphQl && typeof response.content.text === "string"
850
+ ? parsedMetaGraphQlDocuments(response.content.text)
851
+ : [parsedJson(response.content.text)];
852
+ for (const document of documents) {
853
+ addFieldPaths(
854
+ document,
855
+ fields,
856
+ "",
857
+ 0,
858
+ false,
859
+ (fieldName) => reviewedMetaRequestFieldName(url, fieldName),
860
+ isExactFacebookOrigin(url)
861
+ ? isReviewedMetaStructuralContainer
862
+ : isReviewedTwitchGraphQlRoute(url)
863
+ ? isReviewedTwitchStructuralContainer
864
+ : undefined,
865
+ isReviewedLinkedInArticleRoute(url)
866
+ ? INTERNAL_HAR_REVIEW_BOUNDS.maxLinkedInArticleTraversalDepth
867
+ : INTERNAL_HAR_REVIEW_BOUNDS.maxTraversalDepth,
868
+ );
869
+ if (fields.size >= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldPaths) break;
870
+ }
871
+ return [...fields].sort();
872
+ }
873
+
874
+ function metaRegisteredOperationRevision(
875
+ url: URL,
876
+ method: string,
877
+ request: JsonRecord,
878
+ ): string | null {
879
+ if (
880
+ !isExactFacebookOrigin(url)
881
+ || method !== "POST"
882
+ || url.pathname !== "/api/graphql/"
883
+ || !isRecord(request.postData)
884
+ ) return null;
885
+ const form = boundedFormParameters(request.postData);
886
+ if (form === null || form.truncated) return null;
887
+ const friendlyNames: unknown[] = [];
888
+ const documentIds: unknown[] = [];
889
+ for (const candidate of form.parameters) {
890
+ if (!isRecord(candidate) || typeof candidate.name !== "string") continue;
891
+ if (candidate.name === "fb_api_req_friendly_name") friendlyNames.push(candidate.value);
892
+ if (candidate.name === "doc_id") documentIds.push(candidate.value);
893
+ }
894
+ if (friendlyNames.length !== 1 || documentIds.length !== 1) return null;
895
+ const friendlyName = friendlyNames[0];
896
+ const documentId = documentIds[0];
897
+ if (
898
+ typeof friendlyName !== "string"
899
+ || typeof documentId !== "string"
900
+ || friendlyName.length > 161
901
+ || documentId.length > 32
902
+ || !/^[A-Za-z][A-Za-z0-9_]{2,160}$/u.test(friendlyName)
903
+ || !/^[0-9]{5,32}$/u.test(documentId)
904
+ ) return null;
905
+ return `meta=${friendlyName}.${documentId}`;
906
+ }
907
+
908
+ function exactOwnKeys(
909
+ value: JsonRecord,
910
+ expected: readonly string[],
911
+ ): boolean {
912
+ const keys = Object.keys(value).sort();
913
+ return keys.length === expected.length
914
+ && keys.every((key, index) => key === expected[index]);
915
+ }
916
+
917
+ function twitchRegisteredOperationRevisions(
918
+ url: URL,
919
+ method: string,
920
+ request: JsonRecord,
921
+ ): readonly string[] {
922
+ if (
923
+ method !== "POST"
924
+ || !isReviewedTwitchGraphQlRoute(url)
925
+ || !isRecord(request.postData)
926
+ ) return [];
927
+ const rawMime = request.postData.mimeType;
928
+ if (
929
+ typeof rawMime !== "string"
930
+ || rawMime.length > INTERNAL_HAR_REVIEW_BOUNDS.maxFieldNameCharacters
931
+ || rawMime.toLowerCase().split(";", 1)[0]?.trim() !== "text/plain"
932
+ ) return [];
933
+ const body = parsedJson(request.postData.text);
934
+ if (
935
+ !Array.isArray(body)
936
+ || body.length !== 1
937
+ ) return [];
938
+ const candidate = body[0];
939
+ if (
940
+ !isRecord(candidate)
941
+ || !exactOwnKeys(candidate, ["extensions", "operationName", "variables"])
942
+ || typeof candidate.operationName !== "string"
943
+ || !reviewedTwitchGraphQlOperationNames.has(candidate.operationName)
944
+ || !isRecord(candidate.variables)
945
+ || !isRecord(candidate.extensions)
946
+ || !exactOwnKeys(candidate.extensions, ["persistedQuery"])
947
+ || !isRecord(candidate.extensions.persistedQuery)
948
+ || !exactOwnKeys(candidate.extensions.persistedQuery, ["sha256Hash", "version"])
949
+ || candidate.extensions.persistedQuery.version !== 1
950
+ || typeof candidate.extensions.persistedQuery.sha256Hash !== "string"
951
+ || !/^[a-f0-9]{64}$/u.test(candidate.extensions.persistedQuery.sha256Hash)
952
+ ) return [];
953
+ if (
954
+ candidate.operationName === "TopNav_CurrentUser"
955
+ && !exactOwnKeys(candidate.variables, [])
956
+ ) return [];
957
+ if (
958
+ candidate.operationName === "ChannelRoot_AboutPanel"
959
+ && (
960
+ !exactOwnKeys(candidate.variables, ["channelLogin", "skipSchedule"])
961
+ || typeof candidate.variables.channelLogin !== "string"
962
+ || !/^[a-z0-9_]{4,25}$/u.test(candidate.variables.channelLogin)
963
+ || candidate.variables.skipSchedule !== true
964
+ )
965
+ ) return [];
966
+ return [
967
+ `twitch=${candidate.operationName}.${candidate.extensions.persistedQuery.sha256Hash}`,
968
+ ];
969
+ }
970
+
971
+ function revisionValues(url: URL, method: string, request: JsonRecord): readonly string[] {
972
+ const values: string[] = [];
973
+ if (url.origin === "https://www.linkedin.com" && method === "GET") {
974
+ const prefixes = reviewedLinkedInRegisteredQueries.get(url.pathname);
975
+ if (prefixes !== undefined) {
976
+ for (const value of url.searchParams.getAll("queryId")) {
977
+ if (value.length > INTERNAL_HAR_REVIEW_BOUNDS.maxQueryIdCharacters) continue;
978
+ const separator = value.lastIndexOf(".");
979
+ const prefix = value.slice(0, separator);
980
+ const revision = value.slice(separator + 1);
981
+ if (prefixes.has(prefix) && /^[a-f0-9]{32}$/u.test(revision)) values.push(`queryId=${value}`);
982
+ }
983
+ }
984
+ }
985
+ const xGraphQl = reviewedXGraphQlRoute(url);
986
+ if (xGraphQl?.reviewed === true) values.push(`graphql=${xGraphQl.operation}.${xGraphQl.revision}`);
987
+ for (const revision of twitchRegisteredOperationRevisions(url, method, request)) {
988
+ values.push(revision);
989
+ }
990
+ const metaRevision = metaRegisteredOperationRevision(url, method, request);
991
+ if (metaRevision !== null) values.push(metaRevision);
992
+ return values;
993
+ }
994
+
995
+ function registeredOperationType(
996
+ url: URL,
997
+ revision: string | null,
998
+ ): InternalHarCandidate["operationType"] {
999
+ if (revision === null) return "unknown";
1000
+ const xGraphQl = reviewedXGraphQlRoute(url);
1001
+ if (xGraphQl?.reviewed === true) return xGraphQl.operationType;
1002
+ if (revision.startsWith("queryId=")) return "query";
1003
+ if (revision.startsWith("twitch=")) return "query";
1004
+ // Meta friendly names are labels, not authoritative GraphQL syntax. Keep
1005
+ // their type unknown until a code-owned reviewed descriptor supplies it.
1006
+ return "unknown";
1007
+ }
1008
+
1009
+ type Accumulator = {
1010
+ method: string;
1011
+ origin: string;
1012
+ path: string;
1013
+ operationType: InternalHarCandidate["operationType"];
1014
+ semanticRevision: string | null;
1015
+ sampleCount: number;
1016
+ statuses: Set<number>;
1017
+ queryNames: Set<string>;
1018
+ headerNames: Set<string>;
1019
+ requestFieldPaths: Set<string>;
1020
+ responseFieldPaths: Set<string>;
1021
+ revisions: Set<string>;
1022
+ };
1023
+
1024
+ type CandidateCount = {
1025
+ readonly method: string;
1026
+ readonly origin: string;
1027
+ readonly path: string;
1028
+ readonly operationType: InternalHarCandidate["operationType"];
1029
+ readonly semanticRevision: string | null;
1030
+ sampleCount: number;
1031
+ };
1032
+
1033
+ type ReviewedEntry = {
1034
+ readonly entry: JsonRecord;
1035
+ readonly request: JsonRecord;
1036
+ readonly method: string;
1037
+ readonly url: URL;
1038
+ readonly key: string;
1039
+ readonly path: string;
1040
+ readonly operationType: InternalHarCandidate["operationType"];
1041
+ readonly semanticRevision: string | null;
1042
+ };
1043
+
1044
+ function reviewedEntry(
1045
+ entry: unknown,
1046
+ targetOrigin: string,
1047
+ ): ReviewedEntry | null {
1048
+ if (
1049
+ !isRecord(entry)
1050
+ || !isRecord(entry.request)
1051
+ || typeof entry.request.url !== "string"
1052
+ || typeof entry.request.method !== "string"
1053
+ || entry.request.method.length > 16
1054
+ ) return null;
1055
+ const boundedUrl = parseBoundedInternalHarUrl(entry.request.url);
1056
+ if (boundedUrl === null) return null;
1057
+ const { url } = boundedUrl;
1058
+ if (url.origin !== targetOrigin || url.username !== "" || url.password !== "") return null;
1059
+ const method = entry.request.method.toUpperCase();
1060
+ if (!/^(?:GET|HEAD|POST|PUT|PATCH|DELETE)$/u.test(method)) return null;
1061
+ if (/\.(?:css|js|png|jpe?g|gif|svg|woff2?|ico|map)$/iu.test(url.pathname)) return null;
1062
+ const path = boundedUrl.pathTruncated ? "/:oversized-path" : safePath(url);
1063
+ const revisions = [...new Set(revisionValues(url, method, entry.request))];
1064
+ const semanticRevision = revisions.length === 1 ? revisions[0] ?? null : null;
1065
+ const operationType = registeredOperationType(url, semanticRevision);
1066
+ return {
1067
+ entry,
1068
+ request: entry.request,
1069
+ method,
1070
+ url,
1071
+ key: `${method}\0${path}\0${semanticRevision ?? ""}`,
1072
+ path,
1073
+ operationType,
1074
+ semanticRevision,
1075
+ };
1076
+ }
1077
+
1078
+ function compareCandidateCounts(left: CandidateCount, right: CandidateCount): number {
1079
+ return right.sampleCount - left.sampleCount
1080
+ || left.path.localeCompare(right.path)
1081
+ || left.method.localeCompare(right.method)
1082
+ || (left.semanticRevision ?? "").localeCompare(right.semanticRevision ?? "");
1083
+ }
1084
+
1085
+ /**
1086
+ * Retain the exact non-secret names needed to review a first-party API
1087
+ * contract. No URL values, headers, cookies, body values, or response values
1088
+ * survive this analyzer.
1089
+ */
1090
+ export function analyzeInternalHarValue(
1091
+ value: unknown,
1092
+ adapterId: string,
1093
+ targetOrigin: string,
1094
+ now = new Date(),
1095
+ ): InternalHarEvidence {
1096
+ const target = new URL(targetOrigin);
1097
+ if (target.protocol !== "https:" || target.origin !== targetOrigin) throw new Error("target origin must be exact HTTPS");
1098
+ const entries = parseRoot(value);
1099
+ const counts = new Map<string, CandidateCount>();
1100
+ const reviewedKeys: (string | null)[] = [];
1101
+ for (const entry of entries) {
1102
+ const reviewed = reviewedEntry(entry, target.origin);
1103
+ reviewedKeys.push(reviewed?.key ?? null);
1104
+ if (reviewed === null) continue;
1105
+ const current = counts.get(reviewed.key) ?? {
1106
+ method: reviewed.method,
1107
+ origin: reviewed.url.origin,
1108
+ path: reviewed.path,
1109
+ operationType: reviewed.operationType,
1110
+ semanticRevision: reviewed.semanticRevision,
1111
+ sampleCount: 0,
1112
+ };
1113
+ current.sampleCount += 1;
1114
+ counts.set(reviewed.key, current);
1115
+ }
1116
+
1117
+ const selectedCounts = [...counts.entries()]
1118
+ .sort((left, right) => compareCandidateCounts(left[1], right[1]))
1119
+ .slice(0, INTERNAL_HAR_REVIEW_BOUNDS.maxCandidates);
1120
+ const candidates = new Map<string, Accumulator>(selectedCounts.map(([key, candidate]) => [
1121
+ key,
1122
+ {
1123
+ ...candidate,
1124
+ statuses: new Set<number>(),
1125
+ queryNames: new Set<string>(),
1126
+ headerNames: new Set<string>(),
1127
+ requestFieldPaths: new Set<string>(),
1128
+ responseFieldPaths: new Set<string>(),
1129
+ revisions: new Set<string>(),
1130
+ },
1131
+ ]));
1132
+ const selectedEntries = new Map<string, unknown[]>(
1133
+ selectedCounts.map(([key]) => [key, []]),
1134
+ );
1135
+ for (let index = 0; index < entries.length; index += 1) {
1136
+ const key = reviewedKeys[index];
1137
+ if (key === undefined || key === null) continue;
1138
+ selectedEntries.get(key)?.push(entries[index]);
1139
+ }
1140
+ let retainedFieldPaths = 0;
1141
+ let retainedRevisions = 0;
1142
+ const addFieldPath = (output: Set<string>, value: string): void => {
1143
+ if (
1144
+ output.has(value)
1145
+ || output.size >= INTERNAL_HAR_REVIEW_BOUNDS.maxFieldPaths
1146
+ || retainedFieldPaths >= INTERNAL_HAR_REVIEW_BOUNDS.maxTotalFieldPaths
1147
+ ) return;
1148
+ output.add(value);
1149
+ retainedFieldPaths += 1;
1150
+ };
1151
+ const addRevision = (output: Set<string>, value: string): void => {
1152
+ if (
1153
+ output.has(value)
1154
+ || output.size >= INTERNAL_HAR_REVIEW_BOUNDS.maxRevisions
1155
+ || retainedRevisions >= INTERNAL_HAR_REVIEW_BOUNDS.maxTotalRevisions
1156
+ ) return;
1157
+ output.add(value);
1158
+ retainedRevisions += 1;
1159
+ };
1160
+
1161
+ // Enrich in the same deterministic rank order exposed to the reviewer so a
1162
+ // low-ranked prefix of the raw HAR cannot consume the global evidence
1163
+ // budget before a frequently observed candidate is processed.
1164
+ for (const [key] of selectedCounts) {
1165
+ const current = candidates.get(key);
1166
+ if (current === undefined) continue;
1167
+ for (const entry of selectedEntries.get(key) ?? []) {
1168
+ const reviewed = reviewedEntry(entry, target.origin);
1169
+ if (reviewed === null || reviewed.key !== key) continue;
1170
+ if (
1171
+ isRecord(reviewed.entry.response)
1172
+ && typeof reviewed.entry.response.status === "number"
1173
+ && Number.isSafeInteger(reviewed.entry.response.status)
1174
+ && (
1175
+ reviewed.entry.response.status === 0
1176
+ || (reviewed.entry.response.status >= 100 && reviewed.entry.response.status <= 599)
1177
+ )
1178
+ ) {
1179
+ current.statuses.add(reviewed.entry.response.status);
1180
+ }
1181
+ for (const name of reviewed.url.searchParams.keys()) {
1182
+ if (name.length > INTERNAL_HAR_REVIEW_BOUNDS.maxFieldNameCharacters) continue;
1183
+ const renderedName = reviewedMetaRequestFieldName(reviewed.url, name);
1184
+ if (sensitiveName.test(name) && renderedName === ":dynamic") continue;
1185
+ current.queryNames.add(renderedName);
1186
+ }
1187
+ for (const name of headerNames(reviewed.request.headers, reviewed.url)) current.headerNames.add(name);
1188
+ for (const pathName of requestFields(reviewed.request, reviewed.url)) {
1189
+ addFieldPath(current.requestFieldPaths, pathName);
1190
+ }
1191
+ for (const pathName of queryStructuralFields(reviewed.url)) addFieldPath(current.requestFieldPaths, pathName);
1192
+ for (const pathName of responseFields(reviewed.entry.response, reviewed.url)) {
1193
+ addFieldPath(current.responseFieldPaths, pathName);
1194
+ }
1195
+ for (const revision of revisionValues(reviewed.url, reviewed.method, reviewed.request)) {
1196
+ addRevision(current.revisions, revision);
1197
+ }
1198
+ }
1199
+ }
1200
+ return {
1201
+ schemaVersion: 1,
1202
+ adapterId,
1203
+ targetOrigin,
1204
+ analyzedAt: now.toISOString(),
1205
+ observedEntries: entries.length,
1206
+ candidates: [...candidates.values()]
1207
+ .sort(compareCandidateCounts)
1208
+ .map((candidate) => ({
1209
+ method: candidate.method,
1210
+ origin: candidate.origin,
1211
+ path: candidate.path,
1212
+ operationType: candidate.operationType,
1213
+ sampleCount: candidate.sampleCount,
1214
+ statuses: [...candidate.statuses].sort((left, right) => left - right),
1215
+ queryNames: [...candidate.queryNames].sort(),
1216
+ headerNames: [...candidate.headerNames].sort(),
1217
+ requestFieldPaths: [...candidate.requestFieldPaths].sort(),
1218
+ responseFieldPaths: [...candidate.responseFieldPaths].sort(),
1219
+ revisions: [...candidate.revisions].sort(),
1220
+ reviewRequired: true,
1221
+ })),
1222
+ warnings: [
1223
+ "This evidence is inert: every operation still requires a reviewed semantic contract before execution.",
1224
+ "operationType is authoritative only for reviewed route facts; Meta friendly-name evidence remains unknown until bound to a code-owned descriptor.",
1225
+ "Only first-party paths, names, structural field paths, and registered-operation revisions are retained; all credential and content values are discarded.",
1226
+ "Candidate, structural-field, and registered-revision evidence is bounded; lower-ranked candidates and excess fields or revisions may be omitted.",
1227
+ ],
1228
+ };
1229
+ }
1230
+
1231
+ export function analyzeInternalHarFile(
1232
+ path: string,
1233
+ adapterId: string,
1234
+ targetOrigin: string,
1235
+ expectedParent?: Readonly<PrivateDirectoryIdentity>,
1236
+ ): InternalHarEvidence {
1237
+ return analyzeInternalHarValue(
1238
+ JSON.parse(readRegularFile(path, MAX_HAR_BYTES, "HAR input", expectedParent)) as unknown,
1239
+ adapterId,
1240
+ targetOrigin,
1241
+ );
1242
+ }