@oomol-lab/open-connector 1.6.4 → 1.7.0

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 (320) hide show
  1. package/assets/open-connector/catalog/apps/aisa.json +6394 -0
  2. package/assets/open-connector/catalog/apps/baizhi_mcp.json +135 -0
  3. package/assets/open-connector/catalog/apps/bilibili.json +2586 -0
  4. package/assets/open-connector/catalog/apps/bitwarden.json +1423 -0
  5. package/assets/open-connector/catalog/apps/cronfree.json +303 -0
  6. package/assets/open-connector/catalog/apps/cufinder.json +368 -0
  7. package/assets/open-connector/catalog/apps/discordbot.json +2888 -248
  8. package/assets/open-connector/catalog/apps/drawio_mcp.json +192 -0
  9. package/assets/open-connector/catalog/apps/firecrawl.json +176 -32
  10. package/assets/open-connector/catalog/apps/framejet.json +273 -0
  11. package/assets/open-connector/catalog/apps/futunn.json +2967 -0
  12. package/assets/open-connector/catalog/apps/gmail.json +298 -0
  13. package/assets/open-connector/catalog/apps/google_analytics.json +4 -1
  14. package/assets/open-connector/catalog/apps/google_bigquery.json +4 -1
  15. package/assets/open-connector/catalog/apps/google_search_console.json +4 -1
  16. package/assets/open-connector/catalog/apps/googlecalendar.json +4 -1
  17. package/assets/open-connector/catalog/apps/googlechat.json +694 -7
  18. package/assets/open-connector/catalog/apps/googledocs.json +4 -1
  19. package/assets/open-connector/catalog/apps/googlesheets.json +4 -1
  20. package/assets/open-connector/catalog/apps/granola.json +23 -0
  21. package/assets/open-connector/catalog/apps/investoday_mcp.json +22228 -197
  22. package/assets/open-connector/catalog/apps/ixspy.json +2274 -0
  23. package/assets/open-connector/catalog/apps/keepa.json +3534 -987
  24. package/assets/open-connector/catalog/apps/linear.json +17 -0
  25. package/assets/open-connector/catalog/apps/linear_mcp.json +510 -0
  26. package/assets/open-connector/catalog/apps/moneyforward_mcp.json +1432 -0
  27. package/assets/open-connector/catalog/apps/notion.json +446 -1
  28. package/assets/open-connector/catalog/apps/notion_mcp.json +490 -0
  29. package/assets/open-connector/catalog/apps/odoo.json +619 -0
  30. package/assets/open-connector/catalog/apps/osv.json +663 -0
  31. package/assets/open-connector/catalog/apps/outlook.json +30 -8
  32. package/assets/open-connector/catalog/apps/outlook_calendar.json +23 -2
  33. package/assets/open-connector/catalog/apps/pdf_vector.json +329 -0
  34. package/assets/open-connector/catalog/apps/postiz.json +334 -0
  35. package/assets/open-connector/catalog/apps/proworkflow.json +529 -0
  36. package/assets/open-connector/catalog/apps/pulsetic.json +406 -0
  37. package/assets/open-connector/catalog/apps/sellersprite_mcp.json +2 -1
  38. package/assets/open-connector/catalog/apps/shopee.json +1785 -0
  39. package/assets/open-connector/catalog/apps/slack.json +304 -0
  40. package/assets/open-connector/catalog/apps/slackbot.json +295 -0
  41. package/assets/open-connector/catalog/apps/speak_ai.json +360 -0
  42. package/assets/open-connector/catalog/apps/tencent_docs.json +3 -0
  43. package/assets/open-connector/catalog/apps/wisburg.json +1528 -0
  44. package/assets/open-connector/catalog/apps/youtube.json +4 -1
  45. package/assets/open-connector/catalog/apps/zerotier.json +3319 -0
  46. package/assets/open-connector/catalog/apps/zyhub.json +1585 -0
  47. package/assets/open-connector/catalog/apps-index.json +1 -1
  48. package/package.json +1 -1
  49. package/src/core/cast.d.ts +10 -0
  50. package/src/core/cast.js +14 -0
  51. package/src/core/guarded-fetch.d.ts +6 -3
  52. package/src/core/guarded-fetch.js +6 -3
  53. package/src/core/json-schema.d.ts +5 -1
  54. package/src/core/json-schema.js +14 -2
  55. package/src/core/provider-scenarios.js +67 -13
  56. package/src/core/request.js +4 -1
  57. package/src/core/types.d.ts +10 -1
  58. package/src/mcp.js +10 -7
  59. package/src/oauth/oauth-client-config-service.d.ts +16 -1
  60. package/src/oauth/oauth-client-config-service.js +59 -3
  61. package/src/oauth/oauth-credential-refresh-service.js +3 -1
  62. package/src/oauth/oauth-flow-service.d.ts +6 -0
  63. package/src/oauth/oauth-flow-service.js +21 -4
  64. package/src/oauth/oauth-token.d.ts +13 -0
  65. package/src/providers/17track/runtime.js +1 -1
  66. package/src/providers/action-contracts.generated.d.ts +32 -8
  67. package/src/providers/aifinmarket/runtime.js +1 -1
  68. package/src/providers/aisa/executors.d.ts +4 -0
  69. package/src/providers/aisa/executors.js +864 -0
  70. package/src/providers/aliyun_ack/runtime.js +1 -1
  71. package/src/providers/aliyun_sls/runtime.js +1 -1
  72. package/src/providers/app_store_server/runtime-helpers.js +1 -1
  73. package/src/providers/baizhi_mcp/actions.d.ts +2 -0
  74. package/src/providers/baizhi_mcp/actions.js +40 -0
  75. package/src/providers/baizhi_mcp/executors.d.ts +3 -0
  76. package/src/providers/baizhi_mcp/executors.js +102 -0
  77. package/src/providers/bazhuayu/runtime.js +2 -2
  78. package/src/providers/bilibili/articles.d.ts +3 -0
  79. package/src/providers/bilibili/articles.js +369 -0
  80. package/src/providers/bilibili/executors.d.ts +8 -0
  81. package/src/providers/bilibili/executors.js +262 -0
  82. package/src/providers/bilibili/runtime.d.ts +95 -0
  83. package/src/providers/bilibili/runtime.js +236 -0
  84. package/src/providers/bilibili/upload.d.ts +14 -0
  85. package/src/providers/bilibili/upload.js +107 -0
  86. package/src/providers/bitwarden/executors.d.ts +3 -0
  87. package/src/providers/bitwarden/executors.js +146 -0
  88. package/src/providers/cloudflare_browser_rendering/executors.js +1 -1
  89. package/src/providers/cloudflare_dns/executors.js +1 -1
  90. package/src/providers/{cloudflare-current-user.js → cloudflare_dns/runtime-user.js} +2 -2
  91. package/src/providers/cloudflare_dns/runtime.d.ts +1 -1
  92. package/src/providers/cloudflare_dns/runtime.js +1 -1
  93. package/src/providers/cloudflare_r2/executors.js +1 -1
  94. package/src/providers/cloudflare_r2/runtime.d.ts +1 -1
  95. package/src/providers/cloudflare_r2/runtime.js +1 -1
  96. package/src/providers/cloudflare_worker/executors.js +1 -1
  97. package/src/providers/cloudflare_worker/runtime.d.ts +1 -1
  98. package/src/providers/cloudflare_worker/runtime.js +1 -1
  99. package/src/providers/cronfree/executors.d.ts +4 -0
  100. package/src/providers/cronfree/executors.js +89 -0
  101. package/src/providers/cufinder/executors.d.ts +4 -0
  102. package/src/providers/cufinder/executors.js +105 -0
  103. package/src/providers/dingtalk_mcp/runtime.js +1 -1
  104. package/src/providers/discordbot/executors.d.ts +0 -9
  105. package/src/providers/discordbot/executors.js +9 -101
  106. package/src/providers/discordbot/runtime-guilds.d.ts +3 -0
  107. package/src/providers/discordbot/runtime-guilds.js +464 -0
  108. package/src/providers/discordbot/runtime.d.ts +42 -0
  109. package/src/providers/discordbot/runtime.js +124 -0
  110. package/src/providers/drawio_mcp/executors.d.ts +3 -0
  111. package/src/providers/drawio_mcp/executors.js +19 -0
  112. package/src/providers/drawio_mcp/runtime.d.ts +8 -0
  113. package/src/providers/drawio_mcp/runtime.js +175 -0
  114. package/src/providers/elevenreader/runtime.js +6 -5
  115. package/src/providers/fastmoss_mcp/executors.js +2 -2
  116. package/src/providers/finchina/executors.js +1 -1
  117. package/src/providers/framejet/executors.d.ts +4 -0
  118. package/src/providers/framejet/executors.js +19 -0
  119. package/src/providers/framejet/runtime.d.ts +16 -0
  120. package/src/providers/framejet/runtime.js +148 -0
  121. package/src/providers/futunn/actions.d.ts +2 -0
  122. package/src/providers/futunn/actions.js +152 -0
  123. package/src/providers/futunn/actions.research.d.ts +2 -0
  124. package/src/providers/futunn/actions.research.js +423 -0
  125. package/src/providers/futunn/actions.trading.d.ts +2 -0
  126. package/src/providers/futunn/actions.trading.js +204 -0
  127. package/src/providers/futunn/executors.d.ts +3 -0
  128. package/src/providers/futunn/executors.js +24 -0
  129. package/src/providers/futunn/runtime-research.d.ts +3 -0
  130. package/src/providers/futunn/runtime-research.js +133 -0
  131. package/src/providers/futunn/runtime-trading.d.ts +3 -0
  132. package/src/providers/futunn/runtime-trading.js +39 -0
  133. package/src/providers/futunn/runtime.d.ts +2 -0
  134. package/src/providers/futunn/runtime.js +50 -0
  135. package/src/providers/futunn/transport.d.ts +11 -0
  136. package/src/providers/futunn/transport.js +76 -0
  137. package/src/providers/gildata/executors.js +1 -1
  138. package/src/providers/gmail/attachment-stream.d.ts +2 -0
  139. package/src/providers/gmail/attachment-stream.js +162 -0
  140. package/src/providers/gmail/executors.d.ts +3 -1
  141. package/src/providers/gmail/executors.js +54 -26
  142. package/src/providers/gmail/message.d.ts +6 -8
  143. package/src/providers/gmail/message.js +4 -0
  144. package/src/providers/gmail/scopes.d.ts +32 -0
  145. package/src/providers/gmail/scopes.js +37 -0
  146. package/src/providers/google_analytics/executors.js +2 -2
  147. package/src/providers/google_analytics/scopes.js +6 -1
  148. package/src/providers/google_bigquery/executors.js +2 -2
  149. package/src/providers/google_bigquery/scopes.js +2 -0
  150. package/src/providers/google_search_console/executors.js +2 -2
  151. package/src/providers/google_search_console/scopes.js +2 -0
  152. package/src/providers/googleads/executors.js +1 -1
  153. package/src/providers/googleads/scopes.d.ts +1 -3
  154. package/src/providers/googleads/scopes.js +2 -4
  155. package/src/providers/googlecalendar/executors.js +1 -1
  156. package/src/providers/googlecalendar/runtime-shared.js +2 -2
  157. package/src/providers/googlecalendar/scopes.js +2 -0
  158. package/src/providers/googlechat/executors.d.ts +1 -3
  159. package/src/providers/googlechat/executors.js +219 -28
  160. package/src/providers/googlechat/members.d.ts +55 -0
  161. package/src/providers/googlechat/members.js +293 -0
  162. package/src/providers/googlechat/runtime.d.ts +16 -0
  163. package/src/providers/googlechat/runtime.js +25 -0
  164. package/src/providers/googlechat/scopes.d.ts +22 -3
  165. package/src/providers/googlechat/scopes.js +30 -7
  166. package/src/providers/googledocs/executors.js +1 -1
  167. package/src/providers/googledrive/executors.js +2 -2
  168. package/src/providers/{google-auth.d.ts → googledrive/runtime-auth.d.ts} +2 -2
  169. package/src/providers/{google-auth.js → googledrive/runtime-auth.js} +3 -3
  170. package/src/providers/googledrive/runtime-collaboration.js +1 -1
  171. package/src/providers/{google-runtime.d.ts → googledrive/runtime-request.d.ts} +1 -1
  172. package/src/providers/{google-runtime.js → googledrive/runtime-request.js} +2 -2
  173. package/src/providers/googleforms/executors.js +2 -2
  174. package/src/providers/googleforms/scopes.d.ts +0 -3
  175. package/src/providers/googleforms/scopes.js +2 -6
  176. package/src/providers/googlemeet/executors.js +2 -2
  177. package/src/providers/googlemeet/scopes.d.ts +0 -3
  178. package/src/providers/googlemeet/scopes.js +2 -6
  179. package/src/providers/googlesheets/executors.js +1 -1
  180. package/src/providers/googlesheets/scopes.js +2 -0
  181. package/src/providers/googleslides/executors.js +2 -2
  182. package/src/providers/googleslides/scopes.d.ts +0 -3
  183. package/src/providers/googleslides/scopes.js +2 -6
  184. package/src/providers/googletasks/executors.js +2 -2
  185. package/src/providers/googletasks/scopes.d.ts +0 -3
  186. package/src/providers/googletasks/scopes.js +2 -6
  187. package/src/providers/granola/actions.js +2 -0
  188. package/src/providers/granola/runtime-mcp.js +2 -0
  189. package/src/providers/granola/runtime.js +2 -2
  190. package/src/providers/indiegogo/executors.js +1 -1
  191. package/src/providers/investoday_mcp/executors.js +2 -2
  192. package/src/providers/investoday_mcp/official-actions.js +19 -372
  193. package/src/providers/investoday_mcp/official-tool-types.d.ts +10 -0
  194. package/src/providers/investoday_mcp/official-tool-types.js +4 -0
  195. package/src/providers/investoday_mcp/official-tools/charts.d.ts +2 -0
  196. package/src/providers/investoday_mcp/official-tools/charts.js +32 -0
  197. package/src/providers/investoday_mcp/official-tools/fixed-income.d.ts +2 -0
  198. package/src/providers/investoday_mcp/official-tools/fixed-income.js +326 -0
  199. package/src/providers/investoday_mcp/official-tools/funds.d.ts +2 -0
  200. package/src/providers/investoday_mcp/official-tools/funds.js +1085 -0
  201. package/src/providers/investoday_mcp/official-tools/hong-kong.d.ts +2 -0
  202. package/src/providers/investoday_mcp/official-tools/hong-kong.js +275 -0
  203. package/src/providers/investoday_mcp/official-tools/indices-industries.d.ts +2 -0
  204. package/src/providers/investoday_mcp/official-tools/indices-industries.js +642 -0
  205. package/src/providers/investoday_mcp/official-tools/macro-calendar.d.ts +2 -0
  206. package/src/providers/investoday_mcp/official-tools/macro-calendar.js +398 -0
  207. package/src/providers/investoday_mcp/official-tools/other.d.ts +2 -0
  208. package/src/providers/investoday_mcp/official-tools/other.js +131 -0
  209. package/src/providers/investoday_mcp/official-tools/research-news.d.ts +2 -0
  210. package/src/providers/investoday_mcp/official-tools/research-news.js +302 -0
  211. package/src/providers/investoday_mcp/official-tools/stocks.d.ts +2 -0
  212. package/src/providers/investoday_mcp/official-tools/stocks.js +2829 -0
  213. package/src/providers/ixspy/executors.d.ts +3 -0
  214. package/src/providers/ixspy/executors.js +220 -0
  215. package/src/providers/iyunbiao/runtime.js +2 -2
  216. package/src/providers/keepa/actions.d.ts +1 -1
  217. package/src/providers/keepa/actions.js +371 -149
  218. package/src/providers/keepa/deal-attributes.d.ts +1 -0
  219. package/src/providers/keepa/deal-attributes.js +26 -0
  220. package/src/providers/keepa/runtime.js +212 -18
  221. package/src/providers/kingdee/executors.js +1 -1
  222. package/src/providers/kingdee/runtime.js +2 -2
  223. package/src/providers/kuaidi100/runtime.js +1 -1
  224. package/src/providers/linear/executors.js +12 -9
  225. package/src/providers/linear_mcp/actions.d.ts +47 -0
  226. package/src/providers/linear_mcp/actions.js +102 -0
  227. package/src/providers/linear_mcp/endpoints.d.ts +3 -0
  228. package/src/providers/linear_mcp/endpoints.js +3 -0
  229. package/src/providers/linear_mcp/executors.d.ts +3 -0
  230. package/src/providers/linear_mcp/executors.js +9 -0
  231. package/src/providers/linear_mcp/runtime-mcp.d.ts +15 -0
  232. package/src/providers/linear_mcp/runtime-mcp.js +238 -0
  233. package/src/providers/lingxing_mcp/runtime.js +1 -1
  234. package/src/providers/mcdonalds_cn_mcp/runtime.js +1 -1
  235. package/src/providers/metabase/mcp-runtime.js +1 -1
  236. package/src/providers/moneyforward_mcp/executors.d.ts +3 -0
  237. package/src/providers/moneyforward_mcp/executors.js +11 -0
  238. package/src/providers/moneyforward_mcp/manifest.d.ts +31 -0
  239. package/src/providers/moneyforward_mcp/manifest.js +204 -0
  240. package/src/providers/moneyforward_mcp/runtime.d.ts +18 -0
  241. package/src/providers/moneyforward_mcp/runtime.js +296 -0
  242. package/src/providers/notion/executors.js +39 -3
  243. package/src/providers/notion_mcp/endpoints.d.ts +3 -0
  244. package/src/providers/notion_mcp/endpoints.js +3 -0
  245. package/src/providers/notion_mcp/executors.d.ts +3 -0
  246. package/src/providers/notion_mcp/executors.js +176 -0
  247. package/src/providers/notion_mcp/mcp-response.d.ts +86 -0
  248. package/src/providers/notion_mcp/mcp-response.js +314 -0
  249. package/src/providers/odoo/executors.d.ts +3 -0
  250. package/src/providers/odoo/executors.js +32 -0
  251. package/src/providers/odoo/runtime.d.ts +16 -0
  252. package/src/providers/odoo/runtime.js +140 -0
  253. package/src/providers/osv/executors.d.ts +2 -0
  254. package/src/providers/osv/executors.js +11 -0
  255. package/src/providers/osv/runtime.d.ts +9 -0
  256. package/src/providers/osv/runtime.js +66 -0
  257. package/src/providers/outlook/executors.js +27 -6
  258. package/src/providers/outlook/microsoft-graph.js +17 -6
  259. package/src/providers/outlook_calendar/executors.js +24 -7
  260. package/src/providers/patsnap_mcp/executors.js +2 -2
  261. package/src/providers/pdf_vector/executors.d.ts +4 -0
  262. package/src/providers/pdf_vector/executors.js +108 -0
  263. package/src/providers/postiz/executors.d.ts +4 -0
  264. package/src/providers/postiz/executors.js +83 -0
  265. package/src/providers/provider-runtime.d.ts +18 -0
  266. package/src/providers/provider-runtime.js +60 -4
  267. package/src/providers/proworkflow/executors.d.ts +4 -0
  268. package/src/providers/proworkflow/executors.js +135 -0
  269. package/src/providers/pulsetic/executors.d.ts +4 -0
  270. package/src/providers/pulsetic/executors.js +126 -0
  271. package/src/providers/registry.generated.js +24 -0
  272. package/src/providers/scrape_creators/catalog.js +1 -1
  273. package/src/providers/scrape_creators/runtime.js +1 -1
  274. package/src/providers/sellersprite_mcp/executors.js +2 -2
  275. package/src/providers/sellersprite_mcp/official-actions.js +3 -2
  276. package/src/providers/sentry/runtime.js +6 -7
  277. package/src/providers/shopee/actions.d.ts +2 -0
  278. package/src/providers/shopee/actions.js +474 -0
  279. package/src/providers/shopee/executors.d.ts +3 -0
  280. package/src/providers/shopee/executors.js +85 -0
  281. package/src/providers/shopee/runtime.d.ts +79 -0
  282. package/src/providers/shopee/runtime.js +693 -0
  283. package/src/providers/slack/actions.js +69 -3
  284. package/src/providers/slack/runtime.js +116 -21
  285. package/src/providers/slickdeals/runtime.js +1 -1
  286. package/src/providers/sorftime_mcp/executors.js +2 -2
  287. package/src/providers/speak_ai/executors.d.ts +3 -0
  288. package/src/providers/speak_ai/executors.js +187 -0
  289. package/src/providers/tikhub/bounded-response.d.ts +1 -1
  290. package/src/providers/tikhub/bounded-response.js +8 -34
  291. package/src/providers/tikhub/endpoint-catalog.js +1 -1
  292. package/src/providers/tikhub/runtime.js +2 -2
  293. package/src/providers/tongdaxin/executors.js +79 -15
  294. package/src/providers/tushare/runtime.js +22 -9
  295. package/src/providers/uspto/executors.js +1 -1
  296. package/src/providers/waffo/executors.js +1 -1
  297. package/src/providers/waffo/runtime.js +1 -1
  298. package/src/providers/wecom_mcp/runtime.js +1 -1
  299. package/src/providers/wisburg/executors.d.ts +4 -0
  300. package/src/providers/wisburg/executors.js +13 -0
  301. package/src/providers/wisburg/runtime.d.ts +12 -0
  302. package/src/providers/wisburg/runtime.js +133 -0
  303. package/src/providers/xydc_mcp/executors.js +2 -2
  304. package/src/providers/youtube/executors.js +1 -1
  305. package/src/providers/youtube/runtime.js +1 -1
  306. package/src/providers/zerotier/executors.d.ts +9 -0
  307. package/src/providers/zerotier/executors.js +629 -0
  308. package/src/providers/zerotier/runtime.d.ts +54 -0
  309. package/src/providers/zerotier/runtime.js +207 -0
  310. package/src/providers/zyhub/executors.d.ts +3 -0
  311. package/src/providers/zyhub/executors.js +240 -0
  312. package/src/server/api/openapi.js +12 -2
  313. package/src/server/api/runtime-api.d.ts +8 -1
  314. package/src/server/api/runtime-api.js +19 -1
  315. package/src/server/connect-server.js +10 -2
  316. package/src/server/files/transit-files.d.ts +3 -2
  317. package/src/server/files/transit-files.js +31 -35
  318. package/src/providers/sentry/scopes.d.ts +0 -1
  319. package/src/providers/sentry/scopes.js +0 -7
  320. /package/src/providers/{cloudflare-current-user.d.ts → cloudflare_dns/runtime-user.d.ts} +0 -0
@@ -1,27 +1,50 @@
1
1
  import { compactObject, optionalBoolean, optionalInteger, optionalRawString, optionalRecord, optionalString, } from "../../core/cast.js";
2
- import { defineGoogleProviderExecutors, googleBearerProxyAuth, googleServiceAccountValidator } from "../google-auth.js";
3
- import { googleJsonRequest } from "../google-runtime.js";
2
+ import { defineGoogleProviderExecutors, googleBearerProxyAuth, googleServiceAccountValidator, } from "../googledrive/runtime-auth.js";
3
+ import { googleJsonRequest } from "../googledrive/runtime-request.js";
4
4
  import { asObject } from "../googledrive/runtime-shared.js";
5
- import { defineProviderProxy, ProviderRequestError } from "../provider-runtime.js";
6
- import { googleChatOAuthScopes } from "./scopes.js";
7
- export const googleChatApiBaseUrl = "https://chat.googleapis.com/v1";
5
+ import { defineProviderProxy, ProviderRequestError, providerResponseError, requiredResponseRecord, } from "../provider-runtime.js";
6
+ import { attachSenderProfiles, listSpaceMembersPage, maxProfilesPerLookup, resolveDirectMessagePeer, } from "./members.js";
7
+ import { encodeResourceName, googleChatApiBaseUrl, googleChatJsonRequest, stripPrefix } from "./runtime.js";
8
+ import { googleChatServiceAccountScopes } from "./scopes.js";
8
9
  const service = "googlechat";
