@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,1244 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ chmodSync,
4
+ closeSync,
5
+ constants,
6
+ existsSync,
7
+ fstatSync,
8
+ lstatSync,
9
+ mkdirSync,
10
+ openSync,
11
+ readSync,
12
+ readdirSync,
13
+ writeFileSync,
14
+ } from "node:fs";
15
+ import { join } from "node:path";
16
+
17
+ import type { CaptureArguments } from "@hraness/kb/capture";
18
+ import {
19
+ abortCaptureBundle,
20
+ beginCaptureBundle,
21
+ commitCaptureBundle,
22
+ redactSensitiveText,
23
+ sanitizeArtifactUrl,
24
+ type CaptureBundleTransaction,
25
+ type CaptureManifestAsset,
26
+ type CaptureManifestInput,
27
+ writeCaptureBundle,
28
+ } from "@hraness/kb/clip/persist";
29
+ import {
30
+ sanitizeTerminalLine,
31
+ sanitizeTerminalText,
32
+ } from "@hraness/kb/clip/terminal";
33
+ import {
34
+ type GhostgetAuth,
35
+ } from "./auth";
36
+ import {
37
+ ProviderHttpClient,
38
+ requireOAuthScopes,
39
+ type ProviderFetch,
40
+ } from "./provider-http";
41
+ import { resolveOAuthToken } from "./oauth-google";
42
+ import {
43
+ buildGmailThreadUrl,
44
+ createGmailApiClient,
45
+ fetchGmailThread,
46
+ getAuthenticatedGmailProfile,
47
+ parseGmailThread,
48
+ parseGmailThreadUrl,
49
+ resolveGmailAttachmentBytes,
50
+ resolveGmailThreadBodies,
51
+ type GmailApiClient,
52
+ type GmailAttachment,
53
+ } from "./providers/gmail-api";
54
+
55
+ const GMAIL_READ_SCOPE_ALTERNATIVES = [
56
+ ["https://www.googleapis.com/auth/gmail.readonly"],
57
+ ["https://www.googleapis.com/auth/gmail.modify"],
58
+ ["https://mail.google.com/"],
59
+ ] as const;
60
+ const MAX_GMAIL_ATTACHMENT_FILES = 500;
61
+ const MAX_PUBLIC_DIAGNOSTIC_CODE_UNITS = 1_000;
62
+ const MAX_INLINE_FIELD_CODE_UNITS = 8_192;
63
+ const MAX_ATTACHMENT_LABEL_CODE_UNITS = 1_024;
64
+ const FILE_VERIFY_CHUNK_BYTES = 1024 * 1024;
65
+ const GMAIL_PROFILE_RESPONSE_MAX_BYTES = 64 * 1024;
66
+ const GMAIL_RESPONSE_ENVELOPE_MAX_BYTES = 32 * 1024;
67
+ const GMAIL_THREAD_STRUCTURE_MAX_BYTES = 8 * 1024 * 1024;
68
+ const GMAIL_INLINE_ATTACHMENT_MAX_BYTES = 32 * 1024 * 1024;
69
+ const GMAIL_BODY_MAX_BYTES = 64 * 1024 * 1024;
70
+ const GMAIL_ATTACHMENT_MAX_BYTES = 100 * 1024 * 1024;
71
+ const GMAIL_PROVENANCE_FILENAME = "gmail.json";
72
+ const GMAIL_STORED_ATTACHMENT_EXTENSION = "bin";
73
+ const GMAIL_STORED_ATTACHMENT_MIME_TYPE = "application/octet-stream";
74
+
75
+ type GmailOAuthAuth = Extract<
76
+ GhostgetAuth,
77
+ { readonly kind: "oauth-token-file" }
78
+ > & {
79
+ readonly provider: "gmail";
80
+ readonly subject: string;
81
+ };
82
+
83
+ type GmailCaptureOutput = {
84
+ readonly stdout: (value: string) => void;
85
+ readonly stderr: (value: string) => void;
86
+ };
87
+
88
+ type GmailProfileProjection = {
89
+ readonly emailAddress: string;
90
+ };
91
+
92
+ type GmailAttachmentProjection = {
93
+ readonly providerAttachment: GmailAttachment;
94
+ readonly partId: string;
95
+ readonly contentDisposition: "attachment" | "inline" | null;
96
+ readonly messageId: string;
97
+ readonly attachmentId: string | null;
98
+ readonly filename: string;
99
+ readonly mimeType: string;
100
+ readonly size: number;
101
+ };
102
+
103
+ type GmailMessageProjection = {
104
+ readonly id: string;
105
+ readonly historyId: string | null;
106
+ readonly internalDate: string | null;
107
+ readonly from: string | null;
108
+ readonly to: string | null;
109
+ readonly cc: string | null;
110
+ readonly bcc: string | null;
111
+ readonly subject: string | null;
112
+ readonly date: string | null;
113
+ readonly messageId: string | null;
114
+ readonly inReplyTo: string | null;
115
+ readonly body: string;
116
+ readonly attachments: readonly GmailAttachmentProjection[];
117
+ };
118
+
119
+ type GmailThreadProjection = {
120
+ readonly id: string;
121
+ readonly historyId: string | null;
122
+ readonly messages: readonly GmailMessageProjection[];
123
+ };
124
+
125
+ type StagedAttachment = GmailAttachmentProjection & {
126
+ readonly path: string;
127
+ readonly sha256: string;
128
+ readonly bytes: number;
129
+ };
130
+
131
+ type GmailCaptureOutcome = {
132
+ readonly slug: string;
133
+ readonly markdown: string;
134
+ readonly messageCount: number;
135
+ readonly discoveredAttachmentCount: number;
136
+ readonly capturedAttachmentCount: number;
137
+ readonly capturedFileCount: number;
138
+ readonly capturedBytes: number;
139
+ readonly warnings: readonly string[];
140
+ };
141
+
142
+ export type GmailCaptureDependencies = {
143
+ readonly fetch?: ProviderFetch;
144
+ readonly httpClient?: ProviderHttpClient;
145
+ readonly now?: () => Date;
146
+ readonly createClient?: (input: {
147
+ readonly http: ProviderHttpClient;
148
+ readonly accessToken: string;
149
+ readonly subject: string;
150
+ }) => GmailApiClient;
151
+ readonly getProfile?: (
152
+ client: GmailApiClient,
153
+ maximumResponseBytes?: number,
154
+ ) => Promise<GmailProfileProjection>;
155
+ readonly parseThreadUrl?: typeof parseGmailThreadUrl;
156
+ readonly buildThreadUrl?: typeof buildGmailThreadUrl;
157
+ readonly fetchThread?: (
158
+ client: GmailApiClient,
159
+ threadId: string,
160
+ maximumResponseBytes?: number,
161
+ ) => Promise<unknown>;
162
+ readonly parseThread?: (value: unknown) => unknown;
163
+ readonly resolveThreadBodies?: typeof resolveGmailThreadBodies;
164
+ readonly resolveAttachment?: (
165
+ client: GmailApiClient,
166
+ attachment: GmailAttachment,
167
+ maximumResponseBytes?: number,
168
+ ) => Promise<Uint8Array>;
169
+ };
170
+
171
+ export type GmailCaptureRunner = (
172
+ options: CaptureArguments,
173
+ auth: GhostgetAuth,
174
+ environment: Readonly<Record<string, string | undefined>>,
175
+ output: GmailCaptureOutput,
176
+ dependencies?: GmailCaptureDependencies,
177
+ ) => Promise<number>;
178
+
179
+ class PublicGmailCaptureError extends Error {}
180
+
181
+ function publicFailure(message: string, options?: ErrorOptions): PublicGmailCaptureError {
182
+ return new PublicGmailCaptureError(message, options);
183
+ }
184
+
185
+ function isRecord(value: unknown): value is Record<string, unknown> {
186
+ return typeof value === "object" && value !== null && !Array.isArray(value);
187
+ }
188
+
189
+ function boundedString(
190
+ value: unknown,
191
+ label: string,
192
+ maximum: number,
193
+ allowEmpty = false,
194
+ ): string {
195
+ if (
196
+ typeof value !== "string"
197
+ || (!allowEmpty && value.length === 0)
198
+ || value.length > maximum
199
+ ) throw new Error(`Gmail ${label} is invalid`);
200
+ return value;
201
+ }
202
+
203
+ function nullableString(
204
+ value: unknown,
205
+ label: string,
206
+ maximum = MAX_INLINE_FIELD_CODE_UNITS,
207
+ ): string | null {
208
+ if (value === null || value === undefined) return null;
209
+ return boundedString(value, label, maximum, true);
210
+ }
211
+
212
+ function safeInteger(value: unknown, label: string): number {
213
+ if (!Number.isSafeInteger(value) || (value as number) < 0) {
214
+ throw new Error(`Gmail ${label} is invalid`);
215
+ }
216
+ return value as number;
217
+ }
218
+
219
+ function requireGmailAuth(auth: GhostgetAuth): asserts auth is GmailOAuthAuth {
220
+ if (auth.kind !== "oauth-token-file" || auth.provider !== "gmail") {
221
+ throw publicFailure("Gmail thread capture requires a stored Gmail OAuth auth locator");
222
+ }
223
+ if (auth.subject === undefined) {
224
+ throw publicFailure("Gmail thread capture requires an auth locator bound to an account subject");
225
+ }
226
+ try {
227
+ requireOAuthScopes(auth, GMAIL_READ_SCOPE_ALTERNATIVES);
228
+ } catch (error) {
229
+ throw publicFailure(
230
+ "Gmail OAuth auth locator lacks a scope that permits full thread and attachment reads",
231
+ { cause: error },
232
+ );
233
+ }
234
+ }
235
+
236
+ function validateCaptureOptions(options: CaptureArguments): URL {
237
+ if (options.currentTab || options.url === null) {
238
+ throw publicFailure("Gmail capture requires one exact Gmail thread URL; the current browser target is unsupported");
239
+ }
240
+ if (
241
+ options.browserProfile !== undefined
242
+ || options.browserLive
243
+ || options.cdp !== undefined
244
+ || options.cookieSources.length > 0
245
+ || options.cookieProfile !== undefined
246
+ || options.cookiesFile !== undefined
247
+ ) {
248
+ throw publicFailure("Gmail thread capture uses only stored OAuth auth; browser and cookie options are unsupported");
249
+ }
250
+ if (options.mode !== "auto" || options.htmlFile !== undefined) {
251
+ throw publicFailure("Gmail thread capture uses the official API and accepts only --mode auto");
252
+ }
253
+ if (options.scope !== "auto" && options.scope !== "thread") {
254
+ throw publicFailure("Gmail capture supports only thread scope");
255
+ }
256
+ if (options.evidence !== "none") {
257
+ throw publicFailure("Gmail thread capture does not persist browser evidence");
258
+ }
259
+ if (options.media !== "all" && options.media !== "none") {
260
+ throw publicFailure("Gmail thread attachments use --media all or --media none");
261
+ }
262
+ if (options.maxDepth > 32) {
263
+ throw publicFailure("Gmail MIME traversal supports a maximum depth of 32");
264
+ }
265
+ if (options.maxHtmlBytes > GMAIL_BODY_MAX_BYTES) {
266
+ throw publicFailure(`Gmail thread bodies support at most ${GMAIL_BODY_MAX_BYTES} decoded bytes`);
267
+ }
268
+ if (options.maxAssetBytes > GMAIL_ATTACHMENT_MAX_BYTES) {
269
+ throw publicFailure(`Gmail attachments support at most ${GMAIL_ATTACHMENT_MAX_BYTES} bytes per file`);
270
+ }
271
+ return options.url;
272
+ }
273
+
274
+ function sameEmail(left: string, right: string): boolean {
275
+ return left.normalize("NFKC").toLocaleLowerCase("en-US")
276
+ === right.normalize("NFKC").toLocaleLowerCase("en-US");
277
+ }
278
+
279
+ function accountLocatorMatches(locator: string, emailAddress: string): boolean {
280
+ return /^[0-9]{1,8}$/u.test(locator) || sameEmail(locator, emailAddress);
281
+ }
282
+
283
+ function base64WireBytes(decodedBytes: number): number {
284
+ const encoded = Math.ceil(decodedBytes / 3) * 4;
285
+ if (!Number.isSafeInteger(encoded)) {
286
+ throw publicFailure("the Gmail response byte limit is too large");
287
+ }
288
+ return encoded;
289
+ }
290
+
291
+ function checkedResponseBytes(...values: readonly number[]): number {
292
+ const total = values.reduce((sum, value) => sum + value, 0);
293
+ if (!Number.isSafeInteger(total)) {
294
+ throw publicFailure("the Gmail response byte limit is too large");
295
+ }
296
+ return total;
297
+ }
298
+
299
+ function threadResponseMaximumBytes(options: CaptureArguments): number {
300
+ const inlineAttachmentBytes = Math.min(
301
+ options.maxTotalAssetBytes,
302
+ GMAIL_INLINE_ATTACHMENT_MAX_BYTES,
303
+ );
304
+ return checkedResponseBytes(
305
+ base64WireBytes(options.maxHtmlBytes),
306
+ base64WireBytes(inlineAttachmentBytes),
307
+ GMAIL_THREAD_STRUCTURE_MAX_BYTES,
308
+ );
309
+ }
310
+
311
+ function attachmentResponseMaximumBytes(options: CaptureArguments): number {
312
+ return checkedResponseBytes(
313
+ base64WireBytes(options.maxAssetBytes),
314
+ GMAIL_RESPONSE_ENVELOPE_MAX_BYTES,
315
+ );
316
+ }
317
+
318
+ function responseMaximumBytes(options: CaptureArguments): number {
319
+ return Math.max(
320
+ GMAIL_PROFILE_RESPONSE_MAX_BYTES,
321
+ threadResponseMaximumBytes(options),
322
+ attachmentResponseMaximumBytes(options),
323
+ );
324
+ }
325
+
326
+ function defaultFetch(
327
+ input: string | URL | Request,
328
+ init?: RequestInit,
329
+ ): Promise<Response> {
330
+ return fetch(input, init);
331
+ }
332
+
333
+ function projectAttachment(
334
+ value: unknown,
335
+ parentMessageId: string,
336
+ ): GmailAttachmentProjection {
337
+ if (!isRecord(value)) throw new Error("Gmail attachment projection is invalid");
338
+ const messageId = nullableString(value.messageId, "attachment message ID", 512)
339
+ ?? parentMessageId;
340
+ if (messageId !== parentMessageId) {
341
+ throw new Error("Gmail attachment projection belongs to another message");
342
+ }
343
+ const contentDisposition = value.contentDisposition;
344
+ if (
345
+ contentDisposition !== null
346
+ && contentDisposition !== "attachment"
347
+ && contentDisposition !== "inline"
348
+ ) {
349
+ throw new Error("Gmail attachment content disposition is invalid");
350
+ }
351
+ return {
352
+ providerAttachment: value as GmailAttachment,
353
+ partId: boundedString(value.partId, "attachment part ID", 256),
354
+ contentDisposition,
355
+ messageId,
356
+ attachmentId: nullableString(value.attachmentId, "attachment ID", 4_096),
357
+ filename: boundedString(
358
+ value.filename,
359
+ "attachment filename",
360
+ MAX_ATTACHMENT_LABEL_CODE_UNITS,
361
+ true,
362
+ ),
363
+ mimeType: boundedString(value.mimeType, "attachment MIME type", 256),
364
+ size: safeInteger(value.size, "attachment size"),
365
+ };
366
+ }
367
+
368
+ function requiredHeaderProjection(
369
+ value: Record<string, unknown>,
370
+ names: readonly string[],
371
+ label: string,
372
+ ): unknown {
373
+ const containers = [value];
374
+ if (value.headers !== undefined) {
375
+ if (!isRecord(value.headers)) throw new Error("Gmail message header projection is invalid");
376
+ containers.push(value.headers);
377
+ }
378
+ let present = false;
379
+ let result: unknown;
380
+ for (const container of containers) {
381
+ for (const name of names) {
382
+ if (!Object.hasOwn(container, name)) continue;
383
+ if (present) throw new Error(`Gmail ${label} header projection is duplicated`);
384
+ present = true;
385
+ result = container[name];
386
+ }
387
+ }
388
+ if (!present) throw new Error(`Gmail ${label} header projection is missing`);
389
+ return result;
390
+ }
391
+
392
+ function bodyProjection(value: Record<string, unknown>): string {
393
+ if (typeof value.body === "string") return value.body;
394
+ if (!isRecord(value.body)) throw new Error("Gmail message body projection is invalid");
395
+ const body = value.body;
396
+ if (body.text === null) return "";
397
+ if (typeof body.text === "string") return body.text;
398
+ if (typeof body.markdown === "string") return body.markdown;
399
+ throw new Error("Gmail message body projection is invalid");
400
+ }
401
+
402
+ function projectMessage(value: unknown): GmailMessageProjection {
403
+ if (!isRecord(value)) throw new Error("Gmail message projection is invalid");
404
+ const id = boundedString(value.id, "message ID", 512);
405
+ if (!Array.isArray(value.attachments)) {
406
+ throw new Error("Gmail message attachment projection is invalid");
407
+ }
408
+ return {
409
+ id,
410
+ historyId: nullableString(value.historyId, "message history ID", 256),
411
+ internalDate: nullableString(value.internalDate, "message internal date", 64),
412
+ from: nullableString(requiredHeaderProjection(value, ["from"], "From"), "From header"),
413
+ to: nullableString(requiredHeaderProjection(value, ["to"], "To"), "To header"),
414
+ cc: nullableString(requiredHeaderProjection(value, ["cc"], "Cc"), "Cc header"),
415
+ bcc: nullableString(requiredHeaderProjection(value, ["bcc"], "Bcc"), "Bcc header"),
416
+ subject: nullableString(requiredHeaderProjection(value, ["subject"], "Subject"), "Subject header"),
417
+ date: nullableString(requiredHeaderProjection(value, ["date"], "Date"), "Date header"),
418
+ messageId: nullableString(
419
+ requiredHeaderProjection(value, ["messageId", "message-id"], "Message-ID"),
420
+ "Message-ID header",
421
+ ),
422
+ inReplyTo: nullableString(
423
+ requiredHeaderProjection(value, ["inReplyTo", "in-reply-to"], "In-Reply-To"),
424
+ "In-Reply-To header",
425
+ ),
426
+ body: bodyProjection(value),
427
+ attachments: value.attachments.map((attachment) => projectAttachment(attachment, id)),
428
+ };
429
+ }
430
+
431
+ function projectThread(value: unknown): GmailThreadProjection {
432
+ if (!isRecord(value) || !Array.isArray(value.messages)) {
433
+ throw new Error("Gmail thread projection is invalid");
434
+ }
435
+ const messages = value.messages.map(projectMessage);
436
+ if (new Set(messages.map((message) => message.id)).size !== messages.length) {
437
+ throw new Error("Gmail thread projection contains duplicate messages");
438
+ }
439
+ return {
440
+ id: boundedString(value.id, "thread ID", 512),
441
+ historyId: nullableString(value.historyId, "thread history ID", 256),
442
+ messages,
443
+ };
444
+ }
445
+
446
+ function safeSlug(value: string): string {
447
+ const normalized = value
448
+ .slice(0, 16 * 1024)
449
+ .normalize("NFKC")
450
+ .toLocaleLowerCase("en-US")
451
+ .replace(/['’]/gu, "")
452
+ .replace(/[^\p{Letter}\p{Number}]+/gu, "-")
453
+ .replace(/^-+|-+$/gu, "");
454
+ return [...normalized].slice(0, 80).join("").replace(/-+$/gu, "");
455
+ }
456
+
457
+ function captureSlug(
458
+ options: CaptureArguments,
459
+ accountSubject: string,
460
+ threadId: string,
461
+ ): string {
462
+ const source = options.slug === undefined
463
+ ? `gmail-thread-${createHash("sha256")
464
+ .update("gmail-thread\0", "utf8")
465
+ .update(accountSubject.toLocaleLowerCase("en-US"), "utf8")
466
+ .update("\0", "utf8")
467
+ .update(threadId, "utf8")
468
+ .digest("hex")
469
+ .slice(0, 32)}`
470
+ : redactSensitiveText(options.slug);
471
+ const slug = safeSlug(source);
472
+ if (slug === "") {
473
+ throw publicFailure(options.slug === undefined
474
+ ? "could not derive a safe Gmail capture slug"
475
+ : "the Gmail capture slug must contain at least one letter or number");
476
+ }
477
+ return slug;
478
+ }
479
+
480
+ function markdownPlainText(value: string): string {
481
+ return sanitizeTerminalText(value)
482
+ .replace(/&/gu, "&amp;")
483
+ .replace(/</gu, "&lt;")
484
+ .replace(/>/gu, "&gt;")
485
+ .replace(/[\\`*_[\]{}()#+.!|~-]/gu, "\\$&");
486
+ }
487
+
488
+ function inlineMarkdown(value: string, maximum = MAX_INLINE_FIELD_CODE_UNITS): string {
489
+ const sanitized = sanitizeTerminalLine(value)
490
+ .replace(/\s+/gu, " ")
491
+ .trim();
492
+ const bounded = sanitized.length > maximum
493
+ ? `${sanitized.slice(0, Math.max(0, maximum - 1))}…`
494
+ : sanitized;
495
+ return markdownPlainText(bounded);
496
+ }
497
+
498
+ function yamlString(value: string): string {
499
+ return JSON.stringify(sanitizeTerminalLine(value));
500
+ }
501
+
502
+ function countWords(value: string): number {
503
+ const matches = value.trim().match(/\S+/gu);
504
+ return matches?.length ?? 0;
505
+ }
506
+
507
+ function normalizedMimeType(value: string): string {
508
+ const mimeType = value.split(";", 1)[0]?.trim().toLocaleLowerCase("en-US") ?? "";
509
+ return /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/u.test(mimeType)
510
+ ? mimeType
511
+ : "application/octet-stream";
512
+ }
513
+
514
+ function storedAttachmentPath(digest: string): string {
515
+ return `assets/${digest}.${GMAIL_STORED_ATTACHMENT_EXTENSION}`;
516
+ }
517
+
518
+ function sha256(bytes: Uint8Array): string {
519
+ return createHash("sha256").update(bytes).digest("hex");
520
+ }
521
+
522
+ function verifyStagedFile(path: string, expectedBytes: number, expectedSha256: string): void {
523
+ const descriptor = openSync(
524
+ path,
525
+ constants.O_RDONLY
526
+ | ("O_NOFOLLOW" in constants ? constants.O_NOFOLLOW : 0)
527
+ | ("O_NONBLOCK" in constants ? constants.O_NONBLOCK : 0),
528
+ );
529
+ try {
530
+ const before = fstatSync(descriptor, { bigint: true });
531
+ if (
532
+ !before.isFile()
533
+ || before.size !== BigInt(expectedBytes)
534
+ || (process.platform !== "win32" && (before.mode & 0o777n) !== 0o600n)
535
+ ) {
536
+ throw new Error("staged Gmail attachment identity does not match its fetched bytes");
537
+ }
538
+ const digest = createHash("sha256");
539
+ const chunk = Buffer.allocUnsafe(Math.max(
540
+ 1,
541
+ Math.min(FILE_VERIFY_CHUNK_BYTES, expectedBytes),
542
+ ));
543
+ let offset = 0;
544
+ while (offset < expectedBytes) {
545
+ const count = readSync(
546
+ descriptor,
547
+ chunk,
548
+ 0,
549
+ Math.min(chunk.byteLength, expectedBytes - offset),
550
+ offset,
551
+ );
552
+ if (count === 0) {
553
+ throw new Error("staged Gmail attachment ended before its declared byte size");
554
+ }
555
+ digest.update(chunk.subarray(0, count));
556
+ offset += count;
557
+ }
558
+ const after = fstatSync(descriptor, { bigint: true });
559
+ if (
560
+ before.dev !== after.dev
561
+ || before.ino !== after.ino
562
+ || before.size !== after.size
563
+ || before.mtimeNs !== after.mtimeNs
564
+ || before.ctimeNs !== after.ctimeNs
565
+ || digest.digest("hex") !== expectedSha256
566
+ ) {
567
+ throw new Error("staged Gmail attachment digest does not match its fetched bytes");
568
+ }
569
+ } finally {
570
+ closeSync(descriptor);
571
+ }
572
+ }
573
+
574
+ function hardenTree(path: string): void {
575
+ const metadata = lstatSync(path);
576
+ if (metadata.isSymbolicLink()) throw new Error("private Gmail capture contains a symbolic link");
577
+ if (metadata.isFile()) {
578
+ chmodSync(path, 0o600);
579
+ return;
580
+ }
581
+ if (!metadata.isDirectory()) throw new Error("private Gmail capture contains an unsupported entry");
582
+ chmodSync(path, 0o700);
583
+ for (const entry of readdirSync(path)) hardenTree(join(path, entry));
584
+ }
585
+
586
+ function preflightAttachments(
587
+ messages: readonly GmailMessageProjection[],
588
+ options: CaptureArguments,
589
+ attachmentsRequested: boolean,
590
+ ): readonly GmailAttachmentProjection[] {
591
+ const attachments = messages.flatMap((message) => message.attachments);
592
+ const maximumFiles = Math.min(options.maxItems, MAX_GMAIL_ATTACHMENT_FILES);
593
+ if (attachments.length > maximumFiles) {
594
+ throw publicFailure(`Gmail thread has more than the configured ${maximumFiles} attachment file limit`);
595
+ }
596
+ if (!attachmentsRequested) return attachments;
597
+ let total = 0;
598
+ for (const attachment of attachments) {
599
+ if (attachment.size > options.maxAssetBytes) {
600
+ throw publicFailure("a Gmail attachment exceeds the configured per-file byte limit");
601
+ }
602
+ total += attachment.size;
603
+ if (!Number.isSafeInteger(total) || total > options.maxTotalAssetBytes) {
604
+ throw publicFailure("Gmail attachments exceed the configured total byte limit");
605
+ }
606
+ }
607
+ return attachments;
608
+ }
609
+
610
+ function encodePathSegment(value: string): string {
611
+ return encodeURIComponent(value).replace(/[!'()*]/gu, (character) =>
612
+ `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
613
+ }
614
+
615
+ function attachmentEndpoint(attachment: GmailAttachmentProjection): string {
616
+ if (attachment.attachmentId === null) {
617
+ return "https://gmail.googleapis.com/gmail/v1/users/me/messages/"
618
+ + encodePathSegment(attachment.messageId);
619
+ }
620
+ return "https://gmail.googleapis.com/gmail/v1/users/me/messages/"
621
+ + `${encodePathSegment(attachment.messageId)}/attachments/`
622
+ + encodePathSegment(attachment.attachmentId);
623
+ }
624
+
625
+ async function stageAttachments(
626
+ transaction: CaptureBundleTransaction,
627
+ client: GmailApiClient,
628
+ attachments: readonly GmailAttachmentProjection[],
629
+ options: CaptureArguments,
630
+ resolveAttachment: NonNullable<GmailCaptureDependencies["resolveAttachment"]>,
631
+ maximumResponseBytes: number,
632
+ ): Promise<{
633
+ readonly attachments: readonly StagedAttachment[];
634
+ readonly assets: readonly CaptureManifestAsset[];
635
+ readonly uniqueFileCount: number;
636
+ readonly fetchedBytes: number;
637
+ }> {
638
+ if (attachments.length === 0) {
639
+ return { attachments: [], assets: [], uniqueFileCount: 0, fetchedBytes: 0 };
640
+ }
641
+ mkdirSync(transaction.assetsDirectory, { recursive: true, mode: 0o700 });
642
+ chmodSync(transaction.assetsDirectory, 0o700);
643
+ const projected: StagedAttachment[] = [];
644
+ const assetsByPath = new Map<string, CaptureManifestAsset>();
645
+ const stagedPaths = new Set<string>();
646
+ let fetchedBytes = 0;
647
+ for (const attachment of attachments) {
648
+ let bytes: Uint8Array;
649
+ try {
650
+ bytes = await resolveAttachment(
651
+ client,
652
+ attachment.providerAttachment,
653
+ maximumResponseBytes,
654
+ );
655
+ } catch (error) {
656
+ throw publicFailure("could not fetch every Gmail attachment", { cause: error });
657
+ }
658
+ if (!(bytes instanceof Uint8Array)) {
659
+ throw publicFailure("Gmail returned an invalid attachment byte payload");
660
+ }
661
+ if (bytes.byteLength !== attachment.size) {
662
+ throw publicFailure("a Gmail attachment did not match its declared byte size");
663
+ }
664
+ if (bytes.byteLength > options.maxAssetBytes) {
665
+ throw publicFailure("a Gmail attachment exceeds the configured per-file byte limit");
666
+ }
667
+ fetchedBytes += bytes.byteLength;
668
+ if (!Number.isSafeInteger(fetchedBytes) || fetchedBytes > options.maxTotalAssetBytes) {
669
+ throw publicFailure("Gmail attachments exceed the configured total byte limit");
670
+ }
671
+ const digest = sha256(bytes);
672
+ const relativePath = storedAttachmentPath(digest);
673
+ const targetPath = join(transaction.stagingDirectory, relativePath);
674
+ if (!existsSync(targetPath)) {
675
+ writeFileSync(targetPath, bytes, { flag: "wx", mode: 0o600 });
676
+ chmodSync(targetPath, 0o600);
677
+ }
678
+ verifyStagedFile(targetPath, bytes.byteLength, digest);
679
+ projected.push({
680
+ ...attachment,
681
+ path: relativePath,
682
+ sha256: digest,
683
+ bytes: bytes.byteLength,
684
+ });
685
+ stagedPaths.add(relativePath);
686
+ const endpoint = attachmentEndpoint(attachment);
687
+ if (!assetsByPath.has(relativePath)) {
688
+ assetsByPath.set(relativePath, {
689
+ source: endpoint,
690
+ url: endpoint,
691
+ path: relativePath,
692
+ mimeType: GMAIL_STORED_ATTACHMENT_MIME_TYPE,
693
+ bytes: bytes.byteLength,
694
+ sha256: digest,
695
+ });
696
+ }
697
+ }
698
+ return {
699
+ attachments: projected,
700
+ assets: [...assetsByPath.values()],
701
+ uniqueFileCount: stagedPaths.size,
702
+ fetchedBytes,
703
+ };
704
+ }
705
+
706
+ function attachmentLookup(
707
+ attachments: readonly StagedAttachment[],
708
+ ): ReadonlyMap<GmailAttachment, StagedAttachment> {
709
+ return new Map(attachments.map((attachment) => [
710
+ attachment.providerAttachment,
711
+ attachment,
712
+ ]));
713
+ }
714
+
715
+ function previewStagedAttachments(
716
+ attachments: readonly GmailAttachmentProjection[],
717
+ ): readonly StagedAttachment[] {
718
+ return attachments.map((attachment) => {
719
+ return {
720
+ ...attachment,
721
+ path: storedAttachmentPath("0".repeat(64)),
722
+ sha256: "0".repeat(64),
723
+ bytes: attachment.size,
724
+ };
725
+ });
726
+ }
727
+
728
+ function writeGmailProvenance(
729
+ transaction: CaptureBundleTransaction,
730
+ input: {
731
+ readonly sourceUrl: string;
732
+ readonly canonicalUrl: string;
733
+ readonly accountSubject: string;
734
+ readonly accountBinding: "email-locator-verified" | "numeric-slot-rebound";
735
+ readonly capturedAt: Date;
736
+ readonly thread: GmailThreadProjection;
737
+ readonly stagedAttachments: readonly StagedAttachment[];
738
+ },
739
+ ): void {
740
+ const staged = attachmentLookup(input.stagedAttachments);
741
+ const document = {
742
+ schemaVersion: 2,
743
+ sourceUrl: sanitizeArtifactUrl(input.sourceUrl),
744
+ canonicalUrl: sanitizeArtifactUrl(input.canonicalUrl),
745
+ capturedAt: input.capturedAt.toISOString(),
746
+ accountSubject: input.accountSubject,
747
+ accountBinding: input.accountBinding,
748
+ thread: {
749
+ id: input.thread.id,
750
+ historyId: input.thread.historyId,
751
+ messages: input.thread.messages.map((message) => ({
752
+ id: message.id,
753
+ historyId: message.historyId,
754
+ internalDate: message.internalDate,
755
+ headers: {
756
+ from: message.from,
757
+ to: message.to,
758
+ cc: message.cc,
759
+ bcc: message.bcc,
760
+ subject: message.subject,
761
+ date: message.date,
762
+ messageId: message.messageId,
763
+ inReplyTo: message.inReplyTo,
764
+ },
765
+ attachments: message.attachments.map((attachment) => {
766
+ const captured = staged.get(attachment.providerAttachment);
767
+ return {
768
+ partId: attachment.partId,
769
+ attachmentId: attachment.attachmentId,
770
+ contentDisposition: attachment.contentDisposition,
771
+ filename: redactSensitiveText(attachment.filename),
772
+ mimeType: normalizedMimeType(attachment.mimeType),
773
+ declaredBytes: attachment.size,
774
+ path: captured?.path ?? null,
775
+ sha256: captured?.sha256 ?? null,
776
+ capturedBytes: captured?.bytes ?? null,
777
+ };
778
+ }),
779
+ })),
780
+ },
781
+ } as const;
782
+ writeFileSync(
783
+ join(transaction.stagingDirectory, GMAIL_PROVENANCE_FILENAME),
784
+ `${JSON.stringify(document, null, 2)}\n`,
785
+ { encoding: "utf8", flag: "wx", mode: 0o600 },
786
+ );
787
+ }
788
+
789
+ function renderHeaders(message: GmailMessageProjection): readonly string[] {
790
+ return [
791
+ message.from === null ? null : `- **From:** ${inlineMarkdown(message.from)}`,
792
+ message.to === null ? null : `- **To:** ${inlineMarkdown(message.to)}`,
793
+ message.cc === null ? null : `- **Cc:** ${inlineMarkdown(message.cc)}`,
794
+ message.bcc === null ? null : `- **Bcc:** ${inlineMarkdown(message.bcc)}`,
795
+ message.date === null ? null : `- **Date:** ${inlineMarkdown(message.date)}`,
796
+ message.messageId === null ? null : `- **Message-ID:** ${inlineMarkdown(message.messageId)}`,
797
+ message.inReplyTo === null ? null : `- **In-Reply-To:** ${inlineMarkdown(message.inReplyTo)}`,
798
+ ].filter((line): line is string => line !== null);
799
+ }
800
+
801
+ function renderThreadMarkdown(input: {
802
+ readonly thread: GmailThreadProjection;
803
+ readonly slug: string;
804
+ readonly canonicalUrl: string;
805
+ readonly capturedAt: Date;
806
+ readonly stagedAttachments: readonly StagedAttachment[];
807
+ readonly attachmentsCaptured: boolean;
808
+ }): string {
809
+ const title = input.thread.messages
810
+ .map((message) => message.subject?.trim())
811
+ .find((subject): subject is string => subject !== undefined && subject !== "")
812
+ ?? "Gmail thread";
813
+ const localByAttachment = attachmentLookup(input.stagedAttachments);
814
+ const chunks = [
815
+ "---",
816
+ `title: ${yamlString(title)}`,
817
+ `source: ${yamlString(input.canonicalUrl)}`,
818
+ `clipped: ${yamlString(input.capturedAt.toISOString().slice(0, 10))}`,
819
+ 'platform: "gmail"',
820
+ 'capture_status: "complete"',
821
+ 'capture_method: "gmail-api"',
822
+ 'capture_scope: "thread"',
823
+ "---",
824
+ "",
825
+ `# ${inlineMarkdown(title, 2_048) || input.slug}`,
826
+ "",
827
+ ];
828
+ for (const [index, message] of input.thread.messages.entries()) {
829
+ chunks.push(`## Message ${index + 1}`, "", ...renderHeaders(message), "");
830
+ const body = markdownPlainText(message.body).trim();
831
+ chunks.push(body === "" ? "_No text body._" : body, "");
832
+ if (message.attachments.length === 0) continue;
833
+ chunks.push("### Attachments", "");
834
+ for (const attachment of message.attachments) {
835
+ const label = inlineMarkdown(attachment.filename, MAX_ATTACHMENT_LABEL_CODE_UNITS)
836
+ || "Unnamed attachment";
837
+ const staged = localByAttachment.get(attachment.providerAttachment);
838
+ if (input.attachmentsCaptured && staged !== undefined) {
839
+ chunks.push(`- [${label}](${staged.path}) (${staged.bytes} bytes)`);
840
+ } else {
841
+ chunks.push(`- ${label} (${attachment.size} bytes; content not captured)`);
842
+ }
843
+ }
844
+ chunks.push("");
845
+ }
846
+ return `${sanitizeTerminalText(chunks.join("\n")).trimEnd()}\n`;
847
+ }
848
+
849
+ function assertThreadBounds(thread: GmailThreadProjection, options: CaptureArguments): void {
850
+ if (thread.messages.length === 0) {
851
+ throw publicFailure("Gmail returned an empty thread");
852
+ }
853
+ if (thread.messages.length > options.maxItems) {
854
+ throw publicFailure("Gmail thread exceeds the configured message count limit");
855
+ }
856
+ let bodyBytes = 0;
857
+ for (const message of thread.messages) {
858
+ bodyBytes += Buffer.byteLength(message.body, "utf8");
859
+ if (!Number.isSafeInteger(bodyBytes) || bodyBytes > options.maxHtmlBytes) {
860
+ throw publicFailure("Gmail thread bodies exceed the configured content byte limit");
861
+ }
862
+ }
863
+ }
864
+
865
+ function manifestInput(input: {
866
+ readonly sourceUrl: string;
867
+ readonly canonicalUrl: string;
868
+ readonly capturedAt: Date;
869
+ readonly markdown: string;
870
+ readonly messageCount: number;
871
+ readonly assets: readonly CaptureManifestAsset[];
872
+ readonly capturedFileCount: number;
873
+ readonly attachmentsRequested: boolean;
874
+ readonly warnings: readonly string[];
875
+ }): CaptureManifestInput {
876
+ return {
877
+ sourceUrl: input.sourceUrl,
878
+ canonicalUrl: input.canonicalUrl,
879
+ capturedAt: input.capturedAt.toISOString(),
880
+ platform: "gmail",
881
+ status: "complete",
882
+ scope: "thread",
883
+ acquisition: {
884
+ method: "gmail-api",
885
+ finalUrl: input.canonicalUrl,
886
+ contentType: "application/json",
887
+ },
888
+ extraction: {
889
+ extractor: "gmail-api-thread",
890
+ score: 1,
891
+ wordCount: countWords(input.markdown),
892
+ capturedItems: input.messageCount,
893
+ expectedItems: input.messageCount,
894
+ },
895
+ attempts: [{
896
+ method: "gmail-api",
897
+ outcome: "succeeded",
898
+ message: "Fetched the complete Gmail thread through the official API.",
899
+ }],
900
+ assets: input.assets,
901
+ artifacts: {
902
+ images: { requested: false, status: "not-requested", files: 0 },
903
+ media: input.attachmentsRequested
904
+ ? { requested: true, status: "captured", files: input.capturedFileCount }
905
+ : { requested: false, status: "not-requested", files: 0 },
906
+ videoContext: {
907
+ requested: false,
908
+ status: "not-requested",
909
+ thumbnailPath: null,
910
+ transcriptLanguage: null,
911
+ transcriptCueCount: 0,
912
+ transcriptTruncated: false,
913
+ metadata: null,
914
+ },
915
+ },
916
+ evidence: {
917
+ requested: "none",
918
+ screenshotPath: null,
919
+ screenshotStatus: "not-requested",
920
+ sourceHtmlStatus: "not-requested",
921
+ },
922
+ warnings: input.warnings,
923
+ };
924
+ }
925
+
926
+ function beginPrivateCapture(options: CaptureArguments, slug: string): CaptureBundleTransaction {
927
+ try {
928
+ return beginCaptureBundle({
929
+ outputRoot: options.outputBase,
930
+ slug,
931
+ force: options.force,
932
+ });
933
+ } catch (error) {
934
+ const detail = error instanceof Error ? error.message : "";
935
+ if (detail.includes("capture already exists")) {
936
+ throw publicFailure("Gmail thread capture already exists; pass --force to replace it", { cause: error });
937
+ }
938
+ throw publicFailure("could not prepare private Gmail capture storage", { cause: error });
939
+ }
940
+ }
941
+
942
+ async function captureGmailThread(
943
+ options: CaptureArguments,
944
+ auth: GmailOAuthAuth,
945
+ environment: Readonly<Record<string, string | undefined>>,
946
+ dependencies: GmailCaptureDependencies,
947
+ ): Promise<GmailCaptureOutcome> {
948
+ const sourceUrl = validateCaptureOptions(options);
949
+ const parseThreadUrl = dependencies.parseThreadUrl ?? parseGmailThreadUrl;
950
+ let parsedUrl: ReturnType<typeof parseGmailThreadUrl>;
951
+ try {
952
+ parsedUrl = parseThreadUrl(sourceUrl);
953
+ } catch (error) {
954
+ throw publicFailure("capture target must be one exact Gmail thread URL", { cause: error });
955
+ }
956
+ let token: Awaited<ReturnType<typeof resolveOAuthToken>>;
957
+ try {
958
+ token = await resolveOAuthToken(auth, {
959
+ environment,
960
+ now: dependencies.now?.() ?? new Date(),
961
+ minimumValidityMs: options.timeoutMs,
962
+ ...(dependencies.fetch === undefined ? {} : { fetch: dependencies.fetch }),
963
+ });
964
+ } catch (error) {
965
+ throw publicFailure("could not load a valid private Gmail OAuth token", { cause: error });
966
+ }
967
+ const http = dependencies.httpClient ?? new ProviderHttpClient(
968
+ dependencies.fetch ?? defaultFetch,
969
+ options.timeoutMs,
970
+ responseMaximumBytes(options),
971
+ );
972
+ const createClient = dependencies.createClient ?? createGmailApiClient;
973
+ let client: GmailApiClient;
974
+ try {
975
+ client = createClient({
976
+ http,
977
+ accessToken: token.accessToken,
978
+ subject: auth.subject,
979
+ });
980
+ } catch (error) {
981
+ throw publicFailure("could not initialize the Gmail API client", { cause: error });
982
+ }
983
+ const getProfile = dependencies.getProfile ?? getAuthenticatedGmailProfile;
984
+ let profile: GmailProfileProjection;
985
+ try {
986
+ profile = await getProfile(client, GMAIL_PROFILE_RESPONSE_MAX_BYTES);
987
+ } catch (error) {
988
+ throw publicFailure("could not verify the authenticated Gmail account", { cause: error });
989
+ }
990
+ if (typeof profile.emailAddress !== "string" || profile.emailAddress.length > 254) {
991
+ throw publicFailure("Gmail returned an invalid authenticated account profile");
992
+ }
993
+ if (!sameEmail(profile.emailAddress, auth.subject)) {
994
+ throw publicFailure("the authenticated Gmail account does not match the auth locator subject");
995
+ }
996
+ if (!accountLocatorMatches(parsedUrl.accountLocator, profile.emailAddress)) {
997
+ throw publicFailure("the Gmail thread URL account does not match the authenticated account");
998
+ }
999
+ const buildThreadUrl = dependencies.buildThreadUrl ?? buildGmailThreadUrl;
1000
+ let canonicalUrl: string;
1001
+ try {
1002
+ const candidate = buildThreadUrl(
1003
+ profile.emailAddress,
1004
+ parsedUrl.threadId,
1005
+ parsedUrl.view,
1006
+ );
1007
+ const verified = parseGmailThreadUrl(new URL(candidate));
1008
+ if (
1009
+ !sameEmail(verified.accountLocator, profile.emailAddress)
1010
+ || verified.threadId !== parsedUrl.threadId
1011
+ || verified.view !== parsedUrl.view
1012
+ ) throw new Error("canonical Gmail thread URL changed its bound target");
1013
+ canonicalUrl = verified.canonicalUrl;
1014
+ } catch (error) {
1015
+ throw publicFailure("could not construct the canonical Gmail thread URL", { cause: error });
1016
+ }
1017
+ const fetchThread = dependencies.fetchThread ?? fetchGmailThread;
1018
+ let rawThread: unknown;
1019
+ try {
1020
+ rawThread = await fetchThread(
1021
+ client,
1022
+ parsedUrl.threadId,
1023
+ threadResponseMaximumBytes(options),
1024
+ );
1025
+ } catch (error) {
1026
+ throw publicFailure("could not fetch the Gmail thread", { cause: error });
1027
+ }
1028
+ let thread: GmailThreadProjection;
1029
+ try {
1030
+ const parsedThread = dependencies.parseThread === undefined
1031
+ ? parseGmailThread(rawThread, {
1032
+ maxDepth: options.maxDepth,
1033
+ maxBodyBytes: options.maxHtmlBytes,
1034
+ deadlineCheckpoint: () => client.http.throwIfUnavailable(),
1035
+ })
1036
+ : dependencies.parseThread(rawThread);
1037
+ const completeThread = dependencies.parseThread !== undefined
1038
+ && dependencies.resolveThreadBodies === undefined
1039
+ ? parsedThread
1040
+ : await (dependencies.resolveThreadBodies ?? resolveGmailThreadBodies)(
1041
+ client,
1042
+ parsedThread as ReturnType<typeof parseGmailThread>,
1043
+ threadResponseMaximumBytes(options),
1044
+ );
1045
+ thread = projectThread(completeThread);
1046
+ } catch (error) {
1047
+ throw publicFailure("Gmail returned an invalid thread projection", { cause: error });
1048
+ }
1049
+ if (thread.id !== parsedUrl.threadId) {
1050
+ throw publicFailure("Gmail returned a different thread than the requested URL");
1051
+ }
1052
+ assertThreadBounds(thread, options);
1053
+ const readOnly = options.command === "inspect" || options.stdout;
1054
+ const attachmentsRequested = options.media !== "none" && !readOnly;
1055
+ const attachments = preflightAttachments(
1056
+ thread.messages,
1057
+ options,
1058
+ attachmentsRequested,
1059
+ );
1060
+ const slug = captureSlug(options, profile.emailAddress, thread.id);
1061
+ const capturedAt = dependencies.now?.() ?? new Date();
1062
+ if (!Number.isFinite(capturedAt.getTime())) {
1063
+ throw publicFailure("the Gmail capture timestamp is invalid");
1064
+ }
1065
+ const accountBinding = /^[0-9]{1,8}$/u.test(parsedUrl.accountLocator)
1066
+ ? "numeric-slot-rebound" as const
1067
+ : "email-locator-verified" as const;
1068
+ const warnings = accountBinding === "numeric-slot-rebound"
1069
+ ? ["The copied Gmail URL used a browser-local numeric account slot; the stored OAuth subject supplied the captured account identity."]
1070
+ : [];
1071
+ if (readOnly) {
1072
+ const markdown = redactSensitiveText(renderThreadMarkdown({
1073
+ thread,
1074
+ slug,
1075
+ canonicalUrl,
1076
+ capturedAt,
1077
+ stagedAttachments: [],
1078
+ attachmentsCaptured: false,
1079
+ }));
1080
+ if (Buffer.byteLength(markdown, "utf8") > options.maxHtmlBytes) {
1081
+ throw publicFailure("rendered Gmail thread exceeds the configured content byte limit");
1082
+ }
1083
+ return {
1084
+ slug,
1085
+ markdown,
1086
+ messageCount: thread.messages.length,
1087
+ discoveredAttachmentCount: attachments.length,
1088
+ capturedAttachmentCount: 0,
1089
+ capturedFileCount: 0,
1090
+ capturedBytes: 0,
1091
+ warnings: Object.freeze(warnings),
1092
+ };
1093
+ }
1094
+
1095
+ const previewMarkdown = redactSensitiveText(renderThreadMarkdown({
1096
+ thread,
1097
+ slug,
1098
+ canonicalUrl,
1099
+ capturedAt,
1100
+ stagedAttachments: attachmentsRequested
1101
+ ? previewStagedAttachments(attachments)
1102
+ : [],
1103
+ attachmentsCaptured: attachmentsRequested,
1104
+ }));
1105
+ if (Buffer.byteLength(previewMarkdown, "utf8") > options.maxHtmlBytes) {
1106
+ throw publicFailure("rendered Gmail thread exceeds the configured content byte limit");
1107
+ }
1108
+
1109
+ const transaction = beginPrivateCapture(options, slug);
1110
+ try {
1111
+ const staged = attachmentsRequested
1112
+ ? await stageAttachments(
1113
+ transaction,
1114
+ client,
1115
+ attachments,
1116
+ options,
1117
+ dependencies.resolveAttachment ?? resolveGmailAttachmentBytes,
1118
+ attachmentResponseMaximumBytes(options),
1119
+ )
1120
+ : { attachments: [], assets: [], uniqueFileCount: 0, fetchedBytes: 0 };
1121
+ const markdown = redactSensitiveText(renderThreadMarkdown({
1122
+ thread,
1123
+ slug,
1124
+ canonicalUrl,
1125
+ capturedAt,
1126
+ stagedAttachments: staged.attachments,
1127
+ attachmentsCaptured: attachmentsRequested,
1128
+ }));
1129
+ if (Buffer.byteLength(markdown, "utf8") > options.maxHtmlBytes) {
1130
+ throw publicFailure("rendered Gmail thread exceeds the configured content byte limit");
1131
+ }
1132
+ writeGmailProvenance(transaction, {
1133
+ sourceUrl: sourceUrl.href,
1134
+ canonicalUrl,
1135
+ accountSubject: profile.emailAddress,
1136
+ accountBinding,
1137
+ capturedAt,
1138
+ thread,
1139
+ stagedAttachments: staged.attachments,
1140
+ });
1141
+ writeCaptureBundle(transaction, {
1142
+ markdown,
1143
+ manifest: manifestInput({
1144
+ sourceUrl: sourceUrl.href,
1145
+ canonicalUrl,
1146
+ capturedAt,
1147
+ markdown,
1148
+ messageCount: thread.messages.length,
1149
+ assets: staged.assets,
1150
+ capturedFileCount: staged.uniqueFileCount,
1151
+ attachmentsRequested,
1152
+ warnings,
1153
+ }),
1154
+ });
1155
+ hardenTree(transaction.stagingDirectory);
1156
+ try {
1157
+ commitCaptureBundle(transaction);
1158
+ } catch (error) {
1159
+ throw publicFailure("could not commit the private Gmail capture", { cause: error });
1160
+ }
1161
+ return {
1162
+ slug,
1163
+ markdown,
1164
+ messageCount: thread.messages.length,
1165
+ discoveredAttachmentCount: attachments.length,
1166
+ capturedAttachmentCount: staged.attachments.length,
1167
+ capturedFileCount: staged.uniqueFileCount,
1168
+ capturedBytes: staged.fetchedBytes,
1169
+ warnings: Object.freeze(warnings),
1170
+ };
1171
+ } catch (error) {
1172
+ abortCaptureBundle(transaction);
1173
+ if (error instanceof PublicGmailCaptureError) throw error;
1174
+ throw publicFailure("could not persist the private Gmail thread capture", { cause: error });
1175
+ }
1176
+ }
1177
+
1178
+ function captureSummary(outcome: GmailCaptureOutcome): Record<string, unknown> {
1179
+ return {
1180
+ ok: true,
1181
+ status: "complete",
1182
+ platform: "gmail",
1183
+ scope: "thread",
1184
+ slug: outcome.slug,
1185
+ messageCount: outcome.messageCount,
1186
+ discoveredAttachmentCount: outcome.discoveredAttachmentCount,
1187
+ capturedAttachmentCount: outcome.capturedAttachmentCount,
1188
+ capturedFileCount: outcome.capturedFileCount,
1189
+ capturedBytes: outcome.capturedBytes,
1190
+ warnings: outcome.warnings,
1191
+ };
1192
+ }
1193
+
1194
+ function terminalJson(value: unknown): string {
1195
+ return `${JSON.stringify(value, (_key, candidate: unknown) =>
1196
+ typeof candidate === "string"
1197
+ ? sanitizeTerminalText(redactSensitiveText(candidate))
1198
+ : candidate, 2)}\n`;
1199
+ }
1200
+
1201
+ function publicErrorMessage(error: unknown): string {
1202
+ const message = error instanceof PublicGmailCaptureError
1203
+ ? error.message
1204
+ : "Gmail thread capture failed";
1205
+ return sanitizeTerminalLine(redactSensitiveText(message))
1206
+ .slice(0, MAX_PUBLIC_DIAGNOSTIC_CODE_UNITS);
1207
+ }
1208
+
1209
+ export const runGmailCapture: GmailCaptureRunner = async (
1210
+ options,
1211
+ auth,
1212
+ environment,
1213
+ output,
1214
+ dependencies = {},
1215
+ ) => {
1216
+ try {
1217
+ requireGmailAuth(auth);
1218
+ if (!options.quiet && !options.json) {
1219
+ output.stderr("Capturing one Gmail thread through the official API ...\n");
1220
+ }
1221
+ const outcome = await captureGmailThread(options, auth, environment, dependencies);
1222
+ if (!options.json) {
1223
+ for (const warning of outcome.warnings) {
1224
+ output.stderr(`warning: ${sanitizeTerminalLine(warning)}\n`);
1225
+ }
1226
+ }
1227
+ if (options.json) output.stdout(terminalJson(captureSummary(outcome)));
1228
+ else if (options.stdout || options.command === "inspect") {
1229
+ output.stdout(sanitizeTerminalText(outcome.markdown));
1230
+ }
1231
+ else {
1232
+ output.stdout(`Done: Gmail thread captured as ${outcome.slug}.\n`);
1233
+ output.stdout(
1234
+ `Status: complete; ${outcome.messageCount} messages; ${outcome.capturedAttachmentCount} attachments; ${outcome.capturedBytes} bytes.\n`,
1235
+ );
1236
+ }
1237
+ return 0;
1238
+ } catch (error) {
1239
+ const message = publicErrorMessage(error);
1240
+ if (options.json) output.stdout(terminalJson({ ok: false, error: message }));
1241
+ else output.stderr(`error: ${message}\n`);
1242
+ return 1;
1243
+ }
1244
+ };