@jskit-ai/connectors-catalog 0.1.1

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 (399) hide show
  1. package/README.md +192 -0
  2. package/docs/ai-upstream-notices.md +51 -0
  3. package/docs/ai.md +191 -0
  4. package/docs/airtable.md +130 -0
  5. package/docs/algolia.md +190 -0
  6. package/docs/amazon-redshift.md +229 -0
  7. package/docs/amplitude.md +210 -0
  8. package/docs/apify.md +162 -0
  9. package/docs/apollo-io.md +200 -0
  10. package/docs/asana.md +126 -0
  11. package/docs/ashby.md +188 -0
  12. package/docs/atlassian.md +224 -0
  13. package/docs/attention.md +162 -0
  14. package/docs/aws-athena.md +150 -0
  15. package/docs/aws-credentials.md +75 -0
  16. package/docs/aws-s3.md +193 -0
  17. package/docs/bigquery.md +146 -0
  18. package/docs/brevo.md +122 -0
  19. package/docs/calendly.md +158 -0
  20. package/docs/canva.md +191 -0
  21. package/docs/chargebee.md +192 -0
  22. package/docs/clay.md +194 -0
  23. package/docs/clickhouse.md +242 -0
  24. package/docs/confidence-exp.md +74 -0
  25. package/docs/confidence-flags.md +75 -0
  26. package/docs/confidence-oauth.md +137 -0
  27. package/docs/contentful.md +123 -0
  28. package/docs/databricks.md +180 -0
  29. package/docs/dbt-semantic-layer.md +175 -0
  30. package/docs/elevenlabs.md +102 -0
  31. package/docs/figma.md +132 -0
  32. package/docs/firebase-cloud-messaging.md +262 -0
  33. package/docs/firecrawl.md +137 -0
  34. package/docs/fireflies.md +95 -0
  35. package/docs/fireworks-ai.md +53 -0
  36. package/docs/gatewayapi.md +113 -0
  37. package/docs/gemini-enterprise.md +224 -0
  38. package/docs/github-api.md +133 -0
  39. package/docs/gitlab-api.md +138 -0
  40. package/docs/gmail.md +133 -0
  41. package/docs/gong.md +205 -0
  42. package/docs/google-ads.md +156 -0
  43. package/docs/google-analytics.md +202 -0
  44. package/docs/google-docs.md +105 -0
  45. package/docs/google-drive.md +108 -0
  46. package/docs/google-maps-platform.md +208 -0
  47. package/docs/google-oauth.md +123 -0
  48. package/docs/google-search-console.md +124 -0
  49. package/docs/google-sheets.md +94 -0
  50. package/docs/google-slides.md +95 -0
  51. package/docs/granola.md +178 -0
  52. package/docs/hex.md +161 -0
  53. package/docs/heygen.md +124 -0
  54. package/docs/hubspot.md +142 -0
  55. package/docs/incident-io.md +118 -0
  56. package/docs/inngest.md +178 -0
  57. package/docs/klipy.md +149 -0
  58. package/docs/lexware.md +144 -0
  59. package/docs/lightspeed.md +193 -0
  60. package/docs/linear.md +137 -0
  61. package/docs/linkedin.md +196 -0
  62. package/docs/logo-dev.md +152 -0
  63. package/docs/mailgun.md +153 -0
  64. package/docs/mapbox.md +174 -0
  65. package/docs/microsoft-excel.md +97 -0
  66. package/docs/microsoft-fabric.md +249 -0
  67. package/docs/microsoft-oauth.md +143 -0
  68. package/docs/microsoft-onedrive.md +76 -0
  69. package/docs/microsoft-onenote.md +75 -0
  70. package/docs/microsoft-outlook.md +90 -0
  71. package/docs/microsoft-powerpoint.md +119 -0
  72. package/docs/microsoft-sharepoint.md +75 -0
  73. package/docs/microsoft-teams.md +72 -0
  74. package/docs/microsoft-word.md +119 -0
  75. package/docs/miro.md +117 -0
  76. package/docs/n8n.md +169 -0
  77. package/docs/notion.md +137 -0
  78. package/docs/oura.md +161 -0
  79. package/docs/paddle.md +168 -0
  80. package/docs/perplexity.md +85 -0
  81. package/docs/pipedrive.md +120 -0
  82. package/docs/polar.md +63 -0
  83. package/docs/posthog.md +227 -0
  84. package/docs/prestashop.md +151 -0
  85. package/docs/replicate.md +60 -0
  86. package/docs/resend.md +153 -0
  87. package/docs/salesforce.md +234 -0
  88. package/docs/sanity.md +122 -0
  89. package/docs/semrush.md +276 -0
  90. package/docs/sentry.md +112 -0
  91. package/docs/sevdesk.md +107 -0
  92. package/docs/shopify.md +206 -0
  93. package/docs/slack.md +257 -0
  94. package/docs/snowflake.md +271 -0
  95. package/docs/storyblok.md +130 -0
  96. package/docs/stripe.md +77 -0
  97. package/docs/supabase.md +51 -0
  98. package/docs/tally.md +105 -0
  99. package/docs/telegram.md +165 -0
  100. package/docs/tiktok.md +201 -0
  101. package/docs/twilio.md +284 -0
  102. package/docs/twitch.md +428 -0
  103. package/docs/wave.md +440 -0
  104. package/docs/wix.md +1249 -0
  105. package/docs/wiz.md +177 -0
  106. package/docs/woocommerce.md +400 -0
  107. package/docs/wordpress-com.md +358 -0
  108. package/docs/wordpress-self-hosted.md +252 -0
  109. package/docs/workday.md +292 -0
  110. package/docs/x-twitter.md +188 -0
  111. package/docs/xero.md +429 -0
  112. package/docs/zoho-books.md +214 -0
  113. package/docs/zoho-crm.md +192 -0
  114. package/package.json +153 -0
  115. package/patterns/ai-connections/PATTERN.md +84 -0
  116. package/patterns/ai-connections/example/ai-model.js +11 -0
  117. package/patterns/ai-connections/example/perplexity-answer.js +29 -0
  118. package/patterns/api-key-connection/PATTERN.md +368 -0
  119. package/patterns/api-key-connection/example/connections.js +18 -0
  120. package/patterns/api-key-connection/example/integrations.json +19 -0
  121. package/patterns/assistant-mcp/PATTERN.md +110 -0
  122. package/patterns/assistant-mcp/example/integrations.json +13 -0
  123. package/patterns/assistant-mcp-oauth/PATTERN.md +235 -0
  124. package/patterns/assistant-mcp-oauth/example/integrations.json +102 -0
  125. package/patterns/aws-storage-queries/PATTERN.md +169 -0
  126. package/patterns/aws-storage-queries/example/formats/data-formats.js +35 -0
  127. package/patterns/aws-storage-queries/example/formats/package-lock.json +49 -0
  128. package/patterns/aws-storage-queries/example/formats/package.json +12 -0
  129. package/patterns/aws-storage-queries/example/formats/verify-formats.mjs +37 -0
  130. package/patterns/aws-storage-queries/example/integrations.json +38 -0
  131. package/patterns/aws-storage-queries/example/s3-transfer.js +34 -0
  132. package/patterns/event-delivery/PATTERN.md +151 -0
  133. package/patterns/event-delivery/example/integrations.json +17 -0
  134. package/patterns/firebase-messaging/PATTERN.md +156 -0
  135. package/patterns/firebase-messaging/example/server/notifications.js +23 -0
  136. package/patterns/google-ads-search/PATTERN.md +92 -0
  137. package/patterns/google-ads-search/example/ads-setup.js +25 -0
  138. package/patterns/oauth-connection/PATTERN.md +724 -0
  139. package/patterns/oauth-connection/example/integrations.json +20 -0
  140. package/patterns/paddle-catalogue/PATTERN.md +78 -0
  141. package/patterns/paddle-catalogue/example/create-products.js +38 -0
  142. package/patterns/public-image/PATTERN.md +84 -0
  143. package/patterns/public-image/example/integrations.json +13 -0
  144. package/patterns/public-image/example/logo-url.js +16 -0
  145. package/patterns/redshift-queries/PATTERN.md +150 -0
  146. package/patterns/redshift-queries/example/integrations.json +33 -0
  147. package/patterns/source-scanning/PATTERN.md +83 -0
  148. package/patterns/source-scanning/example/source-scanner.js +13 -0
  149. package/src/client/firebase-cloud-messaging.js +10 -0
  150. package/src/client/google-analytics.js +72 -0
  151. package/src/client/google-maps-platform.js +39 -0
  152. package/src/client/logo-dev.js +36 -0
  153. package/src/server/ai.js +46 -0
  154. package/src/server/airtable.js +67 -0
  155. package/src/server/algolia.js +63 -0
  156. package/src/server/amazon-redshift.js +96 -0
  157. package/src/server/amplitude.js +31 -0
  158. package/src/server/apify.js +68 -0
  159. package/src/server/apollo-io.js +85 -0
  160. package/src/server/asana.js +64 -0
  161. package/src/server/ashby.js +61 -0
  162. package/src/server/atlassian.js +32 -0
  163. package/src/server/attention.js +93 -0
  164. package/src/server/aws-athena.js +82 -0
  165. package/src/server/aws-s3.js +49 -0
  166. package/src/server/awsTransport.js +75 -0
  167. package/src/server/bigquery.js +97 -0
  168. package/src/server/brevo.js +97 -0
  169. package/src/server/calendly.js +77 -0
  170. package/src/server/canva.js +13 -0
  171. package/src/server/chargebee.js +88 -0
  172. package/src/server/clay.js +78 -0
  173. package/src/server/clickhouse.js +66 -0
  174. package/src/server/confidence.js +29 -0
  175. package/src/server/contentful.js +40 -0
  176. package/src/server/databricks.js +55 -0
  177. package/src/server/dbt-semantic-layer.js +95 -0
  178. package/src/server/elevenlabs.js +81 -0
  179. package/src/server/figma.js +24 -0
  180. package/src/server/firebase-cloud-messaging.js +111 -0
  181. package/src/server/firecrawl.js +70 -0
  182. package/src/server/fireflies.js +31 -0
  183. package/src/server/fireworks-ai.js +16 -0
  184. package/src/server/gatewayapi.js +48 -0
  185. package/src/server/gemini-enterprise.js +47 -0
  186. package/src/server/github-api.js +90 -0
  187. package/src/server/gitlab-api.js +67 -0
  188. package/src/server/gmail.js +60 -0
  189. package/src/server/gong.js +52 -0
  190. package/src/server/google-ads-search.js +134 -0
  191. package/src/server/google-ads.js +80 -0
  192. package/src/server/google-docs.js +36 -0
  193. package/src/server/google-drive.js +71 -0
  194. package/src/server/google-maps-platform.js +90 -0
  195. package/src/server/google-search-console.js +31 -0
  196. package/src/server/google-sheets.js +60 -0
  197. package/src/server/google-slides.js +36 -0
  198. package/src/server/google.js +61 -0
  199. package/src/server/granola.js +115 -0
  200. package/src/server/graphqlOperation.js +15 -0
  201. package/src/server/hex.js +29 -0
  202. package/src/server/heygen.js +54 -0
  203. package/src/server/hubspot.js +85 -0
  204. package/src/server/incident-io.js +68 -0
  205. package/src/server/inngest.js +86 -0
  206. package/src/server/jsonOperation.js +34 -0
  207. package/src/server/klipy.js +34 -0
  208. package/src/server/lexware.js +59 -0
  209. package/src/server/lightspeed.js +82 -0
  210. package/src/server/linear.js +52 -0
  211. package/src/server/linkedin.js +74 -0
  212. package/src/server/mailgun.js +76 -0
  213. package/src/server/mapbox.js +46 -0
  214. package/src/server/mcpRegistration.js +41 -0
  215. package/src/server/mcpTransport.js +60 -0
  216. package/src/server/microsoft-excel.js +67 -0
  217. package/src/server/microsoft-fabric.js +81 -0
  218. package/src/server/microsoft-onedrive.js +58 -0
  219. package/src/server/microsoft-onenote.js +52 -0
  220. package/src/server/microsoft-outlook.js +58 -0
  221. package/src/server/microsoft-powerpoint.js +5 -0
  222. package/src/server/microsoft-sharepoint.js +46 -0
  223. package/src/server/microsoft-teams.js +29 -0
  224. package/src/server/microsoft-word.js +5 -0
  225. package/src/server/microsoft.js +88 -0
  226. package/src/server/miro.js +29 -0
  227. package/src/server/n8n.js +78 -0
  228. package/src/server/notion.js +76 -0
  229. package/src/server/oura.js +48 -0
  230. package/src/server/paddle.js +83 -0
  231. package/src/server/perplexity.js +15 -0
  232. package/src/server/pipedrive.js +95 -0
  233. package/src/server/polar.js +20 -0
  234. package/src/server/posthog.js +44 -0
  235. package/src/server/prestashop.js +42 -0
  236. package/src/server/replicate.js +42 -0
  237. package/src/server/resend.js +93 -0
  238. package/src/server/salesforce.js +84 -0
  239. package/src/server/sanity.js +31 -0
  240. package/src/server/semrush.js +359 -0
  241. package/src/server/sentry.js +30 -0
  242. package/src/server/sevdesk.js +20 -0
  243. package/src/server/shopify.js +99 -0
  244. package/src/server/slack.js +143 -0
  245. package/src/server/snowflake.js +139 -0
  246. package/src/server/storyblok.js +49 -0
  247. package/src/server/stripe.js +13 -0
  248. package/src/server/supabase.js +13 -0
  249. package/src/server/tally.js +78 -0
  250. package/src/server/telegram.js +55 -0
  251. package/src/server/tiktok.js +86 -0
  252. package/src/server/twilio.js +90 -0
  253. package/src/server/twitch.js +409 -0
  254. package/src/server/wave.js +520 -0
  255. package/src/server/wix.js +1048 -0
  256. package/src/server/wiz.js +117 -0
  257. package/src/server/woocommerce.js +412 -0
  258. package/src/server/wordpress-com.js +236 -0
  259. package/src/server/wordpress-self-hosted.js +134 -0
  260. package/src/server/workday.js +136 -0
  261. package/src/server/x-twitter.js +110 -0
  262. package/src/server/xero.js +397 -0
  263. package/src/server/zoho-books.js +90 -0
  264. package/src/server/zoho-crm.js +89 -0
  265. package/src/server/zohoOAuth.js +21 -0
  266. package/src/shared/ai-models.json +8049 -0
  267. package/src/shared/ai.js +63 -0
  268. package/src/shared/algolia.js +29 -0
  269. package/src/shared/amazon-redshift.js +51 -0
  270. package/src/shared/amplitude.js +28 -0
  271. package/src/shared/atlassian.js +43 -0
  272. package/src/shared/aws.js +72 -0
  273. package/src/shared/canva.js +66 -0
  274. package/src/shared/chargebee.js +26 -0
  275. package/src/shared/clickhouse.js +38 -0
  276. package/src/shared/confidence.js +28 -0
  277. package/src/shared/databricks.js +33 -0
  278. package/src/shared/dbt-semantic-layer.js +32 -0
  279. package/src/shared/definitions.js +140 -0
  280. package/src/shared/figma.js +21 -0
  281. package/src/shared/firebase-cloud-messaging.js +45 -0
  282. package/src/shared/gemini-enterprise.js +33 -0
  283. package/src/shared/gong.js +33 -0
  284. package/src/shared/google-ads-search.js +31 -0
  285. package/src/shared/google-ads.js +28 -0
  286. package/src/shared/google-maps-platform.js +28 -0
  287. package/src/shared/google.js +133 -0
  288. package/src/shared/granola.js +25 -0
  289. package/src/shared/hex.js +28 -0
  290. package/src/shared/inngest.js +35 -0
  291. package/src/shared/lightspeed.js +75 -0
  292. package/src/shared/linkedin.js +27 -0
  293. package/src/shared/logo-dev.js +21 -0
  294. package/src/shared/mapbox.js +38 -0
  295. package/src/shared/mcp.js +92 -0
  296. package/src/shared/microsoft-fabric.js +47 -0
  297. package/src/shared/microsoft.js +231 -0
  298. package/src/shared/miro.js +27 -0
  299. package/src/shared/oura.js +23 -0
  300. package/src/shared/posthog.js +33 -0
  301. package/src/shared/prestashop.js +23 -0
  302. package/src/shared/salesforce.js +36 -0
  303. package/src/shared/semrush.js +24 -0
  304. package/src/shared/shopify.js +51 -0
  305. package/src/shared/siteUrl.js +12 -0
  306. package/src/shared/slack.js +88 -0
  307. package/src/shared/snowflake.js +55 -0
  308. package/src/shared/tiktok.js +22 -0
  309. package/src/shared/tokens.js +747 -0
  310. package/src/shared/twilio.js +41 -0
  311. package/src/shared/twitch.js +49 -0
  312. package/src/shared/wave.js +52 -0
  313. package/src/shared/wix.js +40 -0
  314. package/src/shared/wiz.js +41 -0
  315. package/src/shared/wordpress-com.js +30 -0
  316. package/src/shared/wordpress.js +69 -0
  317. package/src/shared/workday.js +50 -0
  318. package/src/shared/x-twitter.js +19 -0
  319. package/src/shared/xero.js +46 -0
  320. package/src/shared/zoho-books.js +39 -0
  321. package/src/shared/zoho-crm.js +43 -0
  322. package/src/shared/zoho-regions.js +12 -0
  323. package/test/ai.test.js +136 -0
  324. package/test/airtable.test.js +125 -0
  325. package/test/algolia.test.js +207 -0
  326. package/test/amazon-redshift.test.js +272 -0
  327. package/test/amplitude.test.js +239 -0
  328. package/test/apify.test.js +93 -0
  329. package/test/apollo-io.test.js +125 -0
  330. package/test/asana.test.js +99 -0
  331. package/test/ashby.test.js +97 -0
  332. package/test/atlassian.test.js +249 -0
  333. package/test/attention.test.js +79 -0
  334. package/test/aws.test.js +405 -0
  335. package/test/brevo.test.js +71 -0
  336. package/test/calendly.test.js +136 -0
  337. package/test/canva.test.js +258 -0
  338. package/test/chargebee.test.js +195 -0
  339. package/test/clickhouse.test.js +278 -0
  340. package/test/confidence.test.js +205 -0
  341. package/test/databricks.test.js +253 -0
  342. package/test/dbt-semantic-layer.test.js +221 -0
  343. package/test/designMcp.test.js +336 -0
  344. package/test/elevenlabs.test.js +52 -0
  345. package/test/firebase-cloud-messaging.test.js +242 -0
  346. package/test/firecrawl.test.js +66 -0
  347. package/test/gemini-enterprise.test.js +218 -0
  348. package/test/github-api.test.js +167 -0
  349. package/test/gitlab-api.test.js +160 -0
  350. package/test/gong.test.js +192 -0
  351. package/test/google-ads.test.js +350 -0
  352. package/test/google-maps-platform.test.js +191 -0
  353. package/test/google.test.js +710 -0
  354. package/test/granola.test.js +240 -0
  355. package/test/hex.test.js +253 -0
  356. package/test/heygen.test.js +121 -0
  357. package/test/hubspot.test.js +103 -0
  358. package/test/inngest.test.js +219 -0
  359. package/test/lightspeed.test.js +250 -0
  360. package/test/linear.test.js +146 -0
  361. package/test/linkedin.test.js +243 -0
  362. package/test/logo-dev.test.js +79 -0
  363. package/test/mailgun.test.js +107 -0
  364. package/test/mapbox.test.js +181 -0
  365. package/test/mcp.test.js +227 -0
  366. package/test/microsoft-fabric.test.js +258 -0
  367. package/test/microsoft.test.js +554 -0
  368. package/test/n8nOAuth.test.js +188 -0
  369. package/test/notionOAuth.test.js +183 -0
  370. package/test/oura.test.js +211 -0
  371. package/test/paddleCatalogue.test.js +51 -0
  372. package/test/paddlePermissions.test.js +101 -0
  373. package/test/perplexityComposition.test.js +39 -0
  374. package/test/pipedriveOAuth.test.js +140 -0
  375. package/test/posthog.test.js +200 -0
  376. package/test/prestashop.test.js +207 -0
  377. package/test/resend.test.js +163 -0
  378. package/test/salesforce.test.js +276 -0
  379. package/test/semrush.test.js +479 -0
  380. package/test/sentry.test.js +35 -0
  381. package/test/shopify.test.js +247 -0
  382. package/test/slack.test.js +320 -0
  383. package/test/snowflake.test.js +391 -0
  384. package/test/tally.test.js +75 -0
  385. package/test/telegram.test.js +58 -0
  386. package/test/tiktok.test.js +243 -0
  387. package/test/tokens.test.js +1079 -0
  388. package/test/twilio.test.js +221 -0
  389. package/test/twitch.test.js +573 -0
  390. package/test/wave.test.js +678 -0
  391. package/test/wix.test.js +1099 -0
  392. package/test/wiz.test.js +185 -0
  393. package/test/wordpress-com.test.js +469 -0
  394. package/test/wordpress.test.js +660 -0
  395. package/test/workday.test.js +314 -0
  396. package/test/x-twitter.test.js +256 -0
  397. package/test/xero.test.js +700 -0
  398. package/test/zoho-books.test.js +253 -0
  399. package/test/zoho-crm.test.js +223 -0
