@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,1130 @@
1
+ import { createHash } from "node:crypto";
2
+ import { types as nodeTypes } from "node:util";
3
+
4
+ import type { OperationInput } from "../model";
5
+ import type { ProviderActionContext } from "../provider-context";
6
+ import {
7
+ parseContactDirectionStats,
8
+ projectContactDirectionStats,
9
+ type ContactDirectionStats,
10
+ } from "./contact-projection";
11
+ import {
12
+ buildGmailThreadUrl,
13
+ createGmailApiClient,
14
+ extractGmailEmailAddresses,
15
+ fetchGmailContacts,
16
+ fetchGmailMessageInteractionMetadata,
17
+ fetchGmailMessageList,
18
+ fetchGmailMessageMetadata,
19
+ fetchGmailThread,
20
+ listGmailSendAsAliases,
21
+ fetchGmailThreadList,
22
+ fetchGmailThreadMetadata,
23
+ getAuthenticatedGmailProfile,
24
+ parseGmailId,
25
+ parseGmailThread,
26
+ resolveGmailThreadBodies,
27
+ type GmailApiClient,
28
+ type GmailContact,
29
+ type GmailContactCollection,
30
+ type GmailProfile,
31
+ type GmailThread,
32
+ } from "./gmail-api";
33
+
34
+ const CONTACTS_DEFAULT_LIMIT = 20;
35
+ const CONTACTS_MAX_LIMIT = 100;
36
+ const CONTACTS_DEFAULT_STATS_SCAN_LIMIT = 100;
37
+ const CONTACTS_MAX_STATS_SCAN_LIMIT = 2_000;
38
+ const CONTACT_STATS_MAX_DIRECTION_PRODUCT = 2_000;
39
+ const INTERACTIONS_DEFAULT_LIMIT = 100;
40
+ const INTERACTIONS_MAX_LIMIT = 100;
41
+ const INTERACTIONS_MAX_ADDRESSES = 10_000;
42
+ const MESSAGING_DEFAULT_LIMIT = 50;
43
+ const MESSAGING_MAX_LIMIT = 100;
44
+ const GMAIL_LIST_PAGE_MAXIMUM = 500;
45
+ const CONTACT_STATS_QUERY_MAX_BYTES = 4_096;
46
+ const MAX_CONCURRENT_GMAIL_READS = 4;
47
+ const GMAIL_MESSAGING_READ_BODY_BYTES = 7 * 1024 * 1024;
48
+
49
+ type JsonRecord = Readonly<Record<string, unknown>>;
50
+ type GmailListView = "inbox" | "search";
51
+ type ContactDirection = "sent" | "received";
52
+
53
+ type ParsedContactsInput = Readonly<{
54
+ collection: GmailContactCollection;
55
+ cursor: string | null;
56
+ projection: "core" | "dates";
57
+ includeStats: boolean;
58
+ limit: number;
59
+ statsScanLimit: number;
60
+ }>;
61
+
62
+ type ParsedInteractionsInput = Readonly<{
63
+ after: string | null;
64
+ afterMilliseconds: number | null;
65
+ before: string;
66
+ beforeMilliseconds: number;
67
+ cursor: string | null;
68
+ limit: number;
69
+ }>;
70
+
71
+ type ParsedMessagingListInput = Readonly<{
72
+ view: GmailListView;
73
+ query: string | null;
74
+ cursor: string | null;
75
+ limit: number;
76
+ includeSpamTrash: boolean;
77
+ }>;
78
+
79
+ type GmailDirectionStatsIncompleteReason =
80
+ | "message-internal-date-unavailable"
81
+ | "no-contact-addresses"
82
+ | "scan-limit-reached"
83
+ | "unsupported-contact-addresses";
84
+
85
+ type ContactEmailCoverage = Readonly<{
86
+ emails: readonly string[];
87
+ kind: "complete" | "partial" | "unavailable" | "unsupported";
88
+ unsupportedAddressCount: number;
89
+ }>;
90
+
91
+ type ConcurrentAdmission = <T>(action: () => Promise<T>) => Promise<T>;
92
+
93
+ export type GmailThreadSummary = Readonly<{
94
+ id: string;
95
+ historyId: string | null;
96
+ snippet: string | null;
97
+ subject: string | null;
98
+ orderedAt: string | null;
99
+ messageCount: number;
100
+ participants: readonly Readonly<{
101
+ email: string;
102
+ displayName: null;
103
+ }>[];
104
+ unread: boolean;
105
+ archived: boolean | null;
106
+ threadUrl: string;
107
+ readInput: Readonly<{ thread_id: string }>;
108
+ }>;
109
+
110
+ export type GmailMessagingListOutput = Readonly<{
111
+ provider: "gmail";
112
+ operation: "messaging.list";
113
+ accountSubject: string;
114
+ view: GmailListView;
115
+ query: string | null;
116
+ includeSpamTrash: boolean;
117
+ threads: readonly GmailThreadSummary[];
118
+ nextCursor: string | null;
119
+ resultSizeEstimate: number;
120
+ }>;
121
+
122
+ export type GmailMessagingReadOutput = Readonly<{
123
+ provider: "gmail";
124
+ operation: "messaging.read";
125
+ accountSubject: string;
126
+ thread: GmailThread;
127
+ threadUrl: string;
128
+ }>;
129
+
130
+ export type GmailContactInteraction = Readonly<{
131
+ email: string;
132
+ sentCount: number;
133
+ receivedCount: number;
134
+ sentCountComplete: boolean;
135
+ receivedCountComplete: boolean;
136
+ firstSentAt: string | null;
137
+ lastSentAt: string | null;
138
+ firstReceivedAt: string | null;
139
+ lastReceivedAt: string | null;
140
+ sent30d: number;
141
+ sent90d: number;
142
+ sent365d: number;
143
+ received30d: number;
144
+ received90d: number;
145
+ received365d: number;
146
+ }>;
147
+
148
+ export type GmailContactInteractionsListOutput = Readonly<{
149
+ provider: "gmail";
150
+ operation: "contacts.list";
151
+ contactCollection: "interactions";
152
+ accountSubject: string;
153
+ accountAddresses: readonly string[];
154
+ after: string | null;
155
+ before: string;
156
+ interactions: readonly GmailContactInteraction[];
157
+ messageKeys: readonly string[];
158
+ threadKeys: readonly string[];
159
+ messagesScanned: number;
160
+ messagesIncluded: number;
161
+ messagesSkipped: Readonly<{
162
+ draftOrChat: number;
163
+ missingInternalDate: number;
164
+ noExternalAddress: number;
165
+ outsideWindow: number;
166
+ }>;
167
+ nextCursor: string | null;
168
+ resultSizeEstimate: number;
169
+ scanScope: "messages-in-half-open-window-excluding-spam-trash-drafts-chats";
170
+ }>;
171
+
172
+ function fail(label: string, message: string): never {
173
+ throw new Error(`official Gmail ${label} ${message}`);
174
+ }
175
+
176
+ function record(value: unknown, label: string): JsonRecord {
177
+ if (
178
+ nodeTypes.isProxy(value)
179
+ || typeof value !== "object"
180
+ || value === null
181
+ || Array.isArray(value)
182
+ || (
183
+ Object.getPrototypeOf(value) !== Object.prototype
184
+ && Object.getPrototypeOf(value) !== null
185
+ )
186
+ ) return fail(label, "must be a plain object");
187
+ const descriptors = Object.getOwnPropertyDescriptors(value);
188
+ for (const key of Reflect.ownKeys(descriptors)) {
189
+ if (typeof key !== "string") return fail(label, "must not contain symbol properties");
190
+ const descriptor = descriptors[key];
191
+ if (
192
+ descriptor === undefined
193
+ || !("value" in descriptor)
194
+ || !descriptor.enumerable
195
+ ) return fail(`${label}.${key}`, "must be an enumerable data property");
196
+ }
197
+ return value as JsonRecord;
198
+ }
199
+
200
+ function exactKeys(
201
+ value: JsonRecord,
202
+ required: readonly string[],
203
+ optional: readonly string[],
204
+ label: string,
205
+ ): void {
206
+ const allowed = new Set([...required, ...optional]);
207
+ for (const key of Object.keys(value)) {
208
+ if (!allowed.has(key)) return fail(label, `contains unreviewed property ${key}`);
209
+ }
210
+ for (const key of required) {
211
+ if (!Object.hasOwn(value, key)) return fail(`${label}.${key}`, "is required");
212
+ }
213
+ }
214
+
215
+ function hasUnsafeControl(value: string): boolean {
216
+ for (let index = 0; index < value.length; index += 1) {
217
+ const code = value.charCodeAt(index);
218
+ if (
219
+ code <= 0x1f
220
+ || code === 0x7f
221
+ || (code >= 0x80 && code <= 0x9f)
222
+ || code === 0x061c
223
+ || code === 0x200e
224
+ || code === 0x200f
225
+ || (code >= 0x2028 && code <= 0x202e)
226
+ || (code >= 0x2066 && code <= 0x2069)
227
+ ) return true;
228
+ }
229
+ return false;
230
+ }
231
+
232
+ function isWellFormedText(value: string): boolean {
233
+ for (let index = 0; index < value.length; index += 1) {
234
+ const code = value.charCodeAt(index);
235
+ if (code >= 0xd800 && code <= 0xdbff) {
236
+ const next = value.charCodeAt(index + 1);
237
+ if (!(next >= 0xdc00 && next <= 0xdfff)) return false;
238
+ index += 1;
239
+ } else if (code >= 0xdc00 && code <= 0xdfff) return false;
240
+ }
241
+ return true;
242
+ }
243
+
244
+ function boundedInputText(value: unknown, label: string, maximum: number): string {
245
+ if (
246
+ typeof value !== "string"
247
+ || value.length === 0
248
+ || value.length > maximum
249
+ || !isWellFormedText(value)
250
+ || hasUnsafeControl(value)
251
+ ) return fail(label, `must be non-empty text of at most ${maximum} code units without unsafe controls`);
252
+ return value;
253
+ }
254
+
255
+ function optionalInputText(value: unknown, label: string, maximum: number): string | null {
256
+ return value === undefined ? null : boundedInputText(value, label, maximum);
257
+ }
258
+
259
+ function inputInteger(
260
+ value: unknown,
261
+ label: string,
262
+ fallback: number,
263
+ minimum: number,
264
+ maximum: number,
265
+ ): number {
266
+ const candidate = value === undefined ? fallback : value;
267
+ if (
268
+ typeof candidate !== "number"
269
+ || !Number.isSafeInteger(candidate)
270
+ || candidate < minimum
271
+ || candidate > maximum
272
+ ) return fail(label, `must be an integer from ${minimum} through ${maximum}`);
273
+ return candidate;
274
+ }
275
+
276
+ function parseContactsInput(
277
+ input: OperationInput,
278
+ contractVersion: number,
279
+ ): ParsedContactsInput {
280
+ const source = record(input, "contacts.list input");
281
+ if (contractVersion !== 4 && contractVersion !== 5) {
282
+ return fail("contacts.list contract", "must be version 4 or 5");
283
+ }
284
+ if (contractVersion === 4 && source.include_dates !== undefined) {
285
+ return fail(
286
+ "contacts.list input.include_dates",
287
+ "is available only in contract v5",
288
+ );
289
+ }
290
+ exactKeys(source, [], [
291
+ "collection",
292
+ "cursor",
293
+ ...(contractVersion === 5 ? ["include_dates"] : []),
294
+ "include_stats",
295
+ "limit",
296
+ "stats_scan_limit",
297
+ ], "contacts.list input");
298
+ const collection = source.collection === undefined ? "contacts" : source.collection;
299
+ if (collection !== "contacts" && collection !== "other-contacts") {
300
+ return fail(
301
+ "contacts.list input.collection",
302
+ "must be contacts or other-contacts",
303
+ );
304
+ }
305
+ const includeDates = contractVersion === 5 && source.include_dates !== undefined
306
+ ? source.include_dates
307
+ : false;
308
+ if (typeof includeDates !== "boolean") {
309
+ return fail("contacts.list input.include_dates", "must be boolean");
310
+ }
311
+ if (includeDates && collection !== "contacts") {
312
+ return fail(
313
+ "contacts.list input.include_dates",
314
+ "is supported only for saved contacts",
315
+ );
316
+ }
317
+ const includeStats = source.include_stats === undefined
318
+ ? true
319
+ : source.include_stats;
320
+ if (typeof includeStats !== "boolean") {
321
+ return fail("contacts.list input.include_stats", "must be boolean");
322
+ }
323
+ if (!includeStats && source.stats_scan_limit !== undefined) {
324
+ return fail(
325
+ "contacts.list input.stats_scan_limit",
326
+ "is accepted only when include_stats is true",
327
+ );
328
+ }
329
+ const limit = inputInteger(
330
+ source.limit,
331
+ "contacts.list input.limit",
332
+ CONTACTS_DEFAULT_LIMIT,
333
+ 1,
334
+ CONTACTS_MAX_LIMIT,
335
+ );
336
+ const statsScanLimit = inputInteger(
337
+ source.stats_scan_limit,
338
+ "contacts.list input.stats_scan_limit",
339
+ CONTACTS_DEFAULT_STATS_SCAN_LIMIT,
340
+ 1,
341
+ CONTACTS_MAX_STATS_SCAN_LIMIT,
342
+ );
343
+ if (
344
+ includeStats
345
+ && limit * statsScanLimit > CONTACT_STATS_MAX_DIRECTION_PRODUCT
346
+ ) {
347
+ return fail(
348
+ "contacts.list input",
349
+ `limit multiplied by stats_scan_limit must not exceed the ${CONTACT_STATS_MAX_DIRECTION_PRODUCT}-entry per-direction work budget`,
350
+ );
351
+ }
352
+ return Object.freeze({
353
+ collection,
354
+ cursor: optionalInputText(source.cursor, "contacts.list input.cursor", 4_096),
355
+ projection: includeDates ? "dates" : "core",
356
+ includeStats,
357
+ limit,
358
+ statsScanLimit,
359
+ });
360
+ }
361
+
362
+ function parseInteractionsInput(input: OperationInput): ParsedInteractionsInput {
363
+ const source = record(input, "contacts.list interactions input");
364
+ exactKeys(
365
+ source,
366
+ ["collection", "before"],
367
+ ["after", "cursor", "limit"],
368
+ "contacts.list interactions input",
369
+ );
370
+ if (source.collection !== "interactions") {
371
+ return fail("contacts.list interactions input.collection", "must be interactions");
372
+ }
373
+ const before = boundedInputText(
374
+ source.before,
375
+ "contacts.list interactions input.before",
376
+ 24,
377
+ );
378
+ const parsed = new Date(before);
379
+ if (
380
+ !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000Z$/u.test(before)
381
+ || !Number.isFinite(parsed.getTime())
382
+ || parsed.toISOString() !== before
383
+ ) {
384
+ return fail(
385
+ "contacts.list interactions input.before",
386
+ "must be a canonical whole-second UTC timestamp",
387
+ );
388
+ }
389
+ const after = source.after === undefined
390
+ ? null
391
+ : boundedInputText(source.after, "contacts.list interactions input.after", 24);
392
+ const parsedAfter = after === null ? null : new Date(after);
393
+ if (
394
+ after !== null
395
+ && (
396
+ !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.000Z$/u.test(after)
397
+ || parsedAfter === null
398
+ || !Number.isFinite(parsedAfter.getTime())
399
+ || parsedAfter.toISOString() !== after
400
+ || parsedAfter.getTime() >= parsed.getTime()
401
+ )
402
+ ) {
403
+ return fail(
404
+ "contacts.list interactions input.after",
405
+ "must be a canonical whole-second UTC timestamp before input.before",
406
+ );
407
+ }
408
+ return Object.freeze({
409
+ after,
410
+ afterMilliseconds: parsedAfter?.getTime() ?? null,
411
+ before,
412
+ beforeMilliseconds: parsed.getTime(),
413
+ cursor: optionalInputText(
414
+ source.cursor,
415
+ "contacts.list interactions input.cursor",
416
+ 4_096,
417
+ ),
418
+ limit: inputInteger(
419
+ source.limit,
420
+ "contacts.list interactions input.limit",
421
+ INTERACTIONS_DEFAULT_LIMIT,
422
+ 1,
423
+ INTERACTIONS_MAX_LIMIT,
424
+ ),
425
+ });
426
+ }
427
+
428
+ function parseMessagingListInput(input: OperationInput): ParsedMessagingListInput {
429
+ const source = record(input, "messaging.list input");
430
+ exactKeys(source, ["view"], [
431
+ "query",
432
+ "cursor",
433
+ "limit",
434
+ "include_spam_trash",
435
+ ], "messaging.list input");
436
+ if (source.view !== "inbox" && source.view !== "search") {
437
+ return fail("messaging.list input.view", "must be inbox or search");
438
+ }
439
+ const query = optionalInputText(source.query, "messaging.list input.query", 512);
440
+ if (source.view === "search" && query === null) {
441
+ return fail("messaging.list input.query", "is required when view is search");
442
+ }
443
+ if (source.view === "inbox" && query !== null) {
444
+ return fail("messaging.list input.query", "is not accepted when view is inbox");
445
+ }
446
+ if (query !== null && query.trim().length === 0) {
447
+ return fail("messaging.list input.query", "must contain a non-whitespace expression");
448
+ }
449
+ const includeSpamTrash = source.include_spam_trash === undefined
450
+ ? false
451
+ : source.include_spam_trash;
452
+ if (typeof includeSpamTrash !== "boolean") {
453
+ return fail("messaging.list input.include_spam_trash", "must be boolean");
454
+ }
455
+ if (source.view === "inbox" && source.include_spam_trash !== undefined) {
456
+ return fail(
457
+ "messaging.list input.include_spam_trash",
458
+ "is accepted only when view is search",
459
+ );
460
+ }
461
+ return Object.freeze({
462
+ view: source.view,
463
+ query,
464
+ cursor: optionalInputText(source.cursor, "messaging.list input.cursor", 4_096),
465
+ limit: inputInteger(
466
+ source.limit,
467
+ "messaging.list input.limit",
468
+ MESSAGING_DEFAULT_LIMIT,
469
+ 1,
470
+ MESSAGING_MAX_LIMIT,
471
+ ),
472
+ includeSpamTrash,
473
+ });
474
+ }
475
+
476
+ function parseMessagingReadInput(input: OperationInput): string {
477
+ const source = record(input, "messaging.read input");
478
+ exactKeys(source, ["thread_id"], [], "messaging.read input");
479
+ return parseGmailId(source.thread_id, "messaging.read input.thread_id");
480
+ }
481
+
482
+ async function authenticatedClient(context: ProviderActionContext): Promise<{
483
+ readonly client: GmailApiClient;
484
+ readonly profile: GmailProfile;
485
+ }> {
486
+ if (context.auth.subject === undefined) {
487
+ throw new Error("official Gmail reads require an OAuth locator with an exact email subject");
488
+ }
489
+ const client = createGmailApiClient({
490
+ http: context.http,
491
+ accessToken: context.token.accessToken,
492
+ subject: context.auth.subject,
493
+ });
494
+ const profile = await getAuthenticatedGmailProfile(client);
495
+ return Object.freeze({ client, profile });
496
+ }
497
+
498
+ async function mapConcurrent<T, R>(
499
+ values: readonly T[],
500
+ maximum: number,
501
+ worker: (
502
+ value: T,
503
+ index: number,
504
+ admit: ConcurrentAdmission,
505
+ ) => Promise<R>,
506
+ ): Promise<readonly R[]> {
507
+ if (!Number.isSafeInteger(maximum) || maximum < 1) {
508
+ throw new Error("Gmail concurrent mapping requires a positive worker bound");
509
+ }
510
+ const results = new Array<R>(values.length);
511
+ let nextIndex = 0;
512
+ const noFailure = Symbol("no Gmail concurrent failure");
513
+ let firstFailure: unknown = noFailure;
514
+ const rememberFailure = (error: unknown): void => {
515
+ if (firstFailure === noFailure) firstFailure = error;
516
+ };
517
+ const admit: ConcurrentAdmission = async <V>(action: () => Promise<V>): Promise<V> => {
518
+ if (firstFailure !== noFailure) throw firstFailure;
519
+ try {
520
+ return await action();
521
+ } catch (error) {
522
+ rememberFailure(error);
523
+ throw error;
524
+ }
525
+ };
526
+ const run = async (): Promise<void> => {
527
+ while (firstFailure === noFailure && nextIndex < values.length) {
528
+ const index = nextIndex;
529
+ nextIndex += 1;
530
+ const value = values[index];
531
+ if (value === undefined) {
532
+ rememberFailure(new Error("Gmail concurrent mapping lost a dense input item"));
533
+ return;
534
+ }
535
+ try {
536
+ results[index] = await worker(value, index, admit);
537
+ } catch (error) {
538
+ rememberFailure(error);
539
+ return;
540
+ }
541
+ }
542
+ };
543
+ const settlements = await Promise.allSettled(Array.from(
544
+ { length: Math.min(maximum, values.length) },
545
+ () => run(),
546
+ ));
547
+ for (const settlement of settlements) {
548
+ if (settlement.status === "rejected") rememberFailure(settlement.reason);
549
+ }
550
+ if (firstFailure !== noFailure) throw firstFailure;
551
+ return Object.freeze(results);
552
+ }
553
+
554
+ function contactEmailCoverage(contact: GmailContact): ContactEmailCoverage {
555
+ const emails = Object.freeze([...new Set(contact.emailAddresses.flatMap((email) =>
556
+ email.canonicalValue === null ? [] : [email.canonicalValue]))]);
557
+ const unsupportedAddressCount = contact.emailAddresses.reduce(
558
+ (count, email) => count + (email.canonicalValue === null ? 1 : 0),
559
+ 0,
560
+ );
561
+ return Object.freeze({
562
+ emails,
563
+ kind: contact.emailAddresses.length === 0
564
+ ? "unavailable"
565
+ : emails.length === 0
566
+ ? "unsupported"
567
+ : unsupportedAddressCount === 0
568
+ ? "complete"
569
+ : "partial",
570
+ unsupportedAddressCount,
571
+ });
572
+ }
573
+
574
+ function contactQuery(direction: ContactDirection, emails: readonly string[]): string {
575
+ const terms = direction === "received"
576
+ ? emails.map((email) => `from:"${email}"`)
577
+ : emails.flatMap((email) => [`to:"${email}"`, `cc:"${email}"`, `bcc:"${email}"`]);
578
+ const prefix = direction === "received" ? "-in:sent " : "in:sent ";
579
+ return `${prefix}{${terms.join(" ")}}`;
580
+ }
581
+
582
+ function contactQueryChunks(
583
+ direction: ContactDirection,
584
+ emails: readonly string[],
585
+ ): readonly string[] {
586
+ const chunks: string[][] = [];
587
+ let current: string[] = [];
588
+ for (const email of emails) {
589
+ const candidate = [...current, email];
590
+ if (
591
+ current.length > 0
592
+ && Buffer.byteLength(contactQuery(direction, candidate), "utf8") > CONTACT_STATS_QUERY_MAX_BYTES
593
+ ) {
594
+ chunks.push(current);
595
+ current = [email];
596
+ } else current = candidate;
597
+ if (Buffer.byteLength(contactQuery(direction, current), "utf8") > CONTACT_STATS_QUERY_MAX_BYTES) {
598
+ return fail("contacts.list stats query", "cannot represent one validated contact address within the query bound");
599
+ }
600
+ }
601
+ if (current.length > 0) chunks.push(current);
602
+ return Object.freeze(chunks.map((chunk) => contactQuery(direction, chunk)));
603
+ }
604
+
605
+ async function scanContactDirection(
606
+ client: GmailApiClient,
607
+ addressCoverage: ContactEmailCoverage,
608
+ direction: ContactDirection,
609
+ scanLimit: number,
610
+ admit: ConcurrentAdmission,
611
+ ): Promise<ContactDirectionStats> {
612
+ const queries = contactQueryChunks(direction, addressCoverage.emails);
613
+ if (queries.length === 0) {
614
+ const addressCoverageComplete = addressCoverage.kind === "complete";
615
+ const incompleteReasons: readonly GmailDirectionStatsIncompleteReason[] = addressCoverageComplete
616
+ ? Object.freeze([])
617
+ : addressCoverage.kind === "unavailable"
618
+ ? Object.freeze(["no-contact-addresses"] as const)
619
+ : Object.freeze(["unsupported-contact-addresses"] as const);
620
+ return parseContactDirectionStats({
621
+ count: 0,
622
+ complete: addressCoverageComplete,
623
+ lowerBound: !addressCoverageComplete,
624
+ truncated: false,
625
+ lastAt: null,
626
+ lastAtComplete: addressCoverageComplete,
627
+ lastAtBasis: "unavailable",
628
+ incompleteReasons,
629
+ }, "official Gmail contacts.list direction stats");
630
+ }
631
+ const messageIds = new Set<string>();
632
+ let scannedEntries = 0;
633
+ let complete = true;
634
+ for (const [queryIndex, query] of queries.entries()) {
635
+ let cursor: string | null = null;
636
+ const seenCursors = new Set<string>();
637
+ while (scannedEntries < scanLimit) {
638
+ const pageLimit = Math.min(GMAIL_LIST_PAGE_MAXIMUM, scanLimit - scannedEntries);
639
+ const page = await admit(() => fetchGmailMessageList(client, {
640
+ limit: pageLimit,
641
+ pageToken: cursor,
642
+ query,
643
+ includeSpamTrash: false,
644
+ }));
645
+ if (page.messages.length === 0 && page.nextPageToken !== null) {
646
+ return fail("contacts.list stats pagination", "returned an empty non-terminal page");
647
+ }
648
+ scannedEntries += page.messages.length;
649
+ for (const message of page.messages) messageIds.add(message.id);
650
+ if (page.nextPageToken === null) break;
651
+ if (seenCursors.has(page.nextPageToken)) {
652
+ return fail("contacts.list stats pagination", "repeated a page token");
653
+ }
654
+ seenCursors.add(page.nextPageToken);
655
+ cursor = page.nextPageToken;
656
+ if (scannedEntries >= scanLimit) {
657
+ complete = false;
658
+ break;
659
+ }
660
+ }
661
+ if (scannedEntries >= scanLimit && queryIndex < queries.length - 1) complete = false;
662
+ if (!complete) break;
663
+ }
664
+ let lastAt: string | null = null;
665
+ let metadataDatesComplete = true;
666
+ for (const messageId of messageIds) {
667
+ const message = await admit(() => fetchGmailMessageMetadata(client, messageId));
668
+ if (message.id !== messageId) {
669
+ return fail("contacts.list stats metadata", "returned a message other than the requested ID");
670
+ }
671
+ if (message.internalDate === null) metadataDatesComplete = false;
672
+ else if (lastAt === null || message.internalDate > lastAt) {
673
+ lastAt = message.internalDate;
674
+ }
675
+ }
676
+ const addressCoverageComplete = addressCoverage.kind === "complete";
677
+ const countComplete = complete && addressCoverageComplete;
678
+ const lastAtComplete = countComplete && metadataDatesComplete;
679
+ const incompleteReasons: readonly GmailDirectionStatsIncompleteReason[] = Object.freeze([
680
+ ...(addressCoverageComplete ? [] : ["unsupported-contact-addresses" as const]),
681
+ ...(complete ? [] : ["scan-limit-reached" as const]),
682
+ ...(metadataDatesComplete ? [] : ["message-internal-date-unavailable" as const]),
683
+ ]);
684
+ return parseContactDirectionStats({
685
+ count: messageIds.size,
686
+ complete: countComplete,
687
+ lowerBound: !countComplete,
688
+ truncated: !complete,
689
+ lastAt,
690
+ lastAtComplete,
691
+ lastAtBasis: lastAt === null ? "unavailable" : "bounded-matched-message-internal-date",
692
+ incompleteReasons,
693
+ }, "official Gmail contacts.list direction stats");
694
+ }
695
+
696
+ async function executeContactsList(context: ProviderActionContext): Promise<void> {
697
+ const input = parseContactsInput(
698
+ context.input,
699
+ context.recipe.contractVersion,
700
+ );
701
+ const { client, profile } = await authenticatedClient(context);
702
+ const page = await fetchGmailContacts(client, {
703
+ collection: input.collection,
704
+ projection: input.projection,
705
+ limit: input.limit,
706
+ pageToken: input.cursor,
707
+ });
708
+ const tasks = input.includeStats ? page.contacts.flatMap((contact) => {
709
+ const addressCoverage = contactEmailCoverage(contact);
710
+ return (["sent", "received"] as const).map((direction) => Object.freeze({
711
+ addressCoverage,
712
+ resourceName: contact.resourceName,
713
+ direction,
714
+ }));
715
+ }) : [];
716
+ const stats = await mapConcurrent(
717
+ tasks,
718
+ MAX_CONCURRENT_GMAIL_READS,
719
+ async (task, _index, admit) => Object.freeze({
720
+ resourceName: task.resourceName,
721
+ direction: task.direction,
722
+ stats: await scanContactDirection(
723
+ client,
724
+ task.addressCoverage,
725
+ task.direction,
726
+ input.statsScanLimit,
727
+ admit,
728
+ ),
729
+ }),
730
+ );
731
+ const byContact = new Map<string, Partial<Record<ContactDirection, ContactDirectionStats>>>();
732
+ for (const entry of stats) {
733
+ const current = byContact.get(entry.resourceName) ?? {};
734
+ current[entry.direction] = entry.stats;
735
+ byContact.set(entry.resourceName, current);
736
+ }
737
+ const contacts = page.contacts.map((contact) => {
738
+ if (!input.includeStats) return contact;
739
+ const values = byContact.get(contact.resourceName);
740
+ if (values?.sent === undefined || values.received === undefined) {
741
+ return fail("contacts.list stats", "did not settle both directions for one contact");
742
+ }
743
+ const addressCoverage = contactEmailCoverage(contact);
744
+ return Object.freeze({
745
+ ...contact,
746
+ ...projectContactDirectionStats(values.sent, values.received),
747
+ statsAddressCoverage: addressCoverage.kind,
748
+ statsSupportedAddressCount: addressCoverage.emails.length,
749
+ statsUnsupportedAddressCount: addressCoverage.unsupportedAddressCount,
750
+ });
751
+ });
752
+ context.setOutput(Object.freeze({
753
+ provider: "gmail",
754
+ operation: "contacts.list",
755
+ accountSubject: profile.emailAddress,
756
+ contactCollection: input.collection,
757
+ statsIncluded: input.includeStats,
758
+ contacts: Object.freeze(contacts),
759
+ nextCursor: page.nextPageToken,
760
+ totalItems: page.totalItems,
761
+ statsScanLimit: input.includeStats ? input.statsScanLimit : null,
762
+ statsScope: input.includeStats
763
+ ? "per-contact-gmail-search-excluding-spam-trash"
764
+ : "not-requested",
765
+ }));
766
+ }
767
+
768
+ type MutableInteractionDirection = {
769
+ count: number;
770
+ complete: boolean;
771
+ firstAt: string | null;
772
+ lastAt: string | null;
773
+ count30d: number;
774
+ count90d: number;
775
+ count365d: number;
776
+ };
777
+
778
+ type MutableContactInteraction = {
779
+ sent: MutableInteractionDirection;
780
+ received: MutableInteractionDirection;
781
+ };
782
+
783
+ function newInteractionDirection(): MutableInteractionDirection {
784
+ return {
785
+ count: 0,
786
+ complete: true,
787
+ firstAt: null,
788
+ lastAt: null,
789
+ count30d: 0,
790
+ count90d: 0,
791
+ count365d: 0,
792
+ };
793
+ }
794
+
795
+ function interactionFor(
796
+ interactions: Map<string, MutableContactInteraction>,
797
+ email: string,
798
+ ): MutableContactInteraction {
799
+ const present = interactions.get(email);
800
+ if (present !== undefined) return present;
801
+ if (interactions.size >= INTERACTIONS_MAX_ADDRESSES) {
802
+ return fail(
803
+ "contacts.list interactions response",
804
+ `contains more than ${INTERACTIONS_MAX_ADDRESSES} external addresses in one page`,
805
+ );
806
+ }
807
+ const created = {
808
+ sent: newInteractionDirection(),
809
+ received: newInteractionDirection(),
810
+ };
811
+ interactions.set(email, created);
812
+ return created;
813
+ }
814
+
815
+ function messageInteractionAddresses(
816
+ message: GmailThread["messages"][number],
817
+ accountAddresses: ReadonlySet<string>,
818
+ direction: ContactDirection,
819
+ ): readonly string[] {
820
+ const headers = direction === "sent"
821
+ ? [message.to, message.cc, message.bcc]
822
+ : [message.from];
823
+ return Object.freeze([...new Set(headers.flatMap((header) =>
824
+ extractGmailEmailAddresses(header)))].filter((email) => !accountAddresses.has(email)));
825
+ }
826
+
827
+ function recordDatedInteraction(
828
+ direction: MutableInteractionDirection,
829
+ timestamp: string,
830
+ beforeMilliseconds: number,
831
+ ): void {
832
+ const milliseconds = new Date(timestamp).getTime();
833
+ direction.count += 1;
834
+ if (direction.firstAt === null || timestamp < direction.firstAt) direction.firstAt = timestamp;
835
+ if (direction.lastAt === null || timestamp > direction.lastAt) direction.lastAt = timestamp;
836
+ const age = beforeMilliseconds - milliseconds;
837
+ if (age <= 30 * 86_400_000) direction.count30d += 1;
838
+ if (age <= 90 * 86_400_000) direction.count90d += 1;
839
+ if (age <= 365 * 86_400_000) direction.count365d += 1;
840
+ }
841
+
842
+ function opaqueGmailKey(kind: "message" | "thread", id: string): string {
843
+ return createHash("sha256").update(`gmail-${kind}\0${id}`, "utf8").digest("hex");
844
+ }
845
+
846
+ function projectInteraction(
847
+ email: string,
848
+ interaction: MutableContactInteraction,
849
+ ): GmailContactInteraction {
850
+ return Object.freeze({
851
+ email,
852
+ sentCount: interaction.sent.count,
853
+ receivedCount: interaction.received.count,
854
+ sentCountComplete: interaction.sent.complete,
855
+ receivedCountComplete: interaction.received.complete,
856
+ firstSentAt: interaction.sent.firstAt,
857
+ lastSentAt: interaction.sent.lastAt,
858
+ firstReceivedAt: interaction.received.firstAt,
859
+ lastReceivedAt: interaction.received.lastAt,
860
+ sent30d: interaction.sent.count30d,
861
+ sent90d: interaction.sent.count90d,
862
+ sent365d: interaction.sent.count365d,
863
+ received30d: interaction.received.count30d,
864
+ received90d: interaction.received.count90d,
865
+ received365d: interaction.received.count365d,
866
+ });
867
+ }
868
+
869
+ async function executeContactInteractionsList(
870
+ context: ProviderActionContext,
871
+ ): Promise<void> {
872
+ const input = parseInteractionsInput(context.input);
873
+ const { client, profile } = await authenticatedClient(context);
874
+ const accountAddresses = input.cursor === null
875
+ ? await listGmailSendAsAliases(client)
876
+ : Object.freeze([] as string[]);
877
+ const page = await fetchGmailMessageList(client, {
878
+ limit: input.limit,
879
+ pageToken: input.cursor,
880
+ query: input.afterMilliseconds === null
881
+ ? `before:${String(input.beforeMilliseconds / 1_000)}`
882
+ : `after:${String(input.afterMilliseconds / 1_000 - 1)} before:${String(input.beforeMilliseconds / 1_000)}`,
883
+ includeSpamTrash: false,
884
+ });
885
+ const messages = await mapConcurrent(
886
+ page.messages,
887
+ MAX_CONCURRENT_GMAIL_READS,
888
+ async (stub, _index, admit) => {
889
+ const message = await admit(() =>
890
+ fetchGmailMessageInteractionMetadata(client, stub.id));
891
+ if (message.id !== stub.id) {
892
+ return fail(
893
+ "contacts.list interactions metadata",
894
+ "returned a message other than the requested ID",
895
+ );
896
+ }
897
+ if (
898
+ stub.threadId !== null
899
+ && message.threadId !== null
900
+ && stub.threadId !== message.threadId
901
+ ) {
902
+ return fail(
903
+ "contacts.list interactions metadata",
904
+ "returned a message bound to a different thread",
905
+ );
906
+ }
907
+ return message;
908
+ },
909
+ );
910
+ const accountSubject = profile.emailAddress.toLowerCase();
911
+ const selfAddresses = new Set([accountSubject, ...accountAddresses]);
912
+ const interactions = new Map<string, MutableContactInteraction>();
913
+ const threadKeys = new Set<string>();
914
+ let messagesIncluded = 0;
915
+ let draftOrChat = 0;
916
+ let missingInternalDate = 0;
917
+ let noExternalAddress = 0;
918
+ let outsideWindow = 0;
919
+ for (const message of messages) {
920
+ if (message.threadId !== null) {
921
+ threadKeys.add(opaqueGmailKey("thread", message.threadId));
922
+ }
923
+ if (message.labelIds.includes("DRAFT") || message.labelIds.includes("CHAT")) {
924
+ draftOrChat += 1;
925
+ continue;
926
+ }
927
+ const direction: ContactDirection = message.labelIds.includes("SENT")
928
+ ? "sent"
929
+ : "received";
930
+ const addresses = messageInteractionAddresses(message, selfAddresses, direction);
931
+ if (addresses.length === 0) {
932
+ noExternalAddress += 1;
933
+ continue;
934
+ }
935
+ if (message.internalDate === null) {
936
+ missingInternalDate += 1;
937
+ for (const email of addresses) {
938
+ interactionFor(interactions, email)[direction].complete = false;
939
+ }
940
+ continue;
941
+ }
942
+ if (
943
+ input.afterMilliseconds !== null
944
+ && new Date(message.internalDate).getTime() < input.afterMilliseconds
945
+ ) {
946
+ outsideWindow += 1;
947
+ continue;
948
+ }
949
+ if (new Date(message.internalDate).getTime() >= input.beforeMilliseconds) {
950
+ return fail(
951
+ "contacts.list interactions metadata",
952
+ "returned a message outside the requested before cutoff",
953
+ );
954
+ }
955
+ messagesIncluded += 1;
956
+ for (const email of addresses) {
957
+ recordDatedInteraction(
958
+ interactionFor(interactions, email)[direction],
959
+ message.internalDate,
960
+ input.beforeMilliseconds,
961
+ );
962
+ }
963
+ }
964
+ const output: GmailContactInteractionsListOutput = Object.freeze({
965
+ provider: "gmail",
966
+ operation: "contacts.list",
967
+ contactCollection: "interactions",
968
+ accountSubject,
969
+ accountAddresses,
970
+ after: input.after,
971
+ before: input.before,
972
+ interactions: Object.freeze([...interactions.entries()]
973
+ .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
974
+ .map(([email, interaction]) => projectInteraction(email, interaction))),
975
+ messageKeys: Object.freeze(page.messages.map((message) =>
976
+ opaqueGmailKey("message", message.id))),
977
+ threadKeys: Object.freeze([...threadKeys].sort()),
978
+ messagesScanned: messages.length,
979
+ messagesIncluded,
980
+ messagesSkipped: Object.freeze({
981
+ draftOrChat,
982
+ missingInternalDate,
983
+ noExternalAddress,
984
+ outsideWindow,
985
+ }),
986
+ nextCursor: page.nextPageToken,
987
+ resultSizeEstimate: page.resultSizeEstimate,
988
+ scanScope: "messages-in-half-open-window-excluding-spam-trash-drafts-chats",
989
+ });
990
+ context.setOutput(output);
991
+ }
992
+
993
+ function threadOrderedAt(thread: GmailThread): string | null {
994
+ let result: string | null = null;
995
+ for (const message of thread.messages) {
996
+ if (message.internalDate !== null && (result === null || message.internalDate > result)) {
997
+ result = message.internalDate;
998
+ }
999
+ }
1000
+ return result;
1001
+ }
1002
+
1003
+ function threadParticipants(
1004
+ thread: GmailThread,
1005
+ accountSubject: string,
1006
+ ): GmailThreadSummary["participants"] {
1007
+ const account = accountSubject.toLowerCase();
1008
+ const emails = new Set<string>();
1009
+ for (const message of thread.messages) {
1010
+ for (const header of [message.from, message.to, message.cc, message.bcc]) {
1011
+ for (const email of extractGmailEmailAddresses(header)) {
1012
+ if (email !== account) emails.add(email);
1013
+ }
1014
+ }
1015
+ }
1016
+ return Object.freeze([...emails].sort().map((email) => Object.freeze({
1017
+ email,
1018
+ displayName: null,
1019
+ })));
1020
+ }
1021
+
1022
+ function summarizeThread(
1023
+ thread: GmailThread,
1024
+ accountSubject: string,
1025
+ view: GmailListView,
1026
+ ): GmailThreadSummary {
1027
+ const labels = new Set(thread.messages.flatMap((message) => message.labelIds));
1028
+ const archived = labels.has("INBOX")
1029
+ ? false
1030
+ : labels.has("SPAM") || labels.has("TRASH")
1031
+ ? null
1032
+ : true;
1033
+ return Object.freeze({
1034
+ id: thread.id,
1035
+ historyId: thread.historyId,
1036
+ snippet: thread.snippet,
1037
+ subject: thread.messages.find((message) => message.subject !== null)?.subject ?? null,
1038
+ orderedAt: threadOrderedAt(thread),
1039
+ messageCount: thread.messages.length,
1040
+ participants: threadParticipants(thread, accountSubject),
1041
+ unread: labels.has("UNREAD"),
1042
+ archived,
1043
+ threadUrl: buildGmailThreadUrl(
1044
+ accountSubject,
1045
+ thread.id,
1046
+ view === "inbox" ? "inbox" : "all",
1047
+ ),
1048
+ readInput: Object.freeze({ thread_id: thread.id }),
1049
+ });
1050
+ }
1051
+
1052
+ async function executeMessagingList(context: ProviderActionContext): Promise<void> {
1053
+ const input = parseMessagingListInput(context.input);
1054
+ const { client, profile } = await authenticatedClient(context);
1055
+ const page = await fetchGmailThreadList(client, {
1056
+ limit: input.limit,
1057
+ pageToken: input.cursor,
1058
+ query: input.query,
1059
+ labelIds: input.view === "inbox" ? ["INBOX"] : [],
1060
+ includeSpamTrash: input.includeSpamTrash,
1061
+ });
1062
+ const threads = await mapConcurrent(
1063
+ page.threads,
1064
+ MAX_CONCURRENT_GMAIL_READS,
1065
+ async (stub, _index, admit) => {
1066
+ const thread = parseGmailThread(
1067
+ await admit(() => fetchGmailThreadMetadata(client, stub.id)),
1068
+ { deadlineCheckpoint: () => client.http.throwIfUnavailable() },
1069
+ );
1070
+ if (thread.id !== stub.id) {
1071
+ return fail("messaging.list metadata", "returned a thread other than the requested ID");
1072
+ }
1073
+ return summarizeThread(thread, profile.emailAddress, input.view);
1074
+ },
1075
+ );
1076
+ const output: GmailMessagingListOutput = Object.freeze({
1077
+ provider: "gmail",
1078
+ operation: "messaging.list",
1079
+ accountSubject: profile.emailAddress,
1080
+ view: input.view,
1081
+ query: input.query,
1082
+ includeSpamTrash: input.includeSpamTrash,
1083
+ threads,
1084
+ nextCursor: page.nextPageToken,
1085
+ resultSizeEstimate: page.resultSizeEstimate,
1086
+ });
1087
+ context.setOutput(output);
1088
+ }
1089
+
1090
+ async function executeMessagingRead(context: ProviderActionContext): Promise<void> {
1091
+ const threadId = parseMessagingReadInput(context.input);
1092
+ const { client, profile } = await authenticatedClient(context);
1093
+ const parsedThread = parseGmailThread(
1094
+ await fetchGmailThread(client, threadId),
1095
+ {
1096
+ maxBodyBytes: GMAIL_MESSAGING_READ_BODY_BYTES,
1097
+ deadlineCheckpoint: () => client.http.throwIfUnavailable(),
1098
+ },
1099
+ );
1100
+ const thread = await resolveGmailThreadBodies(client, parsedThread);
1101
+ if (thread.id !== threadId) {
1102
+ return fail("messaging.read response", "returned a thread other than the requested ID");
1103
+ }
1104
+ if (thread.messages.length === 0) {
1105
+ return fail("messaging.read response.messages", "must contain at least one message");
1106
+ }
1107
+ const threadUrl = buildGmailThreadUrl(profile.emailAddress, thread.id);
1108
+ const output: GmailMessagingReadOutput = Object.freeze({
1109
+ provider: "gmail",
1110
+ operation: "messaging.read",
1111
+ accountSubject: profile.emailAddress,
1112
+ thread,
1113
+ threadUrl,
1114
+ });
1115
+ context.setOutput(output);
1116
+ context.setFinalUrl(threadUrl);
1117
+ }
1118
+
1119
+ /** Execute one reviewed official Gmail or People read without acknowledgement mutation. */
1120
+ export async function executeGmailProvider(context: ProviderActionContext): Promise<void> {
1121
+ const action = context.recipe.action;
1122
+ if (action === "contacts.list") {
1123
+ const source = record(context.input, "contacts.list input");
1124
+ if (source.collection === "interactions") await executeContactInteractionsList(context);
1125
+ else await executeContactsList(context);
1126
+ }
1127
+ else if (action === "messaging.list") await executeMessagingList(context);
1128
+ else if (action === "messaging.read") await executeMessagingRead(context);
1129
+ else throw new Error(`official Gmail provider does not implement ${action}`);
1130
+ }