10
+ const defaultMemberPageSize = 100;
9
11
  const googleUserInfoUrl = "https://www.googleapis.com/oauth2/v3/userinfo";
10
12
  const spaceNamePattern = /^spaces\/[^/]+$/;
11
13
  const messageNamePattern = /^spaces\/[^/]+\/messages\/[^/]+$/;
14
+ const threadNamePattern = /^spaces\/[^/]+\/threads\/[^/]+$/;
15
+ // findDirectMessage takes the user name as a query parameter, not a path segment,
16
+ // so it is deliberately NOT percent-encoded here: googleRequest already encodes it
17
+ // once through URLSearchParams.set(), and encoding it again would turn a@b.com into
18
+ // a%2540b.com, which Google reads as a literal rather than an email address.
19
+ //
20
+ // Both patterns are allowlists of the expected structure, not denylists, so
21
+ // zero-width and control characters, slashes, backslashes, and inner whitespace
22
+ // all fall outside them without being rejected one by one. Domains accept only
23
+ // ASCII or punycode.
24
+ const userIdPattern = /^[0-9]{1,64}$/;
25
+ // RFC 5322 allows "/" in the local part, but it is deliberately excluded here:
26
+ // otherwise "users/person@example.com" would pass as one valid address and be sent
27
+ // to Google as "users/users/person@example.com". A test pins this down.
28
+ const emailLocalPart = "[A-Za-z0-9!#$%&'*+=?^_`{|}~-]+(?:\\.[A-Za-z0-9!#$%&'*+=?^_`{|}~-]+)*";
29
+ const emailDomainLabel = "[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?";
30
+ const userEmailPattern = new RegExp(`^${emailLocalPart}@${emailDomainLabel}(?:\\.${emailDomainLabel})+$`);
12
31
  export const googleChatActionHandlers = {
13
32
  list_spaces: listSpaces,
14
33
  get_space: getSpace,
15
34
  list_messages: listMessages,
16
35
  get_message: getMessage,
36
+ create_message: createMessage,
37
+ find_direct_message: findDirectMessage,
38
+ get_direct_message_peer: getDirectMessagePeer,
39
+ list_space_members: listSpaceMembers,
17
40
  };
