@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,830 @@
1
+ import { createHash } from "node:crypto";
2
+ import { types as nodeTypes } from "node:util";
3
+
4
+ import type { OperationInput } from "../model";
5
+ import { OmniMaterializerDriftError } from "../omni-model";
6
+ import type {
7
+ OmniAttachmentV1,
8
+ OmniParticipantV1,
9
+ ProviderConversationV1,
10
+ ProviderMaterializedPageV1,
11
+ ProviderMessageV1,
12
+ } from "../omni-model";
13
+ import { isGmailAccountSubject } from "../provider-subject";
14
+ import { parseGmailThreadUrl } from "./gmail-api";
15
+
16
+ type JsonRecord = Readonly<Record<string, unknown>>;
17
+ type GmailListView = "inbox" | "search";
18
+
19
+ const GMAIL_OMNI_BODY_BYTES = 256 * 1024;
20
+ const GMAIL_AGGREGATE_BODY_BYTES = 7 * 1024 * 1024;
21
+ const fatalUtf8Decoder = new TextDecoder("utf-8", {
22
+ fatal: true,
23
+ ignoreBOM: true,
24
+ });
25
+
26
+ type ParsedListInput = Readonly<{
27
+ view: GmailListView;
28
+ query: string | null;
29
+ cursor: string | null;
30
+ limit: number;
31
+ includeSpamTrash: boolean;
32
+ }>;
33
+
34
+ type ParsedParticipant = Readonly<{
35
+ email: string;
36
+ displayName: null;
37
+ }>;
38
+
39
+ type ParsedThreadSummary = Readonly<{
40
+ id: string;
41
+ historyId: string | null;
42
+ snippet: string | null;
43
+ subject: string | null;
44
+ orderedAt: string | null;
45
+ messageCount: number;
46
+ participants: readonly ParsedParticipant[];
47
+ unread: boolean;
48
+ archived: boolean | null;
49
+ }>;
50
+
51
+ type ParsedAttachment = Readonly<{
52
+ partId: string;
53
+ contentDisposition: "attachment" | "inline" | null;
54
+ attachmentId: string | null;
55
+ messageId: string;
56
+ filename: string;
57
+ mimeType: string;
58
+ size: number;
59
+ }>;
60
+
61
+ type ParsedMessage = Readonly<{
62
+ id: string;
63
+ threadId: string | null;
64
+ historyId: string | null;
65
+ internalDate: string | null;
66
+ labelIds: readonly string[];
67
+ from: string | null;
68
+ to: string | null;
69
+ cc: string | null;
70
+ bcc: string | null;
71
+ subject: string | null;
72
+ messageId: string | null;
73
+ inReplyTo: string | null;
74
+ text: string | null;
75
+ bodyTruncated: boolean;
76
+ originalBodyBytes: number;
77
+ attachments: readonly ParsedAttachment[];
78
+ }>;
79
+
80
+ function drift(path: string, message: string): never {
81
+ throw new OmniMaterializerDriftError("gmail", path, message);
82
+ }
83
+
84
+ function record(value: unknown, path: string): JsonRecord {
85
+ if (
86
+ nodeTypes.isProxy(value)
87
+ || typeof value !== "object"
88
+ || value === null
89
+ || Array.isArray(value)
90
+ || (
91
+ Object.getPrototypeOf(value) !== Object.prototype
92
+ && Object.getPrototypeOf(value) !== null
93
+ )
94
+ ) return drift(path, "must be a plain object");
95
+ const descriptors = Object.getOwnPropertyDescriptors(value);
96
+ for (const key of Reflect.ownKeys(descriptors)) {
97
+ if (typeof key !== "string") return drift(path, "must not have symbol properties");
98
+ const descriptor = descriptors[key];
99
+ if (
100
+ descriptor === undefined
101
+ || !("value" in descriptor)
102
+ || !descriptor.enumerable
103
+ ) return drift(`${path}.${key}`, "must be an enumerable data property");
104
+ }
105
+ return value as JsonRecord;
106
+ }
107
+
108
+ function exactKeys(
109
+ value: JsonRecord,
110
+ required: readonly string[],
111
+ optional: readonly string[],
112
+ path: string,
113
+ ): void {
114
+ const allowed = new Set([...required, ...optional]);
115
+ for (const key of Object.keys(value)) {
116
+ if (!allowed.has(key)) return drift(path, "contains an unreviewed property");
117
+ }
118
+ for (const key of required) {
119
+ if (!Object.hasOwn(value, key)) return drift(`${path}.${key}`, "is required");
120
+ }
121
+ }
122
+
123
+ function array(value: unknown, path: string, maximum: number): readonly unknown[] {
124
+ if (
125
+ nodeTypes.isProxy(value)
126
+ || !Array.isArray(value)
127
+ || Object.getPrototypeOf(value) !== Array.prototype
128
+ || value.length > maximum
129
+ ) return drift(path, `must be an array of at most ${maximum} items`);
130
+ const descriptors = Object.getOwnPropertyDescriptors(value);
131
+ const expected = new Set<PropertyKey>([
132
+ "length",
133
+ ...Array.from({ length: value.length }, (_unused, index) => String(index)),
134
+ ]);
135
+ if (
136
+ Reflect.ownKeys(descriptors).length !== expected.size
137
+ || Reflect.ownKeys(descriptors).some((key) => !expected.has(key))
138
+ ) return drift(path, "must be a dense array without named properties");
139
+ const result: unknown[] = [];
140
+ for (let index = 0; index < value.length; index += 1) {
141
+ const descriptor = descriptors[String(index)];
142
+ if (
143
+ descriptor === undefined
144
+ || !("value" in descriptor)
145
+ || !descriptor.enumerable
146
+ ) return drift(`${path}[${index}]`, "must be an enumerable data property");
147
+ result.push(descriptor.value);
148
+ }
149
+ return Object.freeze(result);
150
+ }
151
+
152
+ function hasUnsafeControl(value: string, allowLayout = false): boolean {
153
+ for (let index = 0; index < value.length; index += 1) {
154
+ const code = value.charCodeAt(index);
155
+ if (
156
+ (code <= 0x1f && !(allowLayout && (code === 0x09 || code === 0x0a || code === 0x0d)))
157
+ || code === 0x7f
158
+ || (code >= 0x80 && code <= 0x9f)
159
+ || code === 0x061c
160
+ || code === 0x200e
161
+ || code === 0x200f
162
+ || (code >= 0x2028 && code <= 0x202e)
163
+ || (code >= 0x2066 && code <= 0x2069)
164
+ ) return true;
165
+ }
166
+ return false;
167
+ }
168
+
169
+ function isWellFormedText(value: string): boolean {
170
+ for (let index = 0; index < value.length; index += 1) {
171
+ const code = value.charCodeAt(index);
172
+ if (code >= 0xd800 && code <= 0xdbff) {
173
+ const next = value.charCodeAt(index + 1);
174
+ if (!(next >= 0xdc00 && next <= 0xdfff)) return false;
175
+ index += 1;
176
+ } else if (code >= 0xdc00 && code <= 0xdfff) return false;
177
+ }
178
+ return true;
179
+ }
180
+
181
+ function string(
182
+ value: unknown,
183
+ path: string,
184
+ maximum: number,
185
+ allowEmpty = false,
186
+ allowLayout = false,
187
+ ): string {
188
+ if (
189
+ typeof value !== "string"
190
+ || !isWellFormedText(value)
191
+ || Buffer.byteLength(value, "utf8") > maximum
192
+ || (!allowEmpty && value.length === 0)
193
+ || hasUnsafeControl(value, allowLayout)
194
+ ) return drift(path, "must be bounded text without unsafe controls");
195
+ return value;
196
+ }
197
+
198
+ function inputString(value: unknown, path: string, maximum: number): string {
199
+ if (
200
+ typeof value !== "string"
201
+ || value.length === 0
202
+ || value.length > maximum
203
+ || !isWellFormedText(value)
204
+ || hasUnsafeControl(value)
205
+ ) return drift(path, "must be bounded input text without unsafe controls");
206
+ return value;
207
+ }
208
+
209
+ function nullableString(
210
+ value: unknown,
211
+ path: string,
212
+ maximum: number,
213
+ allowEmpty = false,
214
+ allowLayout = false,
215
+ ): string | null {
216
+ return value === null
217
+ ? null
218
+ : string(value, path, maximum, allowEmpty, allowLayout);
219
+ }
220
+
221
+ function integer(value: unknown, path: string, minimum: number, maximum: number): number {
222
+ if (
223
+ typeof value !== "number"
224
+ || !Number.isSafeInteger(value)
225
+ || value < minimum
226
+ || value > maximum
227
+ ) return drift(path, `must be an integer from ${minimum} through ${maximum}`);
228
+ return value;
229
+ }
230
+
231
+ function boolean(value: unknown, path: string): boolean {
232
+ if (typeof value !== "boolean") return drift(path, "must be boolean");
233
+ return value;
234
+ }
235
+
236
+ function nullableBoolean(value: unknown, path: string): boolean | null {
237
+ return value === null ? null : boolean(value, path);
238
+ }
239
+
240
+ function gmailId(value: unknown, path: string): string {
241
+ const result = string(value, path, 256);
242
+ if (!/^[A-Za-z0-9_-]{1,256}$/u.test(result)) {
243
+ return drift(path, "must be an exact Gmail ID");
244
+ }
245
+ return result;
246
+ }
247
+
248
+ function attachmentId(value: unknown, path: string): string | null {
249
+ return value === null ? null : string(value, path, 4_096);
250
+ }
251
+
252
+ function historyId(value: unknown, path: string): string | null {
253
+ if (value === null) return null;
254
+ const result = string(value, path, 64);
255
+ if (!/^[0-9]{1,64}$/u.test(result)) return drift(path, "must be a decimal history ID");
256
+ return result;
257
+ }
258
+
259
+ function timestamp(value: unknown, path: string): string | null {
260
+ if (value === null) return null;
261
+ const result = string(value, path, 64);
262
+ const date = new Date(result);
263
+ if (!Number.isFinite(date.getTime()) || date.toISOString() !== result) {
264
+ return drift(path, "must be a canonical ISO timestamp or null");
265
+ }
266
+ return result;
267
+ }
268
+
269
+ function accountSubject(value: unknown, path: string): string {
270
+ const result = string(value, path, 254);
271
+ if (!isGmailAccountSubject(result)) return drift(path, "must be an exact Gmail email subject");
272
+ return result;
273
+ }
274
+
275
+ function parseListInput(input: OperationInput): ParsedListInput {
276
+ const source = record(input, "messaging.list input");
277
+ exactKeys(source, ["view"], [
278
+ "query",
279
+ "cursor",
280
+ "limit",
281
+ "include_spam_trash",
282
+ ], "messaging.list input");
283
+ if (source.view !== "inbox" && source.view !== "search") {
284
+ return drift("messaging.list input.view", "must be inbox or search");
285
+ }
286
+ const query = source.query === undefined
287
+ ? null
288
+ : inputString(source.query, "messaging.list input.query", 512);
289
+ if (source.view === "search" && query === null) {
290
+ return drift("messaging.list input.query", "is required when view is search");
291
+ }
292
+ if (source.view === "inbox" && query !== null) {
293
+ return drift("messaging.list input.query", "is not accepted when view is inbox");
294
+ }
295
+ if (query !== null && query.trim().length === 0) {
296
+ return drift("messaging.list input.query", "must contain a non-whitespace expression");
297
+ }
298
+ const includeSpamTrash = source.include_spam_trash === undefined
299
+ ? false
300
+ : boolean(source.include_spam_trash, "messaging.list input.include_spam_trash");
301
+ if (source.view === "inbox" && source.include_spam_trash !== undefined) {
302
+ return drift("messaging.list input.include_spam_trash", "is accepted only for search");
303
+ }
304
+ return Object.freeze({
305
+ view: source.view,
306
+ query,
307
+ cursor: source.cursor === undefined
308
+ ? null
309
+ : inputString(source.cursor, "messaging.list input.cursor", 4_096),
310
+ limit: source.limit === undefined
311
+ ? 50
312
+ : integer(source.limit, "messaging.list input.limit", 1, 100),
313
+ includeSpamTrash,
314
+ });
315
+ }
316
+
317
+ function parseReadInput(input: OperationInput): string {
318
+ const source = record(input, "messaging.read input");
319
+ exactKeys(source, ["thread_id"], [], "messaging.read input");
320
+ return gmailId(source.thread_id, "messaging.read input.thread_id");
321
+ }
322
+
323
+ function parseParticipant(value: unknown, path: string): ParsedParticipant {
324
+ const source = record(value, path);
325
+ exactKeys(source, ["email", "displayName"], [], path);
326
+ if (source.displayName !== null) return drift(`${path}.displayName`, "must be null in the reviewed projection");
327
+ return Object.freeze({
328
+ email: accountSubject(source.email, `${path}.email`).toLowerCase(),
329
+ displayName: null,
330
+ });
331
+ }
332
+
333
+ function parseThreadUrl(
334
+ value: unknown,
335
+ path: string,
336
+ account: string,
337
+ expectedThreadId: string,
338
+ expectedView: "all" | "inbox",
339
+ ): void {
340
+ const raw = string(value, path, 8_192);
341
+ let parsed;
342
+ try {
343
+ parsed = parseGmailThreadUrl(new URL(raw));
344
+ } catch {
345
+ return drift(path, "must be an exact reviewed Gmail thread URL");
346
+ }
347
+ if (
348
+ parsed.threadId !== expectedThreadId
349
+ || parsed.view !== expectedView
350
+ || parsed.accountLocator.toLowerCase() !== account.toLowerCase()
351
+ || parsed.canonicalUrl !== raw
352
+ ) return drift(path, "must bind the output account, view, and thread ID");
353
+ }
354
+
355
+ function parseThreadSummary(
356
+ value: unknown,
357
+ path: string,
358
+ account: string,
359
+ view: GmailListView,
360
+ ): ParsedThreadSummary {
361
+ const source = record(value, path);
362
+ exactKeys(source, [
363
+ "id",
364
+ "historyId",
365
+ "snippet",
366
+ "subject",
367
+ "orderedAt",
368
+ "messageCount",
369
+ "participants",
370
+ "unread",
371
+ "archived",
372
+ "threadUrl",
373
+ "readInput",
374
+ ], [], path);
375
+ const id = gmailId(source.id, `${path}.id`);
376
+ const readInput = record(source.readInput, `${path}.readInput`);
377
+ exactKeys(readInput, ["thread_id"], [], `${path}.readInput`);
378
+ if (gmailId(readInput.thread_id, `${path}.readInput.thread_id`) !== id) {
379
+ return drift(`${path}.readInput.thread_id`, "must bind the summary thread ID");
380
+ }
381
+ parseThreadUrl(
382
+ source.threadUrl,
383
+ `${path}.threadUrl`,
384
+ account,
385
+ id,
386
+ view === "inbox" ? "inbox" : "all",
387
+ );
388
+ const participants = Object.freeze(array(source.participants, `${path}.participants`, 256)
389
+ .map((entry, index) => parseParticipant(entry, `${path}.participants[${index}]`)));
390
+ const participantIds = participants.map((participant) => participant.email);
391
+ if (new Set(participantIds).size !== participantIds.length) {
392
+ return drift(`${path}.participants`, "contains duplicate canonical email identities");
393
+ }
394
+ return Object.freeze({
395
+ id,
396
+ historyId: historyId(source.historyId, `${path}.historyId`),
397
+ snippet: nullableString(source.snippet, `${path}.snippet`, 64 * 1024, true, true),
398
+ subject: nullableString(source.subject, `${path}.subject`, 8_192, true, true),
399
+ orderedAt: timestamp(source.orderedAt, `${path}.orderedAt`),
400
+ messageCount: integer(source.messageCount, `${path}.messageCount`, 1, Number.MAX_SAFE_INTEGER),
401
+ participants,
402
+ unread: boolean(source.unread, `${path}.unread`),
403
+ archived: nullableBoolean(source.archived, `${path}.archived`),
404
+ });
405
+ }
406
+
407
+ function materializedParticipant(value: ParsedParticipant): OmniParticipantV1 {
408
+ return Object.freeze({
409
+ providerId: value.email,
410
+ displayName: value.displayName,
411
+ handle: value.email,
412
+ });
413
+ }
414
+
415
+ function materializedConversation(value: ParsedThreadSummary): ProviderConversationV1 {
416
+ return Object.freeze({
417
+ kind: "conversation",
418
+ conversationKind: "unknown",
419
+ providerId: value.id,
420
+ providerRevision: value.historyId,
421
+ orderedAt: value.orderedAt,
422
+ detail: "summary",
423
+ title: value.subject,
424
+ summary: value.snippet,
425
+ participants: Object.freeze(value.participants.map(materializedParticipant)),
426
+ unread: value.unread,
427
+ unreadCount: null,
428
+ archived: value.archived,
429
+ pending: null,
430
+ });
431
+ }
432
+
433
+ function listPartition(account: string, input: ParsedListInput): string {
434
+ if (input.view === "inbox") return `${account.toLowerCase()}:gmail:threads:inbox`;
435
+ const digest = createHash("sha256")
436
+ .update(input.query ?? "", "utf8")
437
+ .digest("hex");
438
+ return `${account.toLowerCase()}:gmail:threads:search:${input.includeSpamTrash ? "all" : "ordinary"}:${digest}`;
439
+ }
440
+
441
+ /** Strict Gmail thread-list normalization with replayable opaque pagination. */
442
+ export function materializeGmailMessagingList(
443
+ input: OperationInput,
444
+ output: unknown,
445
+ ): ProviderMaterializedPageV1 {
446
+ const parsedInput = parseListInput(input);
447
+ const source = record(output, "messaging.list output");
448
+ exactKeys(source, [
449
+ "provider",
450
+ "operation",
451
+ "accountSubject",
452
+ "view",
453
+ "query",
454
+ "includeSpamTrash",
455
+ "threads",
456
+ "nextCursor",
457
+ "resultSizeEstimate",
458
+ ], [], "messaging.list output");
459
+ if (source.provider !== "gmail") return drift("messaging.list output.provider", "must be gmail");
460
+ if (source.operation !== "messaging.list") {
461
+ return drift("messaging.list output.operation", "must be messaging.list");
462
+ }
463
+ const account = accountSubject(source.accountSubject, "messaging.list output.accountSubject");
464
+ if (source.view !== parsedInput.view) {
465
+ return drift("messaging.list output.view", "must bind messaging.list input.view");
466
+ }
467
+ if (source.query !== parsedInput.query) {
468
+ return drift("messaging.list output.query", "must bind messaging.list input.query");
469
+ }
470
+ if (source.includeSpamTrash !== parsedInput.includeSpamTrash) {
471
+ return drift(
472
+ "messaging.list output.includeSpamTrash",
473
+ "must bind messaging.list input.include_spam_trash",
474
+ );
475
+ }
476
+ const summaries = Object.freeze(array(
477
+ source.threads,
478
+ "messaging.list output.threads",
479
+ parsedInput.limit,
480
+ ).map((entry, index) => parseThreadSummary(
481
+ entry,
482
+ `messaging.list output.threads[${index}]`,
483
+ account,
484
+ parsedInput.view,
485
+ )));
486
+ const ids = summaries.map((thread) => thread.id);
487
+ if (new Set(ids).size !== ids.length) {
488
+ return drift("messaging.list output.threads", "contains duplicate stable thread IDs");
489
+ }
490
+ const nextCursor = nullableString(
491
+ source.nextCursor,
492
+ "messaging.list output.nextCursor",
493
+ 4_096,
494
+ );
495
+ if (nextCursor !== null && nextCursor === parsedInput.cursor) {
496
+ return drift("messaging.list output.nextCursor", "must advance beyond the input cursor");
497
+ }
498
+ integer(
499
+ source.resultSizeEstimate,
500
+ "messaging.list output.resultSizeEstimate",
501
+ 0,
502
+ Number.MAX_SAFE_INTEGER,
503
+ );
504
+ const complete = parsedInput.cursor === null && nextCursor === null;
505
+ const nextInput = nextCursor === null
506
+ ? null
507
+ : parsedInput.view === "inbox"
508
+ ? Object.freeze({
509
+ view: "inbox" as const,
510
+ cursor: nextCursor,
511
+ limit: parsedInput.limit,
512
+ })
513
+ : Object.freeze({
514
+ view: "search" as const,
515
+ query: parsedInput.query ?? "",
516
+ cursor: nextCursor,
517
+ limit: parsedInput.limit,
518
+ include_spam_trash: parsedInput.includeSpamTrash,
519
+ });
520
+ return Object.freeze({
521
+ schemaVersion: 1,
522
+ partition: listPartition(account, parsedInput),
523
+ completeness: Object.freeze({
524
+ kind: complete ? "complete" : "page",
525
+ reason: complete
526
+ ? "Gmail returned the complete requested thread collection in one root page."
527
+ : nextCursor === null
528
+ ? "Gmail returned a terminal continuation page without standalone partition completeness."
529
+ : "Gmail returned an opaque cursor for an older thread-list page.",
530
+ }),
531
+ cursor: Object.freeze({
532
+ direction: "backward",
533
+ request: parsedInput.cursor,
534
+ nextInput,
535
+ }),
536
+ entities: Object.freeze(summaries.map(materializedConversation)),
537
+ tombstones: Object.freeze([]),
538
+ });
539
+ }
540
+
541
+ function parseHeader(value: unknown, path: string): string | null {
542
+ return nullableString(value, path, 64 * 1024, true, true);
543
+ }
544
+
545
+ function parseBody(
546
+ value: unknown,
547
+ path: string,
548
+ ): Readonly<{
549
+ text: string | null;
550
+ truncated: boolean;
551
+ originalBytes: number;
552
+ }> {
553
+ const source = record(value, path);
554
+ exactKeys(source, ["text", "source"], [], path);
555
+ if (
556
+ source.source !== "text/plain"
557
+ && source.source !== "text/html"
558
+ && source.source !== "none"
559
+ ) return drift(`${path}.source`, "must be text/plain, text/html, or none");
560
+ if (source.text === null) {
561
+ if (source.source !== "none") {
562
+ return drift(path, "must bind body text to its MIME projection source");
563
+ }
564
+ return Object.freeze({ text: null, truncated: false, originalBytes: 0 });
565
+ }
566
+ if (
567
+ typeof source.text !== "string"
568
+ || source.text.length > GMAIL_AGGREGATE_BODY_BYTES
569
+ || !isWellFormedText(source.text)
570
+ || hasUnsafeControl(source.text, true)
571
+ ) return drift(`${path}.text`, "must be bounded text without unsafe controls");
572
+ if (source.source === "none") {
573
+ return drift(path, "must bind body text to its MIME projection source");
574
+ }
575
+ const originalBytes = Buffer.byteLength(source.text, "utf8");
576
+ if (originalBytes > GMAIL_AGGREGATE_BODY_BYTES) {
577
+ return drift(
578
+ `${path}.text`,
579
+ `must not exceed the ${GMAIL_AGGREGATE_BODY_BYTES}-byte Gmail aggregate body ceiling`,
580
+ );
581
+ }
582
+ const encoded = Buffer.from(source.text, "utf8");
583
+ if (fatalUtf8Decoder.decode(encoded) !== source.text) {
584
+ return drift(`${path}.text`, "must contain valid Unicode scalar text");
585
+ }
586
+ if (encoded.byteLength <= GMAIL_OMNI_BODY_BYTES) {
587
+ return Object.freeze({
588
+ text: source.text,
589
+ truncated: false,
590
+ originalBytes: encoded.byteLength,
591
+ });
592
+ }
593
+ let prefixBytes = GMAIL_OMNI_BODY_BYTES;
594
+ while (
595
+ prefixBytes > 0
596
+ && (encoded[prefixBytes]! & 0xc0) === 0x80
597
+ ) prefixBytes -= 1;
598
+ return Object.freeze({
599
+ text: fatalUtf8Decoder.decode(encoded.subarray(0, prefixBytes)),
600
+ truncated: true,
601
+ originalBytes: encoded.byteLength,
602
+ });
603
+ }
604
+
605
+ function parseAttachment(
606
+ value: unknown,
607
+ path: string,
608
+ messageIdValue: string,
609
+ ): ParsedAttachment {
610
+ const source = record(value, path);
611
+ exactKeys(source, [
612
+ "partId",
613
+ "contentDisposition",
614
+ "attachmentId",
615
+ "messageId",
616
+ "filename",
617
+ "mimeType",
618
+ "size",
619
+ ], [], path);
620
+ const messageId = gmailId(source.messageId, `${path}.messageId`);
621
+ if (messageId !== messageIdValue) {
622
+ return drift(`${path}.messageId`, "must bind its containing Gmail message");
623
+ }
624
+ const mimeType = string(source.mimeType, `${path}.mimeType`, 256);
625
+ if (!/^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+(?:\s*;.*)?$/u.test(mimeType)) {
626
+ return drift(`${path}.mimeType`, "must be a bounded MIME type");
627
+ }
628
+ if (
629
+ source.contentDisposition !== null
630
+ && source.contentDisposition !== "attachment"
631
+ && source.contentDisposition !== "inline"
632
+ ) {
633
+ return drift(`${path}.contentDisposition`, "must be attachment, inline, or null");
634
+ }
635
+ return Object.freeze({
636
+ partId: string(source.partId, `${path}.partId`, 256),
637
+ contentDisposition: source.contentDisposition,
638
+ attachmentId: attachmentId(source.attachmentId, `${path}.attachmentId`),
639
+ messageId,
640
+ filename: string(source.filename, `${path}.filename`, 8_192, true, true),
641
+ mimeType,
642
+ size: integer(source.size, `${path}.size`, 0, Number.MAX_SAFE_INTEGER),
643
+ });
644
+ }
645
+
646
+ function parseMessage(
647
+ value: unknown,
648
+ path: string,
649
+ expectedThreadId: string,
650
+ ): ParsedMessage {
651
+ const source = record(value, path);
652
+ exactKeys(source, [
653
+ "id",
654
+ "threadId",
655
+ "historyId",
656
+ "internalDate",
657
+ "labelIds",
658
+ "snippet",
659
+ "from",
660
+ "to",
661
+ "cc",
662
+ "bcc",
663
+ "subject",
664
+ "date",
665
+ "messageId",
666
+ "inReplyTo",
667
+ "body",
668
+ "attachments",
669
+ ], [], path);
670
+ const id = gmailId(source.id, `${path}.id`);
671
+ const threadId = source.threadId === null
672
+ ? null
673
+ : gmailId(source.threadId, `${path}.threadId`);
674
+ if (threadId !== null && threadId !== expectedThreadId) {
675
+ return drift(`${path}.threadId`, "must bind its containing Gmail thread");
676
+ }
677
+ const labelIds = Object.freeze(array(source.labelIds, `${path}.labelIds`, 1_000)
678
+ .map((entry, index) => string(entry, `${path}.labelIds[${index}]`, 256)));
679
+ if (new Set(labelIds).size !== labelIds.length) {
680
+ return drift(`${path}.labelIds`, "contains duplicate label IDs");
681
+ }
682
+ // Validate the snippet even though full message bodies are authoritative.
683
+ nullableString(source.snippet, `${path}.snippet`, 64 * 1024, true, true);
684
+ const body = parseBody(source.body, `${path}.body`);
685
+ return Object.freeze({
686
+ id,
687
+ threadId: threadId ?? expectedThreadId,
688
+ historyId: historyId(source.historyId, `${path}.historyId`),
689
+ internalDate: timestamp(source.internalDate, `${path}.internalDate`),
690
+ labelIds,
691
+ from: parseHeader(source.from, `${path}.from`),
692
+ to: parseHeader(source.to, `${path}.to`),
693
+ cc: parseHeader(source.cc, `${path}.cc`),
694
+ bcc: parseHeader(source.bcc, `${path}.bcc`),
695
+ subject: parseHeader(source.subject, `${path}.subject`),
696
+ date: parseHeader(source.date, `${path}.date`),
697
+ messageId: parseHeader(source.messageId, `${path}.messageId`),
698
+ inReplyTo: parseHeader(source.inReplyTo, `${path}.inReplyTo`),
699
+ text: body.text,
700
+ bodyTruncated: body.truncated,
701
+ originalBodyBytes: body.originalBytes,
702
+ attachments: Object.freeze(array(source.attachments, `${path}.attachments`, 512)
703
+ .map((entry, index) => parseAttachment(entry, `${path}.attachments[${index}]`, id))),
704
+ });
705
+ }
706
+
707
+ function extractEmails(value: string | null): readonly string[] {
708
+ if (value === null) return Object.freeze([]);
709
+ const matches = value.match(/[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Za-z0-9.-]+/gu) ?? [];
710
+ return Object.freeze([...new Set(matches
711
+ .filter((candidate) => isGmailAccountSubject(candidate))
712
+ .map((candidate) => candidate.toLowerCase()))]);
713
+ }
714
+
715
+ function participant(email: string): OmniParticipantV1 {
716
+ return Object.freeze({ providerId: email, displayName: null, handle: email });
717
+ }
718
+
719
+ function attachmentKind(mimeType: string): OmniAttachmentV1["kind"] {
720
+ if (mimeType.startsWith("audio/")) return "audio";
721
+ if (mimeType.startsWith("image/")) return "image";
722
+ if (mimeType.startsWith("video/")) return "video";
723
+ return "document";
724
+ }
725
+
726
+ function materializedAttachment(value: ParsedAttachment): OmniAttachmentV1 {
727
+ return Object.freeze({
728
+ kind: attachmentKind(value.mimeType),
729
+ mimeType: value.mimeType,
730
+ name: value.filename,
731
+ sizeBytes: value.size,
732
+ });
733
+ }
734
+
735
+ function materializedMessage(value: ParsedMessage): ProviderMessageV1 {
736
+ const senderEmail = extractEmails(value.from)[0];
737
+ const recipientEmails = [...new Set([
738
+ ...extractEmails(value.to),
739
+ ...extractEmails(value.cc),
740
+ ...extractEmails(value.bcc),
741
+ ])];
742
+ return Object.freeze({
743
+ kind: "message",
744
+ providerId: value.id,
745
+ providerRevision: value.historyId,
746
+ orderedAt: value.internalDate,
747
+ conversationProviderId: value.threadId,
748
+ sender: senderEmail === undefined ? null : participant(senderEmail),
749
+ recipients: Object.freeze(recipientEmails.map(participant)),
750
+ direction: value.labelIds.includes("SENT") ? "outgoing" : "incoming",
751
+ subject: value.subject,
752
+ body: value.text,
753
+ bodyTruncated: value.bodyTruncated,
754
+ unread: value.labelIds.includes("UNREAD"),
755
+ replyToProviderId: null,
756
+ state: "active",
757
+ attachments: Object.freeze(value.attachments.map(materializedAttachment)),
758
+ });
759
+ }
760
+
761
+ /** Strict full-thread normalization with message and attachment metadata only. */
762
+ export function materializeGmailMessagingRead(
763
+ input: OperationInput,
764
+ output: unknown,
765
+ ): ProviderMaterializedPageV1 {
766
+ const requestedThreadId = parseReadInput(input);
767
+ const source = record(output, "messaging.read output");
768
+ exactKeys(source, [
769
+ "provider",
770
+ "operation",
771
+ "accountSubject",
772
+ "thread",
773
+ "threadUrl",
774
+ ], [], "messaging.read output");
775
+ if (source.provider !== "gmail") return drift("messaging.read output.provider", "must be gmail");
776
+ if (source.operation !== "messaging.read") {
777
+ return drift("messaging.read output.operation", "must be messaging.read");
778
+ }
779
+ const account = accountSubject(source.accountSubject, "messaging.read output.accountSubject");
780
+ const thread = record(source.thread, "messaging.read output.thread");
781
+ exactKeys(thread, ["id", "snippet", "historyId", "messages"], [], "messaging.read output.thread");
782
+ const threadId = gmailId(thread.id, "messaging.read output.thread.id");
783
+ if (threadId !== requestedThreadId) {
784
+ return drift("messaging.read output.thread.id", "must bind messaging.read input.thread_id");
785
+ }
786
+ nullableString(thread.snippet, "messaging.read output.thread.snippet", 64 * 1024, true, true);
787
+ historyId(thread.historyId, "messaging.read output.thread.historyId");
788
+ parseThreadUrl(
789
+ source.threadUrl,
790
+ "messaging.read output.threadUrl",
791
+ account,
792
+ threadId,
793
+ "all",
794
+ );
795
+ let aggregateBodyBytes = 0;
796
+ const messages = Object.freeze(array(
797
+ thread.messages,
798
+ "messaging.read output.thread.messages",
799
+ 1_000,
800
+ ).map((entry, index) => {
801
+ const path = `messaging.read output.thread.messages[${index}]`;
802
+ const parsed = parseMessage(entry, path, threadId);
803
+ if (parsed.originalBodyBytes > GMAIL_AGGREGATE_BODY_BYTES - aggregateBodyBytes) {
804
+ return drift(
805
+ `${path}.body.text`,
806
+ `causes the thread to exceed the ${GMAIL_AGGREGATE_BODY_BYTES}-byte Gmail aggregate body ceiling`,
807
+ );
808
+ }
809
+ aggregateBodyBytes += parsed.originalBodyBytes;
810
+ return parsed;
811
+ }));
812
+ if (messages.length === 0) {
813
+ return drift("messaging.read output.thread.messages", "must contain at least one message");
814
+ }
815
+ const ids = messages.map((message) => message.id);
816
+ if (new Set(ids).size !== ids.length) {
817
+ return drift("messaging.read output.thread.messages", "contains duplicate stable message IDs");
818
+ }
819
+ return Object.freeze({
820
+ schemaVersion: 1,
821
+ partition: `${account.toLowerCase()}:gmail:thread:${threadId}`,
822
+ completeness: Object.freeze({
823
+ kind: "complete",
824
+ reason: "Gmail returned the complete full-MIME representation of the exact requested thread.",
825
+ }),
826
+ cursor: Object.freeze({ direction: "none", request: null, nextInput: null }),
827
+ entities: Object.freeze(messages.map(materializedMessage)),
828
+ tombstones: Object.freeze([]),
829
+ });
830
+ }