@@ -0,0 +1,60 @@
1
+ import { createSchema } from "json-rest-schema";
2
+ import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
3
+ import { ConnectorError } from "@jskit-ai/connectors-core/server";
4
+ import { gmailDefinition } from "../shared/google.js";
5
+ import { googleRead, googleProvider, documentId, pageToken } from "./google.js";
6
+ const origin = "https://gmail.googleapis.com";
7
+ const read = ["gmail.readonly", "gmail.modify", "gmail.metadata", "https://mail.google.com/"];
8
+ const contentScopes = ["gmail.readonly", "gmail.modify", "https://mail.google.com/"];
9
+ const composeScopes = ["gmail.compose", "gmail.modify", "https://mail.google.com/"];
10
+ const modifyScopes = ["gmail.modify", "https://mail.google.com/"];
11
+ const labelScopes = ["gmail.labels", ...modifyScopes];
12
+ const messageResult = value => typeof value?.id === "string" && typeof value.threadId === "string";
13
+ const draftResult = value => typeof value?.id === "string" && messageResult(value.message);
14
+ const rawField = { type: "string", required: true, minLength: 4, maxLength: 14000000, noTrim: true,
15
+ validator: value => /^[A-Za-z0-9_-]+={0,2}$/.test(value) && value.replace(/=+$/, "").length % 4 !== 1 || "Supply base64url-encoded MIME content." };
16
+ const labelIds = { type: "array", items: { type: "string", minLength: 1, maxLength: 200 }, validator: value => value.length <= 100 || "Supply no more than 100 labels." };
17
+ const listFields = { pageToken, maxResults: { type: "integer", min: 1, max: 500, defaultTo: 100 }, labelIds, includeSpamTrash: { type: "boolean", defaultTo: false } };
18
+ function gmailWrite(scopes, fields, destination, validateResult, method = "POST") {
19
+ const schema = createSchema(fields);
20
+ return { scopes: scopes.map(value => value.startsWith("https:") ? value : `https://www.googleapis.com/auth/${value}`), validateResult,
21
+ request(input) {
22
+ const values = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
23
+ const { path, body } = destination(values);
24
+ return { method, url: `${origin}/gmail/v1/users/me${path}`, ...(body ? { body } : {}) };
25
+ } };
26
+ }
27
+ const gmailProvider = Object.freeze({ ...googleProvider(gmailDefinition, origin, "profile.read", {
28
+ "messages.read": googleRead(contentScopes, { id: documentId, format: { type: "string", enum: ["full", "raw"], defaultTo: "full" } }, ({ id, format }) => ({ url: `${origin}/gmail/v1/users/me/messages/${encodeURIComponent(id)}`, query: { format } }), messageResult),
29
+ "messages.search": googleRead(contentScopes, { ...listFields, q: { type: "string", required: true, minLength: 1, maxLength: 4096 } }, query => ({ url: `${origin}/gmail/v1/users/me/messages`, query }), value => Number.isInteger(value?.resultSizeEstimate) && (value.messages === undefined || Array.isArray(value.messages))),
30
+ "attachments.get": googleRead(contentScopes, { messageId: documentId, id: documentId }, ({ messageId, id }) => ({ url: `${origin}/gmail/v1/users/me/messages/${encodeURIComponent(messageId)}/attachments/${encodeURIComponent(id)}` }), value => Number.isInteger(value?.size) && typeof value.data === "string"),
31
+ "messages.send": gmailWrite(["gmail.send", ...composeScopes], { raw: rawField, threadId: { ...documentId, required: false } }, body => ({ path: "/messages/send", body }), messageResult),
32
+ "messages.modify": gmailWrite(modifyScopes, { id: documentId, addLabelIds: labelIds, removeLabelIds: labelIds }, ({ id, ...body }) => {
33
+ if (!body.addLabelIds?.length && !body.removeLabelIds?.length) throw new ConnectorError("connector_input_invalid", "Supply labels to add or remove.");
34
+ return { path: `/messages/${encodeURIComponent(id)}/modify`, body };
35
+ }, messageResult),
36
+ "messages.trash": gmailWrite(modifyScopes, { id: documentId }, ({ id }) => ({ path: `/messages/${encodeURIComponent(id)}/trash` }), messageResult),
37
+ "messages.untrash": gmailWrite(modifyScopes, { id: documentId }, ({ id }) => ({ path: `/messages/${encodeURIComponent(id)}/untrash` }), messageResult),
38
+ "drafts.list": googleRead([...contentScopes, "gmail.compose"], { pageToken, maxResults: listFields.maxResults }, query => ({ url: `${origin}/gmail/v1/users/me/drafts`, query }), value => Number.isInteger(value?.resultSizeEstimate) && (value.drafts === undefined || Array.isArray(value.drafts))),
39
+ "drafts.get": googleRead([...contentScopes, "gmail.compose"], { id: documentId }, ({ id }) => ({ url: `${origin}/gmail/v1/users/me/drafts/${encodeURIComponent(id)}`, query: { format: "full" } }), draftResult),
40
+ "drafts.create": gmailWrite(composeScopes, { raw: rawField, threadId: { ...documentId, required: false } }, message => ({ path: "/drafts", body: { message } }), draftResult),
41
+ "drafts.update": gmailWrite(composeScopes, { id: documentId, raw: rawField, threadId: { ...documentId, required: false } }, ({ id, ...message }) => ({ path: `/drafts/${encodeURIComponent(id)}`, body: { message } }), draftResult, "PUT"),
42
+ "drafts.delete": gmailWrite(composeScopes, { id: documentId }, ({ id }) => ({ path: `/drafts/${encodeURIComponent(id)}` }), value => value === null, "DELETE"),
43
+ "drafts.send": gmailWrite(composeScopes, { id: documentId }, body => ({ path: "/drafts/send", body }), messageResult),
44
+ "labels.list": googleRead([...read, "gmail.labels"], {}, () => ({ url: `${origin}/gmail/v1/users/me/labels` }), value => Array.isArray(value?.labels)),
45
+ "labels.delete": gmailWrite(labelScopes, { id: documentId }, ({ id }) => ({ path: `/labels/${encodeURIComponent(id)}` }), value => value === null, "DELETE"),
46
+ "labels.create": gmailWrite(labelScopes, { name: { type: "string", required: true, minLength: 1, maxLength: 225 } }, body => ({ path: "/labels", body }), value => typeof value?.id === "string" && typeof value.name === "string"),
47
+ "labels.update": gmailWrite(labelScopes, { id: documentId, name: { type: "string", required: true, minLength: 1, maxLength: 225 } }, ({ id, ...body }) => ({ path: `/labels/${encodeURIComponent(id)}`, body }), value => typeof value?.id === "string" && typeof value.name === "string", "PUT"),
48
+ "profile.read": googleRead([...read, "gmail.compose"], {}, () => ({ url: `${origin}/gmail/v1/users/me/profile` }),
49
+ (value) => typeof value?.emailAddress === "string" && Number.isInteger(value.messagesTotal)),
50
+ "messages.list": googleRead(read, {
51
+ pageToken, maxResults: { type: "integer", min: 1, max: 500, defaultTo: 100 },
52
+ labelIds: { type: "array", items: { type: "string", minLength: 1, maxLength: 200 } },
53
+ includeSpamTrash: { type: "boolean", defaultTo: false }
54
+ }, (query) => ({ url: `${origin}/gmail/v1/users/me/messages`, query }),
55
+ (value) => Number.isInteger(value?.resultSizeEstimate) && (value.messages === undefined || Array.isArray(value.messages))),
56
+ "messages.get": googleRead(read, { id: documentId }, ({ id }) => ({
57
+ url: `${origin}/gmail/v1/users/me/messages/${encodeURIComponent(id)}`, query: { format: "metadata" }
58
+ }), (value) => typeof value?.id === "string" && typeof value.threadId === "string")
59
+ }), accountLabelFromVerification: (profile) => profile.emailAddress });
60
+ export { gmailProvider };
@@ -0,0 +1,52 @@
1
+ import { createSchema } from "json-rest-schema";
2
+ import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
3
+ import { ConnectorError } from "@jskit-ai/connectors-core/server";
4
+ import { gongDefinition } from "../shared/gong.js";
5
+ import { jsonOperation } from "./jsonOperation.js";
6
+
7
+ const origin = (settings) => new URL(settings.apiBaseUrl).origin;
8
+ const cursor = { type: "string", minLength: 1, maxLength: 8192, noTrim: true };
9
+ const ids = { type: "array", items: { type: "string", pattern: "^[0-9]{1,20}$" }, validator: value => value.length >= 1 && value.length <= 100 || "Supply between 1 and 100 string IDs." };
10
+ const timestamp = { type: "string", validator: value => /^\d{4}-\d{2}-\d{2}T.*(?:Z|[+-]\d{2}:\d{2})$/.test(value) && Number.isFinite(Date.parse(value)) || "Enter an ISO timestamp including its time zone." };
11
+ const callFields = { cursor, fromDateTime: timestamp, toDateTime: timestamp, callIds: ids,
12
+ workspaceId: { type: "string", pattern: "^[0-9]{1,20}$" } };
13
+ const recordsValid = value => ["totalRecords", "currentPageSize", "currentPageNumber"].every(key => Number.isInteger(value?.records?.[key]) && value.records[key] >= 0) && (value.records.cursor === undefined || typeof value.records.cursor === "string");
14
+ function callOperation(path, field, extensive = false) {
15
+ const schema = createSchema({ ...callFields, ...(extensive ? { includeMedia: { type: "boolean", defaultTo: false } } : {}) });
16
+ return { scopes: [], request(input, settings) {
17
+ if (Array.isArray(input.callIds) && input.callIds.some(id => typeof id !== "string") || input.workspaceId !== undefined && typeof input.workspaceId !== "string") throw new ConnectorError("connector_input_invalid", "Supply Gong IDs as strings to preserve all digits.");
18
+ const { cursor, includeMedia, ...filter } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
19
+ if ((!filter.fromDateTime !== !filter.toDateTime) || (filter.fromDateTime && Date.parse(filter.fromDateTime) >= Date.parse(filter.toDateTime)) || (!filter.callIds && !filter.fromDateTime)) throw new ConnectorError("connector_input_invalid", "Supply call IDs or an ordered timestamp range.");
20
+ const body = { filter, ...(cursor ? { cursor } : {}) };
21
+ if (extensive) body.contentSelector = { context: "Extended", contextTiming: ["Now", "TimeOfCall"], exposedFields: {
22
+ parties: true, content: { brief: true, outline: true, topics: true, keyPoints: true, callOutcome: true },
23
+ interaction: { speakers: true, personInteractionStats: true, questions: true }, media: includeMedia
24
+ } };
25
+ return { method: "POST", url: `${origin(settings)}${path}`, body };
26
+ }, validateResult: value => Array.isArray(value?.[field]) && recordsValid(value) };
27
+ }
28
+ const date = { type: "string", required: true, validator: value => /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value || "Enter a valid YYYY-MM-DD date." };
29
+ const statsSchema = createSchema({ cursor, fromDate: date, toDate: date, userIds: ids });
30
+ const gongProvider = Object.freeze({
31
+ ...gongDefinition,
32
+ apiOrigins: (settings) => [origin(settings)],
33
+ apiKey: { headers: (secret, settings) => ({ Authorization: `Basic ${Buffer.from(`${settings.accessKey}:${secret}`).toString("base64")}` }) },
34
+ checkOperation: "users.list",
35
+ operations: {
36
+ "calls.extensive": callOperation("/v2/calls/extensive", "calls", true),
37
+ "calls.transcripts": callOperation("/v2/calls/transcript", "callTranscripts"),
38
+ "stats.interaction": { scopes: [], request(input, settings) {
39
+ if (Array.isArray(input.userIds) && input.userIds.some(id => typeof id !== "string")) throw new ConnectorError("connector_input_invalid", "Supply Gong IDs as strings to preserve all digits.");
40
+ const { cursor, ...filter } = validateSchemaPayload({ schema: statsSchema, mode: "replace" }, input, { statusCode: 422 });
41
+ if (filter.fromDate >= filter.toDate) throw new ConnectorError("connector_input_invalid", "The end date must follow the start date.");
42
+ return { method: "POST", url: `${origin(settings)}/v2/stats/interaction`, body: { filter, ...(cursor ? { cursor } : {}) } };
43
+ }, validateResult: value => Array.isArray(value?.peopleInteractionStats) && recordsValid(value) },
44
+ "users.list": jsonOperation((settings) => `${origin(settings)}/v2/users`, {
45
+ cursor: { type: "string", minLength: 1, maxLength: 8192 },
46
+ includeAvatars: { type: "boolean", defaultTo: false }
47
+ }, (result) => Array.isArray(result?.users) &&
48
+ ["totalRecords", "currentPageSize", "currentPageNumber"].every((key) => Number.isInteger(result.records?.[key]) && result.records[key] >= 0) &&
49
+ (result.records.cursor === undefined || typeof result.records.cursor === "string"))
50
+ }
51
+ });
52
+ export { gongProvider };
@@ -0,0 +1,134 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createSchema } from "json-rest-schema";
3
+ import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
4
+ import { ConnectorError } from "@jskit-ai/connectors-core/server";
5
+ import { validateGoogleAdsSearchPlan } from "../shared/google-ads-search.js";
6
+
7
+ const api = "https://googleads.googleapis.com/v25";
8
+ const scope = "https://www.googleapis.com/auth/adwords";
9
+ const customerId = { type: "string", required: true, validator: value => /^[0-9]{10}$/.test(value) || "Use a ten-digit customer ID." };
10
+ const id = { type: "string", required: true, validator: value => /^[0-9]{1,20}$/.test(value) || "Use a numeric campaign ID." };
11
+ const error = message => { throw new ConnectorError("connector_input_invalid", message, { statusCode: 422 }); };
12
+ const hash = value => createHash("sha256").update(JSON.stringify(value)).digest("hex");
13
+ function requestOperation(fields, build, validateResult) {
14
+ const schema = createSchema(fields);
15
+ return { scopes: [scope], request(input, settings) {
16
+ const values = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
17
+ return { method: "POST", ...build(values), headers: settings.loginCustomerId ? { "login-customer-id": settings.loginCustomerId } : {} };
18
+ }, validateResult };
19
+ }
20
+ const mutated = result => result && typeof result === "object" && !Array.isArray(result) && !result.partialFailureError &&
21
+ (result.results === undefined || Array.isArray(result.results)) && (result.mutateOperationResponses === undefined || Array.isArray(result.mutateOperationResponses));
22
+
23
+ function searchMutation(plan, validateOnly) {
24
+ const p = validateGoogleAdsSearchPlan(plan), root = `customers/${p.customerId}`;
25
+ const budget = `${root}/campaignBudgets/-1`, campaign = `${root}/campaigns/-2`, group = `${root}/adGroups/-3`, goal = `${root}/customConversionGoals/-4`;
26
+ return { url: `${api}/${root}/googleAds:mutate`, body: { validateOnly, partialFailure: false, mutateOperations: [
27
+ { campaignBudgetOperation: { create: { resourceName: budget, name: `${p.name} budget`, amountMicros: p.dailyBudgetMicros, deliveryMethod: "STANDARD", explicitlyShared: false } } },
28
+ { campaignOperation: { create: { resourceName: campaign, name: p.name, advertisingChannelType: "SEARCH", status: "PAUSED", campaignBudget: budget,
29
+ manualCpc: {}, containsEuPoliticalAdvertising: "DOES_NOT_CONTAIN_EU_POLITICAL_ADVERTISING",
30
+ networkSettings: { targetGoogleSearch: true, targetSearchNetwork: false, targetContentNetwork: false, targetPartnerSearchNetwork: false },
31
+ geoTargetTypeSetting: { positiveGeoTargetType: "PRESENCE" } } } },
32
+ { customConversionGoalOperation: { create: { resourceName: goal, name: `${p.name} goal`, status: "ENABLED", conversionActions: [`${root}/conversionActions/${p.conversionActionId}`] } } },
33
+ { conversionGoalCampaignConfigOperation: { update: { resourceName: `${root}/conversionGoalCampaignConfigs/-2`, goalConfigLevel: "CAMPAIGN", customConversionGoal: goal }, updateMask: "goal_config_level,custom_conversion_goal" } },
34
+ ...p.locationIds.map(location => ({ campaignCriterionOperation: { create: { campaign, location: { geoTargetConstant: `geoTargetConstants/${location}` } } } })),
35
+ { campaignCriterionOperation: { create: { campaign, language: { languageConstant: `languageConstants/${p.languageId}` } } } },
36
+ { adGroupOperation: { create: { resourceName: group, name: `${p.name} group`, campaign, status: "ENABLED", type: "SEARCH_STANDARD", cpcBidMicros: p.maxCpcMicros } } },
37
+ ...p.keywords.map(text => ({ adGroupCriterionOperation: { create: { adGroup: group, status: "ENABLED", keyword: { text, matchType: "PHRASE" } } } })),
38
+ { adGroupAdOperation: { create: { adGroup: group, status: "ENABLED", ad: { finalUrls: [p.finalUrl], responsiveSearchAd: {
39
+ headlines: p.headlines.map(text => ({ text })), descriptions: p.descriptions.map(text => ({ text })) } } } } }
40
+ ] } };
41
+ }
42
+ const googleAdsSearchOperations = {
43
+ "search.validate": { scopes: [scope], request: (input, settings) => ({ method: "POST", ...searchMutation(input, true), headers: settings.loginCustomerId ? { "login-customer-id": settings.loginCustomerId } : {} }), validateResult: mutated },
44
+ "search.createPaused": { scopes: [scope], request: (input, settings) => ({ method: "POST", ...searchMutation(input, false), headers: settings.loginCustomerId ? { "login-customer-id": settings.loginCustomerId } : {} }), validateResult: result => mutated(result) && Array.isArray(result.mutateOperationResponses) && result.mutateOperationResponses.some(item => /^customers\/[0-9]{10}\/campaigns\/[0-9]+$/.test(item.campaignResult?.resourceName || "")) },
45
+ "campaigns.setStatus": requestOperation({ customerId, campaignId: id, status: { type: "string", required: true, enum: ["PAUSED", "ENABLED"] } },
46
+ p => ({ url: `${api}/customers/${p.customerId}/campaigns:mutate`, body: { operations: [{ update: { resourceName: `customers/${p.customerId}/campaigns/${p.campaignId}`, status: p.status }, updateMask: "status" }] } }), result => mutated(result) && result.results?.length === 1),
47
+ "conversions.createWebsite": requestOperation({ customerId, name: { type: "string", required: true, minLength: 1, maxLength: 100 } },
48
+ p => ({ url: `${api}/customers/${p.customerId}/conversionActions:mutate`, body: { operations: [{ create: { name: p.name, type: "WEBPAGE", category: "SUBMIT_LEAD_FORM", status: "ENABLED", countingType: "ONE_PER_CLICK", primaryForGoal: true } }] } }), result => mutated(result) && result.results?.length === 1)
49
+ };
50
+
51
+ // Application composition: this service has no editor protocol, command runner or storage.
52
+ function createGoogleAdsSearchService({ connections, configuration, context, integrationId }) {
53
+ const slot = configuration.integrations?.[integrationId];
54
+ if (slot?.provider !== "google-ads") error("Choose a Google Ads integration.");
55
+ const invoke = (operation, input) => connections.invoke({ context, integrationId, operation, input });
56
+ const query = async (customer, query) => (await invoke("reports.search", { customerId: customer, query })).results || [];
57
+ const customer = value => validateSchemaPayload({ schema: createSchema({ customerId }), mode: "replace" }, { customerId: value }, { statusCode: 422 }).customerId;
58
+ const campaignId = value => validateSchemaPayload({ schema: createSchema({ id }), mode: "replace" }, { id: value }, { statusCode: 422 }).id;
59
+ const plan = () => validateGoogleAdsSearchPlan(configuration.extensions?.googleAdsSearch?.[integrationId]);
60
+ const review = data => hash([context.applicationId, context.subjectId, integrationId, slot, plan(), data]);
61
+ async function account(id) {
62
+ const rows = await query(customer(id), "SELECT customer.id, customer.descriptive_name, customer.currency_code, customer.time_zone, customer.status, customer.manager FROM customer LIMIT 1");
63
+ if (rows.length !== 1) error("Google did not return the selected account.");
64
+ return rows[0].customer;
65
+ }
66
+ async function preview() {
67
+ const p = plan(), selected = await account(p.customerId);
68
+ if (selected.manager || selected.status !== "ENABLED" || selected.currencyCode !== p.currency) error("Choose an active client account and match its currency.");
69
+ const goals = await query(p.customerId, `SELECT conversion_action.id, conversion_action.name, conversion_action.status, conversion_action.type FROM conversion_action WHERE conversion_action.id = ${p.conversionActionId} LIMIT 1`);
70
+ if (goals[0]?.conversionAction?.status !== "ENABLED" || goals[0]?.conversionAction?.type !== "WEBPAGE") error("Choose an enabled website conversion action.");
71
+ await invoke("search.validate", p);
72
+ const data = { plan: p, account: selected, conversion: goals[0].conversionAction };
73
+ return { ...data, reviewId: review(data) };
74
+ }
75
+ async function campaign(id) {
76
+ const p = plan(), selected = campaignId(id);
77
+ const rows = await query(p.customerId, `SELECT campaign.id, campaign.name, campaign.status, campaign.advertising_channel_type, campaign.bidding_strategy_type, campaign.network_settings.target_google_search, campaign.network_settings.target_search_network, campaign.network_settings.target_content_network, campaign.network_settings.target_partner_search_network, campaign.geo_target_type_setting.positive_geo_target_type, campaign.geo_target_type_setting.negative_geo_target_type, campaign.contains_eu_political_advertising, campaign_budget.amount_micros, campaign_budget.id FROM campaign WHERE campaign.id = ${selected} LIMIT 1`);
78
+ if (rows.length !== 1 || rows[0].campaign?.advertisingChannelType !== "SEARCH") error("Select an accessible Search campaign.");
79
+ const details = {
80
+ customerId: p.customerId, account: await account(p.customerId), ...rows[0],
81
+ ads: await query(p.customerId, `SELECT ad_group_ad.ad.id, ad_group_ad.ad.final_urls, ad_group_ad.ad.responsive_search_ad.headlines, ad_group_ad.ad.responsive_search_ad.descriptions, ad_group_ad.status, ad_group_ad.policy_summary.approval_status, ad_group.id, ad_group.status, ad_group.cpc_bid_micros FROM ad_group_ad WHERE campaign.id = ${selected} LIMIT 21`),
82
+ goals: await query(p.customerId, `SELECT conversion_goal_campaign_config.goal_config_level, conversion_goal_campaign_config.custom_conversion_goal FROM conversion_goal_campaign_config WHERE conversion_goal_campaign_config.campaign = 'customers/${p.customerId}/campaigns/${selected}' LIMIT 1`),
83
+ targets: await query(p.customerId, `SELECT campaign_criterion.type, campaign_criterion.location.geo_target_constant, campaign_criterion.language.language_constant, campaign_criterion.negative FROM campaign_criterion WHERE campaign.id = ${selected} LIMIT 31`),
84
+ keywords: await query(p.customerId, `SELECT ad_group_criterion.keyword.text, ad_group_criterion.keyword.match_type, ad_group_criterion.negative, ad_group_criterion.status FROM keyword_view WHERE campaign.id = ${selected} LIMIT 31`)
85
+ };
86
+ const goalResource = details.goals[0]?.conversionGoalCampaignConfig?.customConversionGoal;
87
+ if (goalResource) {
88
+ if (!new RegExp(`^customers/${p.customerId}/customConversionGoals/[0-9]+$`).test(goalResource)) error("Google returned an invalid campaign goal.");
89
+ details.customGoals = await query(p.customerId, `SELECT custom_conversion_goal.name, custom_conversion_goal.status, custom_conversion_goal.conversion_actions FROM custom_conversion_goal WHERE custom_conversion_goal.resource_name = '${goalResource}' LIMIT 1`);
90
+ }
91
+ if (details.ads.length > 20 || details.targets.length > 30 || details.keywords.length > 30) error("This campaign exceeds the bounded Search review. Manage it in Google Ads.");
92
+ return { ...details, reviewId: review(details) };
93
+ }
94
+ return {
95
+ async discover(id) {
96
+ if (!id) return { accounts: (await invoke("customers.listAccessible", {})).resourceNames || [] };
97
+ const selected = await account(id);
98
+ return { account: selected,
99
+ clients: selected.manager ? (await invoke("customers.listClients", { customerId: id })).results || [] : [],
100
+ conversions: await query(id, "SELECT conversion_action.id, conversion_action.name, conversion_action.status, conversion_action.type, conversion_action.tag_snippets FROM conversion_action WHERE conversion_action.type = 'WEBPAGE' AND conversion_action.status = 'ENABLED' LIMIT 50"),
101
+ campaigns: await query(id, "SELECT campaign.id, campaign.name, campaign.status FROM campaign WHERE campaign.advertising_channel_type = 'SEARCH' AND campaign.status != 'REMOVED' LIMIT 50") };
102
+ },
103
+ async targets(id, name) {
104
+ customer(id);
105
+ if (typeof name !== "string" || !/^[\p{L}\p{N} .-]{2,80}$/u.test(name)) error("Enter a location name using letters, numbers, spaces, dots or hyphens.");
106
+ return { locations: await query(id, `SELECT geo_target_constant.id, geo_target_constant.canonical_name, geo_target_constant.status FROM geo_target_constant WHERE geo_target_constant.name LIKE '%${name}%' AND geo_target_constant.status = 'ENABLED' LIMIT 20`),
107
+ languages: await query(id, "SELECT language_constant.id, language_constant.name FROM language_constant WHERE language_constant.targetable = TRUE LIMIT 100") };
108
+ },
109
+ preview,
110
+ async create(reviewId) {
111
+ const checked = await preview();
112
+ if (reviewId !== checked.reviewId) error("The Search plan changed. Review it again before creation.");
113
+ const response = await invoke("search.createPaused", checked.plan);
114
+ const resource = response.mutateOperationResponses.find(item => item.campaignResult)?.campaignResult.resourceName;
115
+ return { campaignId: resource.split("/").at(-1), status: "PAUSED" };
116
+ },
117
+ createConversion: ({ customerId, name }) => invoke("conversions.createWebsite", { customerId, name }),
118
+ campaign,
119
+ async launch({ campaignId, reviewId, trackingConfirmed, billingConfirmed }) {
120
+ const current = await campaign(campaignId);
121
+ if (current.reviewId !== reviewId) error("The campaign changed. Inspect and approve it again.");
122
+ if (trackingConfirmed !== true || billingConfirmed !== true) error("Confirm installed conversion tracking and billing/advertiser readiness before launch.");
123
+ if (current.account.manager || current.account.status !== "ENABLED") error("Choose an active client account.");
124
+ if (current.campaign.containsEuPoliticalAdvertising !== "DOES_NOT_CONTAIN_EU_POLITICAL_ADVERTISING" ||
125
+ !current.customGoals?.length || current.customGoals[0].customConversionGoal?.status !== "ENABLED") error("Review a non-political Search campaign with an enabled custom conversion goal.");
126
+ if (!current.ads.length || !current.keywords.length || current.campaign.biddingStrategyType !== "MANUAL_CPC") error("This screen launches only Search campaigns with manual CPC, ads and keywords. Manage other campaigns in Google Ads.");
127
+ if (current.campaign.status !== "PAUSED") error("Only a paused campaign can be launched here.");
128
+ return invoke("campaigns.setStatus", { customerId: current.customerId, campaignId, status: "ENABLED" });
129
+ },
130
+ pause: id => invoke("campaigns.setStatus", { customerId: plan().customerId, campaignId: campaignId(id), status: "PAUSED" }),
131
+ report: async () => ({ campaigns: await query(plan().customerId, "SELECT campaign.id, campaign.name, campaign.status, metrics.impressions, metrics.clicks, metrics.cost_micros, metrics.conversions FROM campaign WHERE segments.date DURING LAST_30_DAYS AND campaign.advertising_channel_type = 'SEARCH' LIMIT 50") })
132
+ };
133
+ }
134
+ export { googleAdsSearchOperations, createGoogleAdsSearchService };
@@ -0,0 +1,80 @@
1
+ import { createSchema } from "json-rest-schema";
2
+ import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
3
+ import { ConnectorError } from "@jskit-ai/connectors-core/server";
4
+ import { googleAdsSearchOperations } from "./google-ads-search.js";
5
+ import { googleAdsDefinition } from "../shared/google-ads.js";
6
+ import { googleProvider, googleRead } from "./google.js";
7
+
8
+ const origin = "https://googleads.googleapis.com";
9
+ const api = `${origin}/v25`;
10
+ const scope = "https://www.googleapis.com/auth/adwords";
11
+ const record = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
12
+ const customerId = { type: "string", required: true, noTrim: true, minLength: 10, maxLength: 10,
13
+ validator: (value) => /^[0-9]{10}$/u.test(value) || "Use a 10-digit customer ID without hyphens." };
14
+ const pageToken = { type: "string", minLength: 1, maxLength: 4096, noTrim: true,
15
+ validator: (value) => !/[\p{Cc}]/u.test(value) || "Use the returned page token without control characters." };
16
+ const searchSchema = createSchema({ customerId, pageToken,
17
+ query: { type: "string", required: true, minLength: 1, maxLength: 16000, noTrim: true,
18
+ validator: (value) => /^SELECT\s/iu.test(value) && !/[\p{Cc}]/u.test(value) && new TextEncoder().encode(value).length <= 32000 || "Use a single-line GAQL SELECT query of at most 32,000 UTF-8 bytes." }
19
+ });
20
+ const clientsSchema = createSchema({ customerId, pageToken });
21
+
22
+ function searchRequest(schema, input, settings, fixedQuery) {
23
+ const { customerId: id, ...body } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
24
+ return { method: "POST", url: `${api}/customers/${id}/googleAds:search`,
25
+ headers: settings.loginCustomerId ? { "login-customer-id": settings.loginCustomerId } : {},
26
+ body: fixedQuery ? { ...body, query: fixedQuery } : body };
27
+ }
28
+
29
+ function searchResult(value) {
30
+ return record(value) && !value.error &&
31
+ (value.results === undefined || Array.isArray(value.results) && value.results.length <= 10000 && value.results.every(record)) &&
32
+ (value.nextPageToken === undefined || typeof value.nextPageToken === "string" && value.nextPageToken.length <= 4096 && !/[\p{Cc}]/u.test(value.nextPageToken)) &&
33
+ (value.fieldMask === undefined || typeof value.fieldMask === "string") &&
34
+ (value.totalResultsCount === undefined || typeof value.totalResultsCount === "string" && /^[0-9]+$/u.test(value.totalResultsCount));
35
+ }
36
+
37
+ const googleAdsProvider = Object.freeze({
38
+ ...googleProvider(googleAdsDefinition, origin, "customers.listAccessible", {
39
+ ...googleAdsSearchOperations,
40
+ "customers.listAccessible": googleRead([scope], {}, () => ({ url: `${api}/customers:listAccessibleCustomers` }),
41
+ (value) => record(value) && !value.error && (value.resourceNames === undefined || Array.isArray(value.resourceNames) &&
42
+ value.resourceNames.every((name) => typeof name === "string" && /^customers\/[0-9]{10}$/u.test(name)))),
43
+ "reports.search": { scopes: [scope], request: (input, settings) => searchRequest(searchSchema, input, settings), validateResult: searchResult },
44
+ "customers.listClients": { scopes: [scope], request: (input, settings) => searchRequest(clientsSchema, input, settings,
45
+ "SELECT customer_client.client_customer, customer_client.level, customer_client.manager, customer_client.descriptive_name, customer_client.currency_code, customer_client.time_zone, customer_client.id, customer_client.status FROM customer_client"), validateResult: searchResult }
46
+ }),
47
+ async exchange(url, options, { fetchImpl }) {
48
+ const headers = new Headers(options.headers);
49
+ options.signal.throwIfAborted();
50
+ if (options.body !== undefined) headers.set("Content-Type", "application/json");
51
+ const response = await fetchImpl(url, { ...options, headers, credentials: "omit",
52
+ ...(options.body === undefined ? {} : { body: JSON.stringify(options.body) }) });
53
+ let value;
54
+ try { value = await response.json(); } catch { /* handled below */ }
55
+ if (!response.ok || value?.error) {
56
+ const details = Array.isArray(value?.error?.details) ? value.error.details : [];
57
+ const errors = details.flatMap((detail) => Array.isArray(detail?.errors) ? detail.errors : []);
58
+ const codes = errors.flatMap((error) => record(error?.errorCode) ? Object.values(error.errorCode) : []);
59
+ if (response.status === 429 || value?.error?.status === "RESOURCE_EXHAUSTED" || errors.some((error) => error?.errorCode?.quotaError)) {
60
+ throw new ConnectorError("connector_rate_limited", "Google Ads API capacity is exhausted. Check the Cloud project and account quotas.", { statusCode: 429 });
61
+ }
62
+ if (details.some((detail) => detail?.reason === "ACCESS_TOKEN_SCOPE_INSUFFICIENT")) {
63
+ throw new ConnectorError("connector_scope_missing", "Connect Google Ads again with its required permission.", { statusCode: 403 });
64
+ }
65
+ if (codes.some((code) => ["CLOUD_PROJECT_NOT_APPROVED_FOR_PRODUCTION"].includes(code))) {
66
+ throw new ConnectorError("connector_api_access_invalid", "Check Google Ads API access approval for the Cloud project that owns this OAuth client.", { statusCode: 403 });
67
+ }
68
+ if (codes.some((code) => ["OAUTH_TOKEN_INVALID", "OAUTH_TOKEN_EXPIRED", "OAUTH_TOKEN_REVOKED"].includes(code))) {
69
+ throw new ConnectorError("connector_reconnect_required", "Connect this Google Ads account again.", { statusCode: 401 });
70
+ }
71
+ if (response.status === 400) throw new ConnectorError("connector_input_invalid", "Google Ads rejected the operation. Check campaign fields, policy requirements and account permissions.", { statusCode: 422 });
72
+ if (response.status === 404) throw new ConnectorError("connector_resource_not_found", "This Google Ads resource is unavailable.", { statusCode: 404 });
73
+ throw Object.assign(new Error("Google Ads request failed."), { status: response.ok ? 502 : response.status });
74
+ }
75
+ if (response.status !== 200 || !record(value)) throw new ConnectorError("connector_response_invalid", "Google Ads returned an unexpected response.", { statusCode: 502 });
76
+ return value;
77
+ }
78
+ });
79
+
80
+ export { googleAdsProvider };
@@ -0,0 +1,36 @@
1
+ import { createSchema } from "json-rest-schema";
2
+ import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
3
+ import { googleDocsDefinition } from "../shared/google.js";
4
+ import { googleRead, googleProvider, googleFileVerification, documentId } from "./google.js";
5
+ const origin = "https://docs.googleapis.com";
6
+ const readScopes = ["documents.readonly", "documents", "drive.readonly", "drive.file", "drive"];
7
+ const writable = ["documents", "drive.file", "drive"].map(scope => `https://www.googleapis.com/auth/${scope}`);
8
+ const validDocument = value => typeof value?.documentId === "string" && typeof value.title === "string";
9
+ const get = googleRead(readScopes, { documentId }, ({ documentId }) => ({
10
+ url: `${origin}/v1/documents/${encodeURIComponent(documentId)}`, query: { includeTabsContent: true }
11
+ }), validDocument);
12
+ const create = createSchema({ title: { type: "string", required: true, minLength: 1, maxLength: 1024 } });
13
+ const batch = createSchema({ documentId, requests: { type: "array", required: true }, requiredRevisionId: { type: "string", minLength: 1, maxLength: 1024 } });
14
+ const base = googleProvider(googleDocsDefinition, origin, "connection.verify", {
15
+ "connection.verify": googleFileVerification(get, "documentId"),
16
+ "documents.get": get,
17
+ "documents.create": {
18
+ scopes: writable, validateResult: validDocument,
19
+ request(input) {
20
+ return { method: "POST", url: `${origin}/v1/documents`, body: validateSchemaPayload({ schema: create, mode: "replace" }, input, { statusCode: 422 }) };
21
+ }
22
+ },
23
+ "documents.batchUpdate": {
24
+ scopes: writable,
25
+ validateResult: value => typeof value?.documentId === "string" && (value.replies === undefined || Array.isArray(value.replies)),
26
+ request(input) {
27
+ const { documentId, requests, requiredRevisionId } = validateSchemaPayload({ schema: batch, mode: "replace" }, input, { statusCode: 422 });
28
+ if (!requests.length || requests.length > 100 || JSON.stringify(requests).length > 1048576 || requests.some(request => !request || typeof request !== "object" || Array.isArray(request) || Object.keys(request).length !== 1 || !/^[a-zA-Z][a-zA-Z0-9]+$/.test(Object.keys(request)[0]) || !Object.values(request)[0] || typeof Object.values(request)[0] !== "object" || Array.isArray(Object.values(request)[0]))) {
29
+ throw Object.assign(new Error("Supply 1–100 Google Docs request objects, one native operation per object, within 1 MiB."), { statusCode: 422 });
30
+ }
31
+ return { method: "POST", url: `${origin}/v1/documents/${encodeURIComponent(documentId)}:batchUpdate`, body: { requests, ...(requiredRevisionId ? { writeControl: { requiredRevisionId } } : {}) } };
32
+ }
33
+ }
34
+ });
35
+ const googleDocsProvider = Object.freeze({ ...base, apiOrigins: [origin, "https://www.googleapis.com"] });
36
+ export { googleDocsProvider };
@@ -0,0 +1,71 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { ConnectorError } from "@jskit-ai/connectors-core/server";
3
+ import { googleDriveDefinition } from "../shared/google.js";
4
+ import { googleOperation, googleRead, googleProvider, pageToken } from "./google.js";
5
+ const origin = "https://www.googleapis.com";
6
+ const fileId = { type: "string", required: true, minLength: 1, maxLength: 1024, validator: value => /^[A-Za-z0-9_-]+$/.test(value) || "Use a Drive file ID, not a URL." };
7
+ const contentScopes = ["drive", "drive.file", "drive.readonly"];
8
+ const writeScopes = ["drive", "drive.file"];
9
+ const validFile = value => typeof value?.id === "string";
10
+ const validBytes = value => typeof value?.bodyBase64 === "string" && Number.isInteger(value.size) && typeof value.contentType === "string";
11
+ const fields = "id,name,mimeType,parents,size,trashed,webViewLink,capabilities,exportLinks";
12
+ function url(fileId, query = {}, suffix = "") {
13
+ const address = new URL(`${origin}/drive/v3/files${fileId ? `/${encodeURIComponent(fileId)}` : ""}${suffix}`);
14
+ for (const [key, value] of Object.entries(query)) if (value !== undefined) address.searchParams.set(key, String(value));
15
+ return address.href;
16
+ }
17
+ const baseProvider = googleProvider(googleDriveDefinition, origin, "files.list", {
18
+ "files.get": googleRead([...contentScopes, "drive.metadata", "drive.metadata.readonly"], { fileId }, ({ fileId }) => ({ url: url(fileId, { supportsAllDrives: true, fields }) }), validFile),
19
+ "files.download": googleRead(contentScopes, { fileId }, ({ fileId }) => ({ url: url(fileId, { supportsAllDrives: true, alt: "media" }) }), validBytes),
20
+ "files.export": googleRead(contentScopes, { fileId, mimeType: { type: "string", required: true, minLength: 1, maxLength: 200 } }, ({ fileId, mimeType }) => ({ url: url(fileId, { mimeType }, "/export") }), validBytes),
21
+ "files.create": googleOperation(writeScopes, { name: { type: "string", required: true, minLength: 1, maxLength: 1024 }, parentId: { ...fileId, required: false }, folder: { type: "boolean", defaultTo: false } }, ({ name, parentId, folder }) => ({ method: "POST", url: url(undefined, { supportsAllDrives: true, fields }), body: { name, ...(parentId ? { parents: [parentId] } : {}), ...(folder ? { mimeType: "application/vnd.google-apps.folder" } : {}) } }), validFile),
22
+ "files.update": googleOperation([...writeScopes, "drive.metadata"], { fileId, name: { type: "string", minLength: 1, maxLength: 1024 }, description: { type: "string", maxLength: 16384 }, trashed: { type: "boolean" } }, ({ fileId, ...body }) => {
23
+ if (!Object.keys(body).length) throw Object.assign(new Error("Supply a name, description or trashed value."), { statusCode: 422 });
24
+ return { method: "PATCH", url: url(fileId, { supportsAllDrives: true, fields }), body };
25
+ }, validFile),
26
+ "files.upload": googleOperation(writeScopes, {
27
+ fileId: { ...fileId, required: false }, name: { type: "string", required: true, minLength: 1, maxLength: 1024 },
28
+ parentId: { ...fileId, required: false }, contentType: { type: "string", required: true, minLength: 3, maxLength: 200 },
29
+ bodyBase64: { type: "string", required: true, maxLength: 6666668, noTrim: true }
30
+ }, ({ fileId, name, parentId, contentType, bodyBase64 }) => {
31
+ if (!/^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+$/.test(contentType)) throw Object.assign(new Error("Use a MIME type without header parameters."), { statusCode: 422 });
32
+ const data = Buffer.from(bodyBase64, "base64");
33
+ if (data.toString("base64") !== bodyBase64) throw Object.assign(new Error("Use standard padded base64 file data."), { statusCode: 422 });
34
+ if (data.byteLength > 5_000_000) throw Object.assign(new Error("Uploads above 5 MB require the framework's native resumable client."), { statusCode: 413 });
35
+ if (fileId && parentId) throw Object.assign(new Error("parentId applies to new files; moving files requires native addParents/removeParents wiring."), { statusCode: 422 });
36
+ const boundary = `drive_${randomUUID()}`;
37
+ const metadata = { name, ...(parentId ? { parents: [parentId] } : {}) };
38
+ const body = Buffer.concat([Buffer.from(`--${boundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n${JSON.stringify(metadata)}\r\n--${boundary}\r\nContent-Type: ${contentType}\r\n\r\n`), data, Buffer.from(`\r\n--${boundary}--\r\n`)]);
39
+ return { method: fileId ? "PATCH" : "POST", url: url(fileId, { uploadType: "multipart", supportsAllDrives: true, fields }).replace("/drive/v3/", "/upload/drive/v3/"), headers: { "Content-Type": `multipart/related; boundary=${boundary}` }, body };
40
+ }, validFile),
41
+ "files.list": googleRead(["drive", "drive.file", "drive.metadata", "drive.metadata.readonly", "drive.readonly", "drive.meet.readonly", "drive.photos.readonly"], {
42
+ pageToken, pageSize: { type: "integer", min: 1, max: 1000, defaultTo: 100 },
43
+ q: { type: "string", maxLength: 8192 },
44
+ driveId: { type: "string", minLength: 1, maxLength: 1024 }
45
+ }, ({ driveId, ...query }) => ({ url: `${origin}/drive/v3/files`, query: {
46
+ ...query, fields: "kind,nextPageToken,incompleteSearch,files(id,name,mimeType,modifiedTime)",
47
+ ...(driveId ? { driveId, corpora: "drive", supportsAllDrives: true, includeItemsFromAllDrives: true } : {})
48
+ } }), (value) => value?.kind === "drive#fileList" && (value.files === undefined || Array.isArray(value.files)))
49
+ });
50
+ const googleDriveProvider = Object.freeze({ ...baseProvider,
51
+ async exchange(address, options, { request, fetchImpl }) {
52
+ const target = new URL(address);
53
+ const upload = target.pathname.startsWith("/upload/drive/v3/");
54
+ const download = target.searchParams.get("alt") === "media" || target.pathname.endsWith("/export");
55
+ if (!upload && !download) return request(address, options);
56
+ const response = await fetchImpl(address, { ...options, credentials: "omit", redirect: "error" });
57
+ if (!response.ok) { await response.body?.cancel(); throw Object.assign(new Error("Google Drive transfer failed."), { status: response.status }); }
58
+ const chunks = []; let size = 0;
59
+ const reader = response.body?.getReader();
60
+ if (reader) try { while (true) {
61
+ const { done, value } = await reader.read(); if (done) break;
62
+ size += value.byteLength;
63
+ if (size > 8 * 1024 * 1024) { await reader.cancel(); throw new ConnectorError("connector_response_too_large", "Use the framework's native Drive client for downloads or exports above 8 MiB.", { statusCode: 413 }); }
64
+ chunks.push(value);
65
+ } } finally { reader.releaseLock(); }
66
+ const body = Buffer.concat(chunks);
67
+ if (upload) return JSON.parse(body.toString("utf8"));
68
+ return { bodyBase64: body.toString("base64"), contentType: response.headers.get("content-type") || "application/octet-stream", size };
69
+ }
70
+ });
71
+ export { googleDriveProvider };
@@ -0,0 +1,90 @@
1
+ import { createSchema } from "json-rest-schema";
2
+ import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
3
+ import { ConnectorError } from "@jskit-ai/connectors-core/server";
4
+ import { googleMapsPlatformDefinition } from "../shared/google-maps-platform.js";
5
+ import { jsonOperation } from "./jsonOperation.js";
6
+
7
+ const endpoint = "https://maps.googleapis.com/maps/api/geocode/json";
8
+ const language = { type: "string", minLength: 2, maxLength: 35,
9
+ validator: (value) => /^[a-z]{2,3}(?:-[a-z0-9]{2,8})*$/i.test(value) || "Use a language code such as en or pt-BR." };
10
+ const reverseSchema = createSchema({
11
+ latitude: { type: "number", required: true, min: -90, max: 90 },
12
+ longitude: { type: "number", required: true, min: -180, max: 180 },
13
+ language
14
+ });
15
+
16
+ function validateGeocoding(result) {
17
+ switch (result?.status) {
18
+ case "OVER_QUERY_LIMIT":
19
+ throw new ConnectorError("connector_rate_limited", "The Google Maps request quota was reached.", { statusCode: 429 });
20
+ case "OVER_DAILY_LIMIT":
21
+ throw new ConnectorError("connector_configuration_invalid", "Check the Google Maps server key, billing and usage cap.");
22
+ case "REQUEST_DENIED":
23
+ throw new ConnectorError("connector_permission_denied", "Google Maps denied access. Check the enabled API and key restrictions.", { statusCode: 403 });
24
+ case "INVALID_REQUEST":
25
+ throw new ConnectorError("connector_input_invalid", "Google Maps could not accept this geocoding request.", { statusCode: 422 });
26
+ case "UNKNOWN_ERROR":
27
+ throw new ConnectorError("connector_provider_failed", "Google Maps could not process the request. Try again.", { statusCode: 502 });
28
+ case "ZERO_RESULTS":
29
+ return Array.isArray(result.results) && result.results.length === 0;
30
+ case "OK":
31
+ return Array.isArray(result.results) && result.results.length > 0 && result.results.every((place) =>
32
+ typeof place?.formatted_address === "string" && typeof place.place_id === "string" &&
33
+ Number.isFinite(place.geometry?.location?.lat) && Math.abs(place.geometry.location.lat) <= 90 &&
34
+ Number.isFinite(place.geometry.location.lng) && Math.abs(place.geometry.location.lng) <= 180);
35
+ default:
36
+ return false;
37
+ }
38
+ }
39
+
40
+ function mapsOperation(fields, request, validateResult) {
41
+ const schema = createSchema(fields);
42
+ return { scopes: [], validateResult, request(input) { return request(validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 })); } };
43
+ }
44
+ const latitude = { type: "number", required: true, min: -90, max: 90 };
45
+ const longitude = { type: "number", required: true, min: -180, max: 180 };
46
+ const placeFields = "id,displayName,formattedAddress,location,attributions,googleMapsUri";
47
+ const googleMapsPlatformProvider = Object.freeze({
48
+ ...googleMapsPlatformDefinition,
49
+ apiOrigins: ["https://maps.googleapis.com", "https://places.googleapis.com", "https://routes.googleapis.com"],
50
+ apiKey: { queryParameter: "key" },
51
+ checkOperation: "geocoding.forward",
52
+ exchange(address, options, { request, apiKey }) {
53
+ const target = new URL(address);
54
+ if (target.hostname === "maps.googleapis.com") return request(address, options);
55
+ target.searchParams.delete("key");
56
+ return request(target.href, { ...options, headers: { ...options.headers, "X-Goog-Api-Key": apiKey } });
57
+ },
58
+ operations: {
59
+ "places.search": mapsOperation({ textQuery: { type: "string", required: true, minLength: 1, maxLength: 1000 }, pageSize: { type: "integer", min: 1, max: 20, defaultTo: 10 }, pageToken: { type: "string", minLength: 1, maxLength: 4096 }, languageCode: language }, body => ({ method: "POST", url: "https://places.googleapis.com/v1/places:searchText", headers: { "X-Goog-FieldMask": placeFields.split(",").map(field => `places.${field}`).join(",") + ",nextPageToken" }, body }),
60
+ result => Boolean(result && typeof result === "object" && !Array.isArray(result) && !result.error && (result.places === undefined || Array.isArray(result.places) && result.places.every(place => typeof place?.id === "string")))),
61
+ "places.get": mapsOperation({ placeId: { type: "string", required: true, minLength: 1, maxLength: 512, validator: value => /^[A-Za-z0-9_-]+$/.test(value) || "Use a place ID, not a URL." } }, ({ placeId }) => ({ method: "GET", url: `https://places.googleapis.com/v1/places/${encodeURIComponent(placeId)}`, headers: { "X-Goog-FieldMask": placeFields } }), result => typeof result?.id === "string"),
62
+ "routes.compute": mapsOperation({ originLatitude: latitude, originLongitude: longitude, destinationLatitude: latitude, destinationLongitude: longitude,
63
+ travelMode: { type: "string", enum: ["DRIVE", "WALK", "BICYCLE"], defaultTo: "DRIVE" }
64
+ }, ({ originLatitude, originLongitude, destinationLatitude, destinationLongitude, travelMode }) => ({ method: "POST", url: "https://routes.googleapis.com/directions/v2:computeRoutes", headers: { "X-Goog-FieldMask": "routes.distanceMeters,routes.duration,routes.polyline.geoJsonLinestring" }, body: {
65
+ origin: { location: { latLng: { latitude: originLatitude, longitude: originLongitude } } },
66
+ destination: { location: { latLng: { latitude: destinationLatitude, longitude: destinationLongitude } } },
67
+ travelMode, polylineEncoding: "GEO_JSON_LINESTRING"
68
+ } }), result => Boolean(result && typeof result === "object" && !Array.isArray(result) && !result.error && (result.routes === undefined || Array.isArray(result.routes)))),
69
+ "geocoding.forward": jsonOperation(endpoint, {
70
+ address: { type: "string", required: true, minLength: 1, maxLength: 1000,
71
+ validator: (value) => Boolean(value.trim()) || "Enter an address or plus code." },
72
+ language,
73
+ region: { type: "string", minLength: 2, maxLength: 2,
74
+ validator: (value) => /^[a-z]{2}$/i.test(value) || "Use a two-letter region code." }
75
+ }, validateGeocoding),
76
+ "geocoding.reverse": {
77
+ scopes: [],
78
+ request(input) {
79
+ const values = validateSchemaPayload({ schema: reverseSchema, mode: "replace" }, input, { statusCode: 422 });
80
+ const url = new URL(endpoint);
81
+ url.searchParams.set("latlng", `${values.latitude},${values.longitude}`);
82
+ if (values.language !== undefined) url.searchParams.set("language", values.language);
83
+ return { method: "GET", url: url.href };
84
+ },
85
+ validateResult: validateGeocoding
86
+ }
87
+ }
88
+ });
89
+
90
+ export { googleMapsPlatformProvider };
@@ -0,0 +1,31 @@
1
+ import { googleSearchConsoleDefinition } from "../shared/google.js";
2
+ import { googleOperation, googleRead, googleProvider } from "./google.js";
3
+ const origin = "https://searchconsole.googleapis.com";
4
+ const readScopes = ["webmasters.readonly", "webmasters"];
5
+ const website = value => { try { const url = new URL(value); return ["http:", "https:"].includes(url.protocol) && !url.username && !url.password && !url.hash; } catch { return false; } };
6
+ const siteUrl = { type: "string", required: true, minLength: 1, maxLength: 2048, validator: value => /^sc-domain:[A-Za-z0-9.-]+$/.test(value) || website(value) || "Copy the exact property URL or sc-domain:name from sites.list." };
7
+ const feedpath = { type: "string", required: true, minLength: 1, maxLength: 2048, validator: value => website(value) || "Use the full sitemap URL." };
8
+ const date = { type: "string", required: true, maxLength: 10, validator: value => /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(value)) && new Date(value).toISOString().slice(0, 10) === value || "Use a valid YYYY-MM-DD date." };
9
+ const dimensions = ["date", "country", "device", "page", "query", "searchAppearance"];
10
+ const plain = value => Boolean(value && typeof value === "object" && !Array.isArray(value) && !value.error);
11
+ function propertyPath(site) { return `${origin}/webmasters/v3/sites/${encodeURIComponent(site)}`; }
12
+ const googleSearchConsoleProvider = googleProvider(googleSearchConsoleDefinition, origin, "sites.list", {
13
+ "sites.get": googleRead(readScopes, { siteUrl }, ({ siteUrl }) => ({ url: propertyPath(siteUrl) }), value => typeof value?.siteUrl === "string" && typeof value.permissionLevel === "string"),
14
+ "searchAnalytics.query": googleOperation(readScopes, { siteUrl, startDate: date, endDate: date,
15
+ dimensions: { type: "array", validator: value => value.length <= dimensions.length && new Set(value).size === value.length && value.every(dimension => dimensions.includes(dimension)) || "Choose distinct supported dimensions." },
16
+ filters: { type: "array", validator: filters => filters.length <= 100 && filters.every(filter => filter && typeof filter === "object" && !Array.isArray(filter) && Object.keys(filter).every(key => ["dimension", "operator", "expression"].includes(key)) && dimensions.includes(filter.dimension) && filter.dimension !== "date" && ["contains", "equals", "notContains", "notEquals", "includingRegex", "excludingRegex"].includes(filter.operator) && typeof filter.expression === "string" && filter.expression.length <= 4096) || "Use supported dimension/operator/expression filters." },
17
+ type: { type: "string", enum: ["web", "image", "video", "news", "discover", "googleNews"], defaultTo: "web" },
18
+ rowLimit: { type: "integer", min: 1, max: 25000, defaultTo: 1000 }, startRow: { type: "integer", min: 0, defaultTo: 0 }
19
+ }, ({ siteUrl, filters, ...body }) => {
20
+ if (body.startDate > body.endDate) throw Object.assign(new Error("startDate must not follow endDate."), { statusCode: 422 });
21
+ return { method: "POST", url: `${propertyPath(siteUrl)}/searchAnalytics/query`, body: { ...body, dataState: "final", ...(filters?.length ? { dimensionFilterGroups: [{ groupType: "and", filters }] } : {}) } };
22
+ }, value => plain(value) && (value.rows === undefined || Array.isArray(value.rows))),
23
+ "urlInspection.inspect": googleOperation(readScopes, { siteUrl, inspectionUrl: { ...feedpath }, languageCode: { type: "string", minLength: 2, maxLength: 35 } }, body => ({ method: "POST", url: `${origin}/v1/urlInspection/index:inspect`, body }), value => plain(value?.inspectionResult)),
24
+ "sitemaps.list": googleRead(readScopes, { siteUrl }, ({ siteUrl }) => ({ url: `${propertyPath(siteUrl)}/sitemaps` }), value => plain(value) && (value.sitemap === undefined || Array.isArray(value.sitemap))),
25
+ "sitemaps.get": googleRead(readScopes, { siteUrl, feedpath }, ({ siteUrl, feedpath }) => ({ url: `${propertyPath(siteUrl)}/sitemaps/${encodeURIComponent(feedpath)}` }), value => typeof value?.path === "string"),
26
+ "sitemaps.submit": googleOperation(["webmasters"], { siteUrl, feedpath }, ({ siteUrl, feedpath }) => ({ method: "PUT", url: `${propertyPath(siteUrl)}/sitemaps/${encodeURIComponent(feedpath)}` }), value => value === null || plain(value) && !Object.keys(value).length),
27
+ "sitemaps.delete": googleOperation(["webmasters"], { siteUrl, feedpath }, ({ siteUrl, feedpath }) => ({ method: "DELETE", url: `${propertyPath(siteUrl)}/sitemaps/${encodeURIComponent(feedpath)}` }), value => value === null || plain(value) && !Object.keys(value).length),
28
+ "sites.list": googleRead(["webmasters.readonly", "webmasters"], {}, () => ({ url: `${origin}/webmasters/v3/sites` }),
29
+ (value) => Boolean(value && typeof value === "object" && !Array.isArray(value) && !value.error && (value.siteEntry === undefined || Array.isArray(value.siteEntry))))
30
+ });
31
+ export { googleSearchConsoleProvider };