18
41
  export const executors = defineGoogleProviderExecutors(service, googleChatActionHandlers, {
19
- scopes: googleChatOAuthScopes,
42
+ scopes: googleChatServiceAccountScopes,
20
43
  });
21
44
  export const proxy = defineProviderProxy({
22
45
  service,
23
46
  baseUrl: googleChatApiBaseUrl,
24
- auth: googleBearerProxyAuth(googleChatOAuthScopes),
47
+ auth: googleBearerProxyAuth(googleChatServiceAccountScopes),
25
48
  skipDnsValidation: true,
26
49
  });
27
50
  export const credentialValidators = {
@@ -42,7 +65,7 @@ export const credentialValidators = {
42
65
  },
43
66
  };
44
67
  },
45
- customCredential: googleServiceAccountValidator(service, googleChatOAuthScopes),
68
+ customCredential: googleServiceAccountValidator(service, googleChatServiceAccountScopes),
46
69
  };
47
70
  async function listSpaces(input, context) {
48
71
  const payload = await googleChatJsonRequest(`${googleChatApiBaseUrl}/spaces`, {
@@ -65,6 +88,129 @@ async function getSpace(input, context) {
65
88
  });
66
89
  return normalizeSpace(payload);
67
90
  }
91
+ /**
92
+ * Locate the existing direct message space between the authenticated user and
93
+ * another user. This is the only way to address a person by identity rather than
94
+ * by an opaque space id: a DIRECT_MESSAGE space carries no displayName, so
95
+ * `list_spaces` alone can never tell you who a DM is with.
96
+ *
97
+ * Finding the space needs nothing beyond chat.spaces.readonly, which the read
98
+ * actions already use. Naming its peer needs more, so that part is best effort.
99
+ */
100
+ async function findDirectMessage(input, context) {
101
+ const name = resolveUserName(input.user);
102
+ let payload;
103
+ try {
104
+ payload = await googleChatJsonRequest(`${googleChatApiBaseUrl}/spaces:findDirectMessage`, {
105
+ context,
106
+ query: { name },
107
+ });
108
+ }
109
+ catch (error) {
110
+ // Google answers 404 for "no such DM", but also for an address that does not
111
+ // resolve or is not visible to this account. Don't collapse those into
112
+ // "you haven't talked yet" — say what is actually known.
113
+ if (error instanceof ProviderRequestError && error.status === 404) {
114
+ throw new ProviderRequestError(404, `no existing direct message space with ${name}. Either the two of you have never started a conversation, or the identifier is invalid or not visible to this account.`, error.details);
115
+ }
116
+ // An address Google cannot resolve to an account comes back as 400
117
+ // "Missing or malformed user resource name ... must follow this format:
118
+ // users/{user}". The format was not the problem — that is Google's wording
119
+ // for "no such user", and taken at face value it sends the caller off to add
120
+ // a users/ prefix, which this action deliberately rejects. So relabel it.
121
+ //
122
+ // Deliberately matched on that specific wording rather than on "any 400":
123
+ // our own validation already ran, so a 400 almost always concerns the
124
+ // address, but "almost always" is not "always", and mislabelling some other
125
+ // 400 as a bad address would point debugging in the wrong direction. If
126
+ // Google ever rewords this, the raw error comes through unchanged — the
127
+ // status quo, not a regression.
128
+ if (error instanceof ProviderRequestError && error.status === 400 && /user resource name/i.test(error.message)) {
129
+ throw new ProviderRequestError(400, `Google did not accept ${name} as a user identifier. The value is well-formed here, so this usually means it does not resolve to a Google account that this connection can see — check the address. Google's own wording asks for users/{user}; do not add that prefix, this action takes the bare address or id.`, error.details);
130
+ }
131
+ throw error;
132
+ }
133
+ const space = normalizeSpace(payload);
134
+ const spaceName = requirePayloadString(space.name, "Google Chat returned a space without a resource name");
135
+ try {
136
+ return { ...space, peer: await resolveDirectMessagePeer(spaceName, context) };
137
+ }
138
+ catch (error) {
139
+ // The space was found; failing to name its other member must not hide that.
140
+ // Report why instead, so the caller knows the recipient is unconfirmed. Only a
141
+ // cancelled request stops the action. Listing the members and reading the
142
+ // caller's own id already say which of them failed, including a network error
143
+ // or a response that is not JSON as a 502; the prefix below only covers a
144
+ // failure neither of them anticipated.
145
+ if (context.signal?.aborted) {
146
+ throw error;
147
+ }
148
+ const failure = error instanceof ProviderRequestError
149
+ ? error
150
+ : providerResponseError(`could not resolve the direct message peer: ${error instanceof Error ? error.message : String(error)}`);
151
+ return { ...space, peer: null, peerError: { status: failure.status, message: failure.message } };
152
+ }
153
+ }
154
+ /**
155
+ * Name the other participant of a direct message space. Under user authentication
156
+ * Chat may report a member only as `users/{id}`, so whatever it leaves out is
157
+ * looked up in the Workspace directory through the People API, where the same id
158
+ * names the person.
159
+ */
160
+ async function getDirectMessagePeer(input, context) {
161
+ const spaceName = resolveSpaceName(input.space, "space is required");
162
+ const space = requiredResponseRecord(await googleChatJsonRequest(`${googleChatApiBaseUrl}/${encodeResourceName(spaceName)}`, { context }), "Google Chat space");
163
+ // Google always reports the type of a space it returns, so a missing one is a
164
+ // malformed response rather than something wrong with the caller's input.
165
+ const spaceType = requirePayloadString(space.spaceType, `Google Chat returned ${spaceName} without a space type`);
166
+ if (spaceType !== "DIRECT_MESSAGE") {
167
+ // Every member of a named space would otherwise be a "peer" candidate, and
168
+ // calling one of them the peer would be a guess presented as a fact.
169
+ throw new ProviderRequestError(400, `${spaceName} is ${spaceType}, not a direct message`);
170
+ }
171
+ return { space: spaceName, peer: await resolveDirectMessagePeer(spaceName, context) };
172
+ }
173
+ /**
174
+ * List a space's members with their directory names, one page at a time. Works
175
+ * for any space type; unlike get_direct_message_peer it names everyone and makes
176
+ * no claim about who "the" other participant is.
177
+ */
178
+ async function listSpaceMembers(input, context) {
179
+ const spaceName = resolveSpaceName(input.space, "space is required");
180
+ const pageSize = resolvePageSize(input.pageSize);
181
+ const page = await listSpaceMembersPage(spaceName, { pageSize, pageToken: optionalString(input.pageToken) }, context);
182
+ return { space: spaceName, ...page };
183
+ }
184
+ /**
185
+ * Chat allows up to 1000 members per page, but every human on a page is named in
186
+ * one People batch that takes at most 200, so larger pages are refused rather than
187
+ * silently split into several lookups.
188
+ */
189
+ function resolvePageSize(value) {
190
+ if (value === undefined || value === null) {
191
+ return defaultMemberPageSize;
192
+ }
193
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > maxProfilesPerLookup) {
194
+ throw new ProviderRequestError(400, `pageSize must be an integer from 1 to ${maxProfilesPerLookup}`);
195
+ }
196
+ return value;
197
+ }
198
+ /**
199
+ * Accept a bare user id or a bare email address and build `users/{user}`.
200
+ *
201
+ * Deliberately narrow: no `users/` prefix (one input shape, no ambiguity), no
202
+ * `me`/`app` aliases, nothing containing a path separator or whitespace. Case is
203
+ * preserved and nothing is percent-decoded — the value is passed through as-is.
204
+ */
205
+ function resolveUserName(value) {
206
+ const raw = requireString(value, "user is required");
207
+ const trimmed = raw.trim();
208
+ const wellFormed = userIdPattern.test(trimmed) || userEmailPattern.test(trimmed);
209
+ if (trimmed.length > 320 || !wellFormed) {
210
+ throw new ProviderRequestError(400, `user must be an email address or a numeric user id, received "${raw}"`);
211
+ }
212
+ return `users/${trimmed}`;
213
+ }
68
214
  async function listMessages(input, context) {
69
215
  const spaceName = resolveSpaceName(input.space, "space is required");
70
216
  const payload = await googleChatJsonRequest(`${googleChatApiBaseUrl}/${encodeResourceName(spaceName)}/messages`, {
@@ -78,7 +224,7 @@ async function listMessages(input, context) {
78
224
  }),
79
225
  });
80
226
  return {
81
- messages: Array.isArray(payload.messages) ? payload.messages.map((item) => normalizeMessage(item)) : [],
227
+ messages: await attachSenderProfiles(Array.isArray(payload.messages) ? payload.messages.map((item) => normalizeMessage(item)) : [], context),
82
228
  nextPageToken: optionalString(payload.nextPageToken) ?? null,
83
229
  };
84
230
  }
@@ -87,6 +233,36 @@ async function getMessage(input, context) {
87
233
  const payload = await googleChatJsonRequest(`${googleChatApiBaseUrl}/${encodeResourceName(messageName)}`, {
88
234
  context,
89
235
  });
236
+ const [message] = await attachSenderProfiles([normalizeMessage(payload)], context);
237
+ return message;
238
+ }
239
+ async function createMessage(input, context) {
240
+ const spaceName = resolveSpaceName(input.space, "space is required");
241
+ const text = requireRawText(input.text, "text is required");
242
+ // Read the thread raw: optionalString() trims and then treats the result as
243
+ // falsy, which would turn a whitespace-only thread into "no thread given" and
244
+ // silently post a new message instead of the requested reply. Anything the
245
+ // caller actually supplied has to reach resolveThreadName and fail loudly.
246
+ const rawThread = optionalRawString(input.thread);
247
+ const threadName = rawThread === undefined ? undefined : resolveThreadName(rawThread, spaceName);
248
+ const replyOption = optionalString(input.messageReplyOption);
249
+ if (replyOption && !threadName) {
250
+ throw new ProviderRequestError(400, "messageReplyOption is only allowed together with thread");
251
+ }
252
+ const payload = await googleChatJsonRequest(`${googleChatApiBaseUrl}/${encodeResourceName(spaceName)}/messages`, {
253
+ context,
254
+ method: "POST",
255
+ query: compactObject({
256
+ // Google's own default silently ignores `thread` and starts a new one, so a
257
+ // caller that supplied a thread gets an explicit failure instead.
258
+ messageReplyOption: replyOption ?? (threadName ? "REPLY_MESSAGE_OR_FAIL" : undefined),
259
+ requestId: optionalString(input.requestId),
260
+ }),
261
+ body: compactObject({
262
+ text,
263
+ thread: threadName ? { name: threadName } : undefined,
264
+ }),
265
+ });
90
266
  return normalizeMessage(payload);
91
267
  }
92
268
  function normalizeSpace(value) {
@@ -134,6 +310,7 @@ function normalizeMessage(value) {
134
310
  ? compactObject({
135
311
  name: optionalString(sender.name),
136
312
  displayName: optionalString(sender.displayName),
313
+ email: optionalString(sender.email),
137
314
  type: optionalString(sender.type),
138
315
  domainId: optionalString(sender.domainId),
139
316
  isAnonymous: optionalBoolean(sender.isAnonymous),
@@ -186,25 +363,27 @@ function resolveMessageName(input) {
186
363
  }
187
364
  return name;
188
365
  }
189
- function googleChatJsonRequest(url, input) {
190
- return googleJsonRequest(url, {
191
- accessToken: input.context.accessToken,
192
- fetcher: input.context.fetcher,
193
- signal: input.context.signal,
194
- method: input.method,
195
- query: input.query,
196
- body: input.body,
197
- service,
198
- });
199
- }
200
- function encodeResourceName(name) {
201
- return name
202
- .split("/")
203
- .map((segment) => encodeURIComponent(segment))
204
- .join("/");
205
- }
206
- function stripPrefix(value, prefix) {
207
- return value.startsWith(prefix) ? value.slice(prefix.length) : value;
366
+ /**
367
+ * Accept only the full `spaces/{space}/threads/{thread}` resource name, and require
368
+ * it to belong to the space being posted into. The thread travels in the request
369
+ * body rather than the URL, so this is a contract check rather than traversal
370
+ * defence: it turns a cross-space or malformed thread into a local 400 instead of
371
+ * an opaque remote error.
372
+ */
373
+ function resolveThreadName(value, parentSpaceName) {
374
+ const trimmed = trimSlashes(value.trim());
375
+ const [, spaceId, , threadId] = trimmed.split("/");
376
+ if (!threadNamePattern.test(trimmed) ||
377
+ spaceId === "." ||
378
+ spaceId === ".." ||
379
+ threadId === "." ||
380
+ threadId === "..") {
381
+ throw new ProviderRequestError(400, `thread must be spaces/{space}/threads/{thread}, received "${value}"`);
382
+ }
383
+ if (`spaces/${spaceId}` !== parentSpaceName) {
384
+ throw new ProviderRequestError(400, `thread must belong to ${parentSpaceName}, received "${value}"`);
385
+ }
386
+ return trimmed;
208
387
  }
209
388
  function trimSlashes(value) {
210
389
  return value.replace(/^\/+/, "").replace(/\/+$/, "");
@@ -224,6 +403,18 @@ function requireString(value, message) {
224
403
  }
225
404
  return resolved;
226
405
  }
406
+ /**
407
+ * Like `requireString`, but keeps the value exactly as provided. Message bodies
408
+ * carry meaningful indentation, so the outgoing text must not be trimmed — while a
409
+ * body that is only whitespace is still rejected.
410
+ */
411
+ function requireRawText(value, message) {
412
+ const resolved = optionalRawString(value);
413
+ if (resolved === undefined || resolved.trim().length === 0) {
414
+ throw new ProviderRequestError(400, message);
415
+ }
416
+ return resolved;
417
+ }
227
418
  function requirePayloadString(value, message) {
228
419
  const resolved = optionalString(value);
229
420
  if (!resolved) {
@@ -0,0 +1,55 @@
1
+ import type { GoogleChatRuntimeContext } from "./runtime.ts";
2
+ /** people:batchGet accepts at most this many resource names per call. */
3
+ export declare const maxProfilesPerLookup = 200;
4
+ type ProfileUnavailableReason = "profile_name_missing" | "people_forbidden" | "people_not_found" | "people_request_failed";
5
+ interface MemberProfile {
6
+ displayName: string | null;
7
+ email: string | null;
8
+ profileUnavailableReason?: ProfileUnavailableReason;
9
+ }
10
+ type DirectMessagePeerKind = "HUMAN" | "BOT" | "SELF" | "AMBIGUOUS";
11
+ interface DirectMessagePeer extends MemberProfile {
12
+ kind: DirectMessagePeerKind;
13
+ user: string | null;
14
+ candidates?: string[];
15
+ }
16
+ interface SpaceMember extends MemberProfile {
17
+ user: string;
18
+ kind: string | undefined;
19
+ role: string | undefined;
20
+ isSelf: boolean;
21
+ }
22
+ interface SpaceMembersPage {
23
+ members: SpaceMember[];
24
+ nextPageToken: string | null;
25
+ }
26
+ interface SpaceMembersPageRequest {
27
+ pageSize: number;
28
+ pageToken: string | undefined;
29
+ /** Also list members who were invited but have not joined yet. */
30
+ showInvited?: boolean;
31
+ }
32
+ /**
33
+ * Name the other participant of a direct message. The authenticated user and the
34
+ * peer are told apart by id, because a Chat membership never says which member is
35
+ * "me". BOT and SELF are ordinary outcomes, not failures: they keep whatever name
36
+ * and email Chat reports for the membership and skip the directory.
37
+ */
38
+ export declare function resolveDirectMessagePeer(spaceName: string, context: GoogleChatRuntimeContext): Promise<DirectMessagePeer>;
39
+ /**
40
+ * One page of a space's members. Every human Google Chat left without a name or
41
+ * email is looked up in a single batched directory call; the page size is capped
42
+ * so one call always covers the page.
43
+ */
44
+ export declare function listSpaceMembersPage(spaceName: string, request: SpaceMembersPageRequest, context: GoogleChatRuntimeContext): Promise<SpaceMembersPage>;
45
+ /**
46
+ * Fill in each human sender's name and email on normalized messages. Under user
47
+ * authentication Chat reports them itself only for members of the space and users
48
+ * with prior affinity to the caller, so every human sender missing either is
49
+ * looked up in the directory. Distinct senders are looked up together, so a page
50
+ * costs one extra request per 200 senders. A failed lookup leaves the message
51
+ * intact with a null name and a reason; bots and messages without a sender are
52
+ * left as they are.
53
+ */
54
+ export declare function attachSenderProfiles(messages: Record<string, unknown>[], context: GoogleChatRuntimeContext): Promise<Record<string, unknown>[]>;
55
+ export {};
@@ -0,0 +1,293 @@
1
+ import { compactObject, looseArray, optionalRecord, optionalString, recordOrEmpty } from "../../core/cast.js";
2
+ import { ProviderRequestError, providerResponseError } from "../provider-runtime.js";
3
+ import { encodeResourceName, googleChatApiBaseUrl, googleChatJsonRequest, stripPrefix } from "./runtime.js";
4
+ const peopleApiBaseUrl = "https://people.googleapis.com/v1";
5
+ // A direct message has at most two members, so this only guards against a
6
+ // pagination token that never runs out.
7
+ const maxDirectMessageMemberPages = 20;
8
+ /** people:batchGet accepts at most this many resource names per call. */
9
+ export const maxProfilesPerLookup = 200;
10
+ // google.rpc.Code values a per-person batchGet status can carry.
11
+ const rpcNotFound = 5;
12
+ const rpcPermissionDenied = 7;
13
+ /**
14
+ * Name the other participant of a direct message. The authenticated user and the
15
+ * peer are told apart by id, because a Chat membership never says which member is
16
+ * "me". BOT and SELF are ordinary outcomes, not failures: they keep whatever name
17
+ * and email Chat reports for the membership and skip the directory.
18
+ */
19
+ export async function resolveDirectMessagePeer(spaceName, context) {
20
+ const [selfUser, members] = await Promise.all([readSelfUserName(context), listAllMembers(spaceName, context)]);
21
+ const humans = members.filter((member) => member.type === "HUMAN");
22
+ const bots = members.filter((member) => member.type === "BOT");
23
+ const others = humans.filter((member) => member.name !== selfUser);
24
+ const self = humans.find((member) => member.name === selfUser);
25
+ if (others.length === 1) {
26
+ const [peer] = others;
27
+ const profiles = await lookupProfiles(missingChatProfiles(others), context);
28
+ return { kind: "HUMAN", user: peer.name, ...mergeProfile(peer, profiles.get(peer.name)) };
29
+ }
30
+ if (others.length > 1) {
31
+ return ambiguousPeer(others);
32
+ }
33
+ if (bots.length === 1) {
34
+ const [peer] = bots;
35
+ return { kind: "BOT", user: peer.name, ...mergeProfile(peer, undefined) };
36
+ }
37
+ if (bots.length === 0 && self) {
38
+ return { kind: "SELF", user: self.name, ...mergeProfile(self, undefined) };
39
+ }
40
+ return ambiguousPeer(bots);
41
+ }
42
+ /**
43
+ * One page of a space's members. Every human Google Chat left without a name or
44
+ * email is looked up in a single batched directory call; the page size is capped
45
+ * so one call always covers the page.
46
+ */
47
+ export async function listSpaceMembersPage(spaceName, request, context) {
48
+ const [selfUser, page] = await Promise.all([readSelfUserName(context), fetchMemberPage(spaceName, request, context)]);
49
+ const humans = page.members.filter((member) => member.type === "HUMAN");
50
+ const profiles = await lookupProfiles(missingChatProfiles(humans), context);
51
+ return {
52
+ members: page.members.map((member) => ({
53
+ user: member.name,
54
+ kind: member.type,
55
+ role: member.role,
56
+ isSelf: member.name === selfUser,
57
+ ...mergeProfile(member, profiles.get(member.name)),
58
+ })),
59
+ nextPageToken: page.nextPageToken ?? null,
60
+ };
61
+ }
62
+ function ambiguousPeer(candidates) {
63
+ return {
64
+ kind: "AMBIGUOUS",
65
+ user: null,
66
+ displayName: null,
67
+ email: null,
68
+ candidates: candidates.map((member) => member.name),
69
+ };
70
+ }
71
+ /** The authenticated user's own `users/{id}`; People and Chat share the id. */
72
+ async function readSelfUserName(context) {
73
+ let payload;
74
+ try {
75
+ payload = recordOrEmpty(await googleChatJsonRequest(`${peopleApiBaseUrl}/people/me`, {
76
+ context,
77
+ query: { personFields: "metadata" },
78
+ }));
79
+ }
80
+ catch (error) {
81
+ if (context.signal?.aborted) {
82
+ throw error;
83
+ }
84
+ // Only a 403 can mean the People API is not enabled for the project.
85
+ throw explainLookupError("could not read the authenticated user's own id from the People API, so members cannot be told apart from the caller", error, "The People API must be enabled for the OAuth client's Google Cloud project.");
86
+ }
87
+ const resourceName = optionalString(payload.resourceName);
88
+ if (!resourceName?.startsWith("people/")) {
89
+ throw new ProviderRequestError(502, "the People API returned no resource name for the authenticated user");
90
+ }
91
+ return `users/${stripPrefix(resourceName, "people/")}`;
92
+ }
93
+ /**
94
+ * Say which Chat or People lookup failed. An HTTP failure keeps its status and
95
+ * details and gains forbiddenHint on a 403, the only status a console or consent
96
+ * change can fix; a 429, a 5xx, or a timeout needs a retry instead. A failure that
97
+ * is not an HTTP error, such as a response that is not JSON, becomes a 502 that
98
+ * names the lookup instead of reaching the caller as a raw internal error.
99
+ */
100
+ function explainLookupError(summary, error, forbiddenHint) {
101
+ if (!(error instanceof ProviderRequestError)) {
102
+ return providerResponseError(`${summary}: ${error instanceof Error ? error.message : String(error)}`);
103
+ }
104
+ const hint = error.status === 403 ? ` ${forbiddenHint}` : "";
105
+ // Google's messages usually end in a period already; drop it so the sentence ends once.
106
+ const detail = error.message.replace(/\.\s*$/, "");
107
+ return new ProviderRequestError(error.status, `${summary}: ${detail}.${hint}`, error.details);
108
+ }
109
+ async function listAllMembers(spaceName, context) {
110
+ let members = [];
111
+ let pageToken;
112
+ for (let page = 0; page < maxDirectMessageMemberPages; page += 1) {
113
+ let result;
114
+ try {
115
+ // Chat leaves invited members out by default. A peer who has not joined the
116
+ // conversation yet is still the peer; without it the caller would be the only
117
+ // member left and the direct message would be misreported as SELF.
118
+ result = await fetchMemberPage(spaceName, { pageSize: 100, pageToken, showInvited: true }, context);
119
+ }
120
+ catch (error) {
121
+ if (context.signal?.aborted) {
122
+ throw error;
123
+ }
124
+ // A 403 here is almost always the missing scope: the space itself was readable.
125
+ throw explainLookupError(`could not list the members of ${spaceName}, so the direct message's other participant is unknown`, error, "Listing members needs the chat.memberships.readonly scope, which service account connections never request; an OAuth connection authorized before this scope was added must be reconnected to grant it.");
126
+ }
127
+ members = [...members, ...result.members];
128
+ pageToken = result.nextPageToken;
129
+ if (!pageToken) {
130
+ return members;
131
+ }
132
+ }
133
+ throw new ProviderRequestError(502, `Google Chat kept paginating the members of ${spaceName}`);
134
+ }
135
+ async function fetchMemberPage(spaceName, request, context) {
136
+ const payload = await googleChatJsonRequest(`${googleChatApiBaseUrl}/${encodeResourceName(spaceName)}/members`, {
137
+ context,
138
+ query: compactObject({
139
+ pageSize: String(request.pageSize),
140
+ pageToken: request.pageToken,
141
+ showInvited: request.showInvited ? "true" : undefined,
142
+ }),
143
+ });
144
+ return {
145
+ members: looseArray(payload.memberships).flatMap((item) => toChatMember(item)),
146
+ nextPageToken: optionalString(payload.nextPageToken),
147
+ };
148
+ }
149
+ function toChatMember(value) {
150
+ const membership = recordOrEmpty(value);
151
+ const member = recordOrEmpty(membership.member);
152
+ const name = optionalString(member.name);
153
+ return name
154
+ ? [
155
+ {
156
+ name,
157
+ type: optionalString(member.type),
158
+ role: optionalString(membership.role),
159
+ displayName: optionalString(member.displayName),
160
+ email: optionalString(member.email),
161
+ },
162
+ ]
163
+ : [];
164
+ }
165
+ /**
166
+ * Fill in each human sender's name and email on normalized messages. Under user
167
+ * authentication Chat reports them itself only for members of the space and users
168
+ * with prior affinity to the caller, so every human sender missing either is
169
+ * looked up in the directory. Distinct senders are looked up together, so a page
170
+ * costs one extra request per 200 senders. A failed lookup leaves the message
171
+ * intact with a null name and a reason; bots and messages without a sender are
172
+ * left as they are.
173
+ */
174
+ export async function attachSenderProfiles(messages, context) {
175
+ const profiles = await lookupProfiles(missingChatProfiles(messages.flatMap((message) => humanSender(message))), context);
176
+ return messages.map((message) => {
177
+ const [sender] = humanSender(message);
178
+ return sender
179
+ ? { ...message, sender: { ...recordOrEmpty(message.sender), ...mergeProfile(sender, profiles.get(sender.name)) } }
180
+ : message;
181
+ });
182
+ }
183
+ /** The human users/{id} sender of a normalized message, if it has one. */
184
+ function humanSender(message) {
185
+ const sender = optionalRecord(message.sender);
186
+ const name = optionalString(sender?.name);
187
+ return sender?.type === "HUMAN" && name?.startsWith("users/")
188
+ ? [{ name, displayName: optionalString(sender.displayName), email: optionalString(sender.email) }]
189
+ : [];
190
+ }
191
+ /** The distinct users whose name or email Chat left out, which only the directory can fill in. */
192
+ function missingChatProfiles(users) {
193
+ const missing = users.filter((user) => user.displayName === undefined || user.email === undefined);
194
+ return [...new Set(missing.map((user) => user.name))];
195
+ }
196
+ /**
197
+ * Combine what Chat reported with the directory profile. Chat's own name and email
198
+ * win and the directory only fills in what Chat left out, so a failed lookup never
199
+ * replaces a known value with null. profileUnavailableReason explains a null
200
+ * displayName, so it is only kept while the name is still missing.
201
+ */
202
+ function mergeProfile(chat, directory) {
203
+ const displayName = chat.displayName ?? directory?.displayName ?? null;
204
+ return {
205
+ displayName,
206
+ email: chat.email ?? directory?.email ?? null,
207
+ profileUnavailableReason: displayName === null ? directory?.profileUnavailableReason : undefined,
208
+ };
209
+ }
210
+ /**
211
+ * Name `users/{id}` members through the Workspace directory, batching up to the
212
+ * People API limit per call. A member whose profile cannot be read keeps a null
213
+ * name and a reason; a failure of a whole call gives each member in it that reason.
214
+ */
215
+ async function lookupProfiles(users, context) {
216
+ const batches = Array.from({ length: Math.ceil(users.length / maxProfilesPerLookup) }, (_, index) => users.slice(index * maxProfilesPerLookup, (index + 1) * maxProfilesPerLookup));
217
+ const results = await Promise.all(batches.map((batch) => lookupProfileBatch(batch, context)));
218
+ return new Map(results.flatMap((result) => [...result]));
219
+ }
220
+ async function lookupProfileBatch(users, context) {
221
+ let payload;
222
+ try {
223
+ payload = recordOrEmpty(await googleChatJsonRequest(`${peopleApiBaseUrl}/people:batchGet`, {
224
+ context,
225
+ query: {
226
+ resourceNames: users.map((user) => `people/${stripPrefix(user, "users/")}`),
227
+ personFields: "names,emailAddresses",
228
+ sources: "READ_SOURCE_TYPE_PROFILE",
229
+ },
230
+ }));
231
+ }
232
+ catch (error) {
233
+ // Naming is best effort, so only a cancelled request stops the action. Any
234
+ // other failure, including a network error or a 200 whose body is not JSON,
235
+ // leaves the users unnamed with a reason.
236
+ if (context.signal?.aborted) {
237
+ throw error;
238
+ }
239
+ const reason = error instanceof ProviderRequestError ? httpFailureReason(error.status) : "people_request_failed";
240
+ return new Map(users.map((user) => [user, unavailableProfile(reason)]));
241
+ }
242
+ const responses = new Map(looseArray(payload.responses).map((entry) => {
243
+ const response = recordOrEmpty(entry);
244
+ return [optionalString(response.requestedResourceName), response];
245
+ }));
246
+ return new Map(users.map((user) => [user, toMemberProfile(responses.get(`people/${stripPrefix(user, "users/")}`))]));
247
+ }
248
+ function toMemberProfile(response) {
249
+ // A missing entry says nothing about the person: a genuine miss comes back as its
250
+ // own NOT_FOUND status, so an absent or unmatched entry is a malformed response.
251
+ if (!response) {
252
+ return unavailableProfile("people_request_failed");
253
+ }
254
+ const rpcCode = optionalRecord(response.status)?.code;
255
+ if (typeof rpcCode === "number" && rpcCode !== 0) {
256
+ return unavailableProfile(rpcFailureReason(rpcCode));
257
+ }
258
+ const person = recordOrEmpty(response.person);
259
+ const displayName = primaryProfileValue(person.names, "displayName");
260
+ return {
261
+ displayName: displayName ?? null,
262
+ email: primaryProfileValue(person.emailAddresses, "value") ?? null,
263
+ // A 200 without a name is what a Workspace with profile sharing turned off returns.
264
+ profileUnavailableReason: displayName ? undefined : "profile_name_missing",
265
+ };
266
+ }
267
+ function unavailableProfile(reason) {
268
+ return { displayName: null, email: null, profileUnavailableReason: reason };
269
+ }
270
+ /** Only 403 and 404 say something about the profile; anything else is a failed request, not a hidden one. */
271
+ function httpFailureReason(status) {
272
+ if (status === 403) {
273
+ return "people_forbidden";
274
+ }
275
+ if (status === 404) {
276
+ return "people_not_found";
277
+ }
278
+ return "people_request_failed";
279
+ }
280
+ function rpcFailureReason(code) {
281
+ if (code === rpcPermissionDenied) {
282
+ return "people_forbidden";
283
+ }
284
+ if (code === rpcNotFound) {
285
+ return "people_not_found";
286
+ }
287
+ return "people_request_failed";
288
+ }
289
+ function primaryProfileValue(value, field) {
290
+ const entries = looseArray(value).map((entry) => recordOrEmpty(entry));
291
+ const primary = entries.find((entry) => optionalRecord(entry.metadata)?.primary === true) ?? entries[0];
292
+ return optionalString(primary?.[field]);
293
+ }