@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,242 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+ import { generateKeyPair, exportPKCS8, jwtVerify } from "jose";
7
+ import { createConnectionService } from "../../connectors-core/src/server/index.js";
8
+ import { createFileConnectionStore, createCredentialProtection } from "../../connectors-core/src/server/fileStorage.js";
9
+ import { parseIntegrationConfiguration } from "../../connectors-core/src/shared/configuration.js";
10
+ import { firebaseCloudMessagingProvider as provider } from "../src/server/firebase-cloud-messaging.js";
11
+ import { firebaseMessagingScope } from "../src/shared/firebase-cloud-messaging.js";
12
+ import { firebaseCloudMessagingWebConfiguration } from "../src/client/firebase-cloud-messaging.js";
13
+
14
+ const keys = await generateKeyPair("RS256", { extractable: true });
15
+ const account = { type: "service_account", project_id: "sender-project", private_key_id: "a".repeat(40),
16
+ private_key: await exportPKCS8(keys.privateKey), client_email: "messaging@sender-project.iam.gserviceaccount.com",
17
+ token_uri: "https://oauth2.googleapis.com/token", universe_domain: "googleapis.com" };
18
+ const web = { clientMode: "web", projectId: "target-project", apiKey: `AIza${"a".repeat(35)}`, appId: "1:123456789012:web:abcdef1234567890", vapidKey: `B${"a".repeat(85)}A` };
19
+ const context = { applicationId: "notifications-app", subjectId: "team" };
20
+ const input = { context, integrationId: "push" };
21
+ const message = { fid: "fixture-installation-id", notification: { title: "Order ready", body: "Your order is ready." }, data: { orderId: "42" } };
22
+ async function fixture(t) {
23
+ const directory = await mkdtemp(path.join(tmpdir(), "firebase-connector-"));
24
+ t.after(() => rm(directory, { recursive: true, force: true }));
25
+ const state = { now: Date.now(), credential: JSON.stringify(account, null, 2), grants: [], requests: [], status: 200, tokenStatus: 200 };
26
+ const options = {
27
+ configuration: { schemaVersion: 1, registrations: {}, integrations: { push: {
28
+ provider: provider.id, accountMode: "shared", scopes: [firebaseMessagingScope], settings: { projectId: "target-project", clientMode: "server" },
29
+ authentication: { method: "service-account", secretRef: "env:FIREBASE_SERVICE_ACCOUNT" }, extensions: { cli: true }
30
+ } }, extensions: { retained: true } },
31
+ now: () => state.now, providers: [{ ...provider, requestTimeoutMs: 100 }], authorize: async (owner) => owner,
32
+ store: createFileConnectionStore({ directory, protection: createCredentialProtection({ keys: { current: new Uint8Array(32).fill(7) }, activeKeyId: "current" }) }),
33
+ resolveReference: async () => state.credential,
34
+ fetchImpl: async (address, init) => {
35
+ init.signal.throwIfAborted(); const url = String(address);
36
+ assert.equal(init.redirect, "error"); assert.equal(init.credentials, "omit");
37
+ const token = url === account.token_uri;
38
+ if (token) state.grants.push({ init, parameters: new URLSearchParams(init.body) });
39
+ else { assert.equal(url, "https://fcm.googleapis.com/v1/projects/target-project/messages:send"); state.requests.push({ init, body: JSON.parse(init.body) }); }
40
+ if (state.hang === (token ? "token" : "message")) {
41
+ state.started?.();
42
+ return new Promise((_, reject) => init.signal.addEventListener("abort", () => reject(init.signal.reason), { once: true }));
43
+ }
44
+ if (token && state.rawTokenResponse) return new Response("not-json", { status: 200 });
45
+ return token ? Response.json(state.tokenResponse ?? { token_type: "Bearer", access_token: `access-${state.grants.length}`, expires_in: 3600 }, { status: state.tokenStatus })
46
+ : Response.json(state.response ?? { name: "projects/target-project/messages/fixture-receipt" }, { status: state.status });
47
+ }
48
+ };
49
+ return { state, options, directory, service: createConnectionService(options) };
50
+ }
51
+
52
+ test("Firebase CLI validates both client modes, public fields and indirect credentials without accepting mixed setup", async (t) => {
53
+ const f = await fixture(t);
54
+ const parse = (config) => parseIntegrationConfiguration(JSON.stringify(config), { providers: [provider] });
55
+ assert.deepEqual(parse(f.options.configuration), f.options.configuration);
56
+ const browser = structuredClone(f.options.configuration); browser.integrations.push.settings = web;
57
+ assert.deepEqual(parse(browser).integrations.push.settings, web);
58
+ for (const changes of [{ projectId: "https://target-project" }, { projectId: "123456789012" }, { projectId: "UPPERCASE" }, { projectId: "bad-" }, { clientMode: "unknown" }, { apiKey: web.apiKey }]) {
59
+ const config = structuredClone(f.options.configuration); Object.assign(config.integrations.push.settings, changes);
60
+ assert.throws(() => parse(config), { code: "integration_configuration_invalid" });
61
+ }
62
+ for (const field of ["apiKey", "appId", "vapidKey"]) {
63
+ for (const value of [undefined, "", "env:WRONG_KIND", "raw-invalid-value"]) {
64
+ const config = structuredClone(browser); config.integrations.push.settings[field] = value;
65
+ assert.throws(() => parse(config), { code: "integration_configuration_invalid" });
66
+ }
67
+ }
68
+ for (const changes of [{ scopes: [] }, { scopes: ["openid"] }, { accountMode: "per-user" },
69
+ { authentication: { method: "service-account", secretRef: JSON.stringify(account) } },
70
+ { authentication: { method: "api-key", secretRef: "env:KEY" } },
71
+ { authentication: { method: "service-account", secretRef: "env:KEY", registrationRef: "google" } }]) {
72
+ const config = structuredClone(browser); Object.assign(config.integrations.push, changes);
73
+ assert.throws(() => parse(config), { code: "integration_configuration_invalid" });
74
+ }
75
+ assert.equal(f.state.grants.length, 0);
76
+ });
77
+
78
+ test("the browser helper returns only public Firebase settings and derives the sender ID without numeric rounding", () => {
79
+ const config = firebaseCloudMessagingWebConfiguration({ ...web, appId: "1:9007199254740993:web:abcdef1234567890" });
80
+ assert.deepEqual(config, { firebaseConfig: { projectId: web.projectId, apiKey: web.apiKey, appId: "1:9007199254740993:web:abcdef1234567890", messagingSenderId: "9007199254740993" }, vapidKey: web.vapidKey });
81
+ assert.throws(() => firebaseCloudMessagingWebConfiguration({ projectId: "project-id" }), TypeError);
82
+ assert.throws(() => firebaseCloudMessagingWebConfiguration({ ...web, private_key: account.private_key }));
83
+ });
84
+
85
+ test("Firebase signs a bounded RS256 assertion and verifies target-project IAM using only a dry-run message", async (t) => {
86
+ const f = await fixture(t); const connected = await f.service.connectServiceAccount(input);
87
+ assert.equal(connected.status, "connected"); assert.equal(f.state.grants.length, 1); assert.equal(f.state.requests.length, 1);
88
+ const grant = f.state.grants[0]; assert.equal(grant.parameters.get("grant_type"), "urn:ietf:params:oauth:grant-type:jwt-bearer");
89
+ assert.equal(new Headers(grant.init.headers).get("content-type"), "application/x-www-form-urlencoded");
90
+ const { payload, protectedHeader } = await jwtVerify(grant.parameters.get("assertion"), keys.publicKey, {
91
+ algorithms: ["RS256"], issuer: account.client_email, audience: account.token_uri, currentDate: new Date(f.state.now)
92
+ });
93
+ assert.deepEqual(protectedHeader, { alg: "RS256", typ: "JWT", kid: account.private_key_id });
94
+ assert.deepEqual(payload, { scope: firebaseMessagingScope, iss: account.client_email, aud: account.token_uri, iat: Math.floor(f.state.now / 1000), exp: Math.floor(f.state.now / 1000) + 3600 });
95
+ assert.deepEqual(f.state.requests[0].body, { validate_only: true, message: { topic: "jskit-connection-check", data: { connectionCheck: "true" } } });
96
+ assert.equal(new Headers(f.state.requests[0].init.headers).get("authorization"), "Bearer access-1");
97
+ for (const file of await readdir(f.directory)) {
98
+ const text = await readFile(path.join(f.directory, file), "utf8"); assert(!text.includes("access-1")); assert(!text.includes("PRIVATE KEY"));
99
+ }
100
+ });
101
+
102
+ test("explicit validate and send operations support each target and never replay or turn validation into delivery", async (t) => {
103
+ const f = await fixture(t); await f.service.connectServiceAccount(input);
104
+ for (const target of [{ fid: "fixture-fid" }, { token: "fixture-token" }, { topic: "news_updates" }, { condition: "'news' in topics && 'offers' in topics" }]) {
105
+ for (const operation of ["messages.validate", "messages.send"]) {
106
+ const value = { ...target, notification: message.notification, data: message.data };
107
+ const before = f.state.requests.length;
108
+ await f.service.invoke({ ...input, operation, input: { message: value } });
109
+ assert.deepEqual(f.state.requests.at(-1).body, { message: value, validate_only: operation === "messages.validate" });
110
+ assert.equal(f.state.requests.length, before + 1);
111
+ }
112
+ }
113
+ assert.equal(f.state.grants.length, 1);
114
+ });
115
+
116
+ test("Firebase rejects malformed targets, reserved data and oversized payloads before sending", async (t) => {
117
+ const f = await fixture(t); await f.service.connectServiceAccount(input); const before = f.state.requests.length;
118
+ for (const value of [{}, { ...message, topic: "extra-target" }, { ...message, fid: "white space" },
119
+ { topic: "/topics/news", data: { value: "x" } }, { fid: "x" }, { fid: "x", notification: {} },
120
+ { ...message, name: "projects/another/messages/1" }, { ...message, data: { count: 2 } },
121
+ { ...message, data: { from: "x" } }, { ...message, data: { "google.x": "x" } },
122
+ { ...message, data: { "gcm.notification.body": "x" } }, { ...message, data: { value: "💌".repeat(1100) } },
123
+ { topic: "news", data: { value: "x".repeat(2100) } }, { ...message, notification: { image: "http://image.test/x" } }]) {
124
+ await assert.rejects(f.service.invoke({ ...input, operation: "messages.send", input: { message: value } }), { code: "connector_input_invalid" });
125
+ }
126
+ await assert.rejects(f.service.invoke({ ...input, operation: "messages.send", input: { message, validate_only: true } }), { code: "connector_input_invalid" });
127
+ await assert.rejects(f.service.invoke({ ...input, operation: "messages.delete" }), { code: "connector_operation_unknown" });
128
+ assert.equal(f.state.requests.length, before);
129
+ });
130
+
131
+ test("Firebase accepts only explicit service-account keys and never loads another credential or token destination", async (t) => {
132
+ const f = await fixture(t);
133
+ for (const changes of [{ type: "external_account" }, { type: "authorized_user" }, { project_id: "../other" }, { private_key: "bad-key" },
134
+ { private_key_id: "unsafe\nkey" }, { client_email: "not-an-account@example.com" }, { token_uri: "http://169.254.169.254/token" },
135
+ { token_uri: "https://oauth2.googleapis.com/token?extra=1" }, { universe_domain: "other.googleapis.com" }]) {
136
+ f.state.credential = JSON.stringify({ ...account, ...changes });
137
+ await assert.rejects(f.service.connectServiceAccount(input), { code: "connector_binding_missing" });
138
+ }
139
+ f.state.credential = "{not-json";
140
+ await assert.rejects(f.service.connectServiceAccount(input), { code: "connector_binding_missing" });
141
+ assert.equal(f.state.grants.length, 0); assert.equal(f.state.requests.length, 0);
142
+ });
143
+
144
+ test("failed or malformed token grants remain disconnected and redact provider diagnostics", async (t) => {
145
+ const f = await fixture(t);
146
+ for (const [status, value, code] of [[400, { error: "invalid_grant" }, "connector_reconnect_required"],
147
+ [403, {}, "connector_permission_denied"], [429, {}, "connector_rate_limited"], [500, {}, "connector_provider_failed"],
148
+ [200, { access_token: "raw-private-token", token_type: "Bearer" }, "connector_response_invalid"],
149
+ [302, {}, "connector_response_invalid"]]) {
150
+ f.state.tokenStatus = status; f.state.tokenResponse = { ...value, error_description: account.private_key };
151
+ await assert.rejects(f.service.connectServiceAccount(input), (error) => error.code === code && !JSON.stringify(error).includes("PRIVATE KEY"));
152
+ }
153
+ f.state.rawTokenResponse = true;
154
+ await assert.rejects(f.service.connectServiceAccount(input), { code: "connector_response_invalid" });
155
+ assert.equal((await f.service.status(input)).status, "disconnected"); assert.equal(f.state.requests.length, 0);
156
+ });
157
+
158
+ test("Firebase cannot claim a connection for denied sends or a malformed or wrong-project receipt", async (t) => {
159
+ const f = await fixture(t);
160
+ for (const [status, response, code] of [[401, {}, "connector_reconnect_required"], [403, {}, "connector_permission_denied"],
161
+ [429, {}, "connector_rate_limited"], [500, {}, "connector_provider_failed"], [200, {}, "connector_response_invalid"],
162
+ [200, { name: "projects/other-project/messages/1" }, "connector_response_invalid"], [200, { name: "garbage" }, "connector_response_invalid"]]) {
163
+ f.state.status = status; f.state.response = response;
164
+ await assert.rejects(f.service.connectServiceAccount(input), { code });
165
+ assert.equal((await f.service.status(input)).status, "disconnected");
166
+ }
167
+ });
168
+
169
+ test("Firebase grants survive file restart, renew with rotated credentials and retain owner and project boundaries", async (t) => {
170
+ const f = await fixture(t); await f.service.connectServiceAccount(input);
171
+ const restarted = createConnectionService(f.options); await restarted.invoke({ ...input, operation: "messages.validate", input: { message } });
172
+ assert.equal(f.state.grants.length, 1); f.state.now += 3_600_000;
173
+ await restarted.invoke({ ...input, operation: "messages.validate", input: { message } }); assert.equal(f.state.grants.length, 2);
174
+ f.state.credential = JSON.stringify({ ...account, private_key_id: "b".repeat(40) });
175
+ await restarted.invoke({ ...input, operation: "messages.validate", input: { message } }); assert.equal(f.state.grants.length, 3);
176
+ for (const owner of [{ ...context, applicationId: "other" }, { ...context, subjectId: "other" }]) {
177
+ await assert.rejects(restarted.invoke({ ...input, context: owner, operation: "messages.send", input: { message } }), { code: "connector_reconnect_required" });
178
+ }
179
+ const configuration = structuredClone(f.options.configuration); configuration.integrations.push.settings.projectId = "other-project";
180
+ const changed = createConnectionService({ ...f.options, configuration }); assert.equal((await changed.status(input)).status, "reconnect-required");
181
+ await assert.rejects(changed.invoke({ ...input, operation: "messages.send", input: { message } }), { code: "connector_reconnect_required" });
182
+ await restarted.disconnect(input); assert.equal((await f.service.status(input)).status, "disconnected");
183
+ });
184
+
185
+ test("sending requires application authorization over the exact audience and payload", async (t) => {
186
+ const f = await fixture(t); await f.service.connectServiceAccount(input);
187
+ let release; const paused = new Promise((resolve) => { release = resolve; }); let started;
188
+ const authorizing = new Promise((resolve) => { started = resolve; });
189
+ const service = createConnectionService({ ...f.options, authorize: async (owner, operation) => {
190
+ if (operation.operation !== "messages.send") return owner;
191
+ if (operation.input.message.fid !== message.fid) return null;
192
+ started(); await paused; operation.input.message.fid = "policy-mutation"; return owner;
193
+ } });
194
+ await assert.rejects(service.invoke({ ...input, operation: "messages.send", input: { message: { ...message, fid: "not-owned" } } }), { code: "connector_access_denied" });
195
+ const values = { message: structuredClone(message) };
196
+ const pending = service.invoke({ ...input, operation: "messages.send", input: values }); await authorizing;
197
+ values.message.fid = "caller-mutation"; release(); await pending;
198
+ assert.equal(f.state.requests.at(-1).body.message.fid, message.fid);
199
+ });
200
+
201
+ test("Firebase cancellation and timeout never retry a token exchange or notification send", async (t) => {
202
+ const f = await fixture(t);
203
+ for (const stage of ["token", "message"]) {
204
+ f.state.hang = stage; const control = new AbortController(); const started = new Promise((resolve) => { f.state.started = resolve; });
205
+ const pending = f.service.connectServiceAccount({ ...input, signal: control.signal }); await started; control.abort();
206
+ await assert.rejects(pending, { code: "connector_cancelled" });
207
+ }
208
+ f.state.hang = false; await f.service.connectServiceAccount(input);
209
+ f.state.hang = "message"; const before = f.state.requests.length;
210
+ const keepAlive = setTimeout(() => {}, 1000);
211
+ try { await assert.rejects(f.service.invoke({ ...input, operation: "messages.send", input: { message } }), { code: "connector_provider_timeout" }); } finally { clearTimeout(keepAlive); }
212
+ assert.equal(f.state.requests.length, before + 1); assert.equal((await f.service.status(input)).status, "connected");
213
+ });
214
+
215
+ test("the published Firebase setup example validates as the same CLI configuration", async () => {
216
+ const guide = await readFile(new URL("../docs/firebase-cloud-messaging.md", import.meta.url), "utf8");
217
+ const source = guide.match(/```json\n([\s\S]*?)\n```/u)[1];
218
+ const config = parseIntegrationConfiguration(source, { providers: [provider] });
219
+ assert.equal(config.integrations.push.authentication.method, "service-account");
220
+ assert.equal(config.integrations.push.settings.projectId, "my-notifications-app");
221
+ assert.deepEqual(config.registrations, {});
222
+ });
223
+
224
+
225
+ test("Firebase distinguishes expired recipients from malformed payloads without invalidating the sender grant", async t => {
226
+ const f = await fixture(t); await f.service.connectServiceAccount(input);
227
+ f.state.status = 404; f.state.response = { error: { message: "private provider diagnostics", details: [
228
+ { "@type": "type.googleapis.com/google.firebase.fcm.v1.FcmError", errorCode: "UNREGISTERED" }
229
+ ] } };
230
+ for (const target of [{ fid: "old-installation" }, { token: "old-token" }]) {
231
+ const before = f.state.requests.length;
232
+ await assert.rejects(f.service.invoke({ ...input, operation: "messages.send", input: { message: { ...target, data: { orderId: "42" } } } }), error => error.code === "connector_recipient_unregistered" && !error.message.includes("private provider"));
233
+ assert.equal(f.state.requests.length, before + 1);
234
+ assert.equal((await f.service.status(input)).status, "connected");
235
+ }
236
+ f.state.status = 400; f.state.response = { error: { details: [{ "@type": "type.googleapis.com/google.firebase.fcm.v1.FcmError", errorCode: "INVALID_ARGUMENT" }] } };
237
+ await assert.rejects(f.service.invoke({ ...input, operation: "messages.send", input: { message } }), { code: "connector_message_invalid" });
238
+ f.state.status = 404; f.state.response = { error: { status: "NOT_FOUND" } };
239
+ await assert.rejects(f.service.invoke({ ...input, operation: "messages.send", input: { message } }), { code: "connector_provider_failed" });
240
+ f.state.status = 200; f.state.response = undefined;
241
+ assert.equal((await f.service.invoke({ ...input, operation: "messages.send", input: { message } })).name, "projects/target-project/messages/fixture-receipt");
242
+ });
@@ -0,0 +1,66 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { mkdtemp, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+ import { createConnectionService } from "../../connectors-core/src/server/index.js";
7
+ import { createFileConnectionStore, createCredentialProtection } from "../../connectors-core/src/server/fileStorage.js";
8
+ import { firecrawlProvider } from "../src/server/firecrawl.js";
9
+
10
+ test("Firecrawl searches and imports bounded crawl pages without leaking the key or replaying paid work", async t => {
11
+ const directory = await mkdtemp(path.join(tmpdir(), "firecrawl-repair-"));
12
+ t.after(() => rm(directory, { recursive: true, force: true }));
13
+ const requests = []; let status = 200; let fail = false; let malformed = false; let deny = false; let key = "fixture-key";
14
+ const service = createConnectionService({
15
+ configuration: { schemaVersion: 1, registrations: {}, integrations: { firecrawl: { provider: "firecrawl", accountMode: "shared", scopes: [], authentication: { method: "api-key", secretRef: "env:FIRECRAWL_API_KEY" } } } },
16
+ providers: [firecrawlProvider], authorize: async owner => { if (deny) throw new Error("denied"); return owner; },
17
+ resolveReference: async () => key,
18
+ store: createFileConnectionStore({ directory, protection: createCredentialProtection({ keys: { current: new Uint8Array(32).fill(8) }, activeKeyId: "current" }) }),
19
+ fetchImpl: async (url, init) => {
20
+ const parsed = new URL(url); requests.push({ url: parsed, init });
21
+ assert.equal(parsed.origin, "https://api.firecrawl.dev");
22
+ assert.equal(new Headers(init.headers).get("authorization"), `Bearer ${key}`);
23
+ assert.equal(init.redirect, "error");
24
+ if (fail) throw new Error("unknown outcome");
25
+ if (status !== 200) return Response.json({ success: false }, { status });
26
+ if (malformed) return Response.json({ success: true });
27
+ const body = init.body ? JSON.parse(init.body) : {};
28
+ if (parsed.pathname.endsWith("credit-usage")) return Response.json({ success: true, data: { remainingCredits: 0 } });
29
+ if (parsed.pathname.endsWith("/search")) return Response.json({ success: true, data: { web: [{ url: "https://example.com", markdown: "Search content" }] } });
30
+ if (parsed.pathname.endsWith("/map")) return Response.json({ success: true, links: [{ url: "https://example.com/docs" }] });
31
+ if (parsed.pathname.endsWith("/scrape")) return Response.json({ success: true, data: typeof body.formats[0] === "object" ? { json: { title: "Docs" } } : { markdown: "# Docs" } });
32
+ if (parsed.pathname.endsWith("/errors")) return Response.json({ errors: [{ url: "https://example.com/missing", error: "404" }], robotsBlocked: [] });
33
+ if (init.method === "DELETE") return Response.json({ status: "cancelled" });
34
+ if (init.method === "POST") return Response.json({ success: true, id: "job-one" });
35
+ return Response.json({ status: "completed", total: 2, completed: 2, data: [{ markdown: parsed.search ? "Page two" : "Page one" }], next: parsed.search ? null : "https://api.firecrawl.dev/v2/crawl/job-one?skip=1" });
36
+ }
37
+ });
38
+ const owner = { context: { applicationId: "docs-app", subjectId: "workspace" }, integrationId: "firecrawl" };
39
+ const invoke = (operation, input) => service.invoke({ ...owner, operation, input });
40
+ await service.connectApiKey(owner);
41
+ assert.equal((await invoke("pages.scrape", { url: "https://example.com" })).data.markdown, "# Docs");
42
+ assert.equal((await invoke("pages.search", { query: "docs", includeContent: true })).data.web[0].markdown, "Search content");
43
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { query: "docs", limit: 5, sources: ["web"], scrapeOptions: { formats: ["markdown"] } });
44
+ assert.equal((await invoke("sites.map", { url: "https://example.com", limit: 20 })).links.length, 1);
45
+ assert.equal((await invoke("pages.extract", { url: "https://example.com", schema: { type: "object", properties: { title: { type: "string" } } } })).data.json.title, "Docs");
46
+ const job = await invoke("crawls.start", { url: "https://example.com", limit: 20 });
47
+ assert.equal(job.id, "job-one");
48
+ assert.equal(JSON.parse(requests.at(-1).init.body).allowExternalLinks, false);
49
+ const first = await invoke("crawls.get", { id: job.id });
50
+ const second = await invoke("crawls.get", { id: job.id, next: first.next });
51
+ assert.equal(second.data[0].markdown, "Page two"); assert.equal(second.next, null);
52
+ assert.equal((await invoke("crawls.errors", { id: job.id })).errors[0].error, "404");
53
+ assert.equal((await invoke("crawls.cancel", { id: job.id })).status, "cancelled");
54
+ const before = requests.length;
55
+ for (const next of ["https://evil.example/v2/crawl/job-one", "https://api.firecrawl.dev/v2/crawl/another", "https://key@api.firecrawl.dev/v2/crawl/job-one"]) await assert.rejects(invoke("crawls.get", { id: job.id, next }), { code: "connector_input_invalid" });
56
+ for (const input of [{ url: "https://example.com" }, { url: "file:///secret", limit: 1 }, { url: "https://user:pass@example.com", limit: 1 }, { url: "https://example.com", limit: 1001 }]) await assert.rejects(invoke("crawls.start", input), { code: "connector_input_invalid" });
57
+ deny = true; await assert.rejects(invoke("pages.search", { query: "docs" })); deny = false;
58
+ assert.equal(requests.length, before);
59
+ malformed = true; await assert.rejects(invoke("crawls.get", { id: job.id }), { code: "connector_response_invalid" }); malformed = false;
60
+ for (const [httpStatus, code] of [[403, "connector_permission_denied"], [429, "connector_rate_limited"], [500, "connector_provider_failed"]]) { status = httpStatus; await assert.rejects(invoke("crawls.start", { url: "https://example.com", limit: 1 }), { code }); }
61
+ status = 200; fail = true; const failedBefore = requests.length; await assert.rejects(invoke("crawls.start", { url: "https://example.com", limit: 1 })); assert.equal(requests.length, failedBefore + 1); fail = false;
62
+ key = "rotated-key"; await service.connectApiKey(owner);
63
+ await service.disconnect(owner); const disconnected = requests.length;
64
+ await assert.rejects(invoke("pages.search", { query: "docs" })); assert.equal(requests.length, disconnected);
65
+ await service.connectApiKey(owner); assert.equal((await invoke("credits.read")).data.remainingCredits, 0);
66
+ });
@@ -0,0 +1,218 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import path from "node:path";
6
+ import { createConnectionService } from "../../connectors-core/src/server/index.js";
7
+ import { createFileConnectionStore, createCredentialProtection } from "../../connectors-core/src/server/fileStorage.js";
8
+ import { parseIntegrationConfiguration } from "../../connectors-core/src/shared/configuration.js";
9
+ import { geminiEnterpriseProvider } from "../src/server/gemini-enterprise.js";
10
+
11
+ const scope = "https://www.googleapis.com/auth/cloud-platform";
12
+ const context = { applicationId: "app-one", subjectId: "owner-one" };
13
+ const args = { context, integrationId: "enterprise-search" };
14
+ const callback = "https://app.example.test/oauth/gemini-enterprise/callback";
15
+ const searchPage = { results: [{ id: "policy", document: { id: "policy", structData: { title: "Holiday policy" } } }], totalSize: 2, attributionToken: "search-reference", nextPageToken: "next+/=&" };
16
+
17
+ async function fixture(t, { location = "global", accountMode = "shared" } = {}) {
18
+ const directory = await mkdtemp(path.join(tmpdir(), "gemini-enterprise-connector-"));
19
+ t.after(() => rm(directory, { recursive: true, force: true }));
20
+ const configuration = { schemaVersion: 1, registrations: { google: { source: "own", clientId: "fixture-google-client",
21
+ clientSecretRef: "env:GEMINI_SECRET", callbackUrlRef: "env:GEMINI_CALLBACK" } }, integrations: {
22
+ "enterprise-search": { provider: "gemini-enterprise", accountMode, settings: { projectId: "my-gcp-project", location, engineId: "company_search-1" },
23
+ scopes: [scope], authentication: { method: "oauth2", registrationRef: "google" } }
24
+ } };
25
+ const engine = { name: `projects/123456789012/locations/${location}/collections/default_collection/engines/company_search-1`, displayName: "Company search" };
26
+ const state = { time: Date.now(), count: 0, value: undefined, status: 200, tokenPatch: {}, tokenStatus: 200, hang: false };
27
+ const requests = [];
28
+ const options = { configuration, providers: [{ ...geminiEnterpriseProvider, requestTimeoutMs: 60 }], authorize: async (owner) => owner,
29
+ now: () => state.time, resolveReference: async (ref) => {
30
+ if (ref === "env:GEMINI_SECRET") return "private-client-secret";
31
+ assert.equal(ref, "env:GEMINI_CALLBACK"); return callback;
32
+ },
33
+ store: createFileConnectionStore({ directory, protection: createCredentialProtection({ keys: { current: new Uint8Array(32).fill(8) }, activeKeyId: "current" }) }),
34
+ fetchImpl: async (address, init) => {
35
+ const url = new URL(String(address)); requests.push({ url, init });
36
+ const headers = new Headers(init.headers);
37
+ if (url.href === "https://oauth2.googleapis.com/token") {
38
+ assert.equal(init.method, "POST"); const body = new URLSearchParams(init.body);
39
+ assert.equal(body.get("client_id"), "fixture-google-client"); assert.equal(body.get("client_secret"), "private-client-secret");
40
+ if (body.get("grant_type") === "authorization_code") {
41
+ assert.equal(body.get("redirect_uri"), callback); assert.equal(body.get("code"), "private-code"); assert.ok(body.get("code_verifier"));
42
+ } else {
43
+ assert.equal(body.get("grant_type"), "refresh_token"); assert.equal(body.get("refresh_token"), `private-refresh-${state.count}`);
44
+ }
45
+ state.count++;
46
+ return Response.json(state.tokenStatus === 200 ? { access_token: `private-access-${state.count}`, token_type: "Bearer", expires_in: 120,
47
+ refresh_token: `private-refresh-${state.count}`, scope, ...state.tokenPatch } : { error: "invalid_grant", message: "private-provider-error" }, { status: state.tokenStatus });
48
+ }
49
+ assert.equal(url.origin, `https://${location === "global" ? "" : `${location}-`}discoveryengine.googleapis.com`);
50
+ const expected = `/v1/projects/my-gcp-project/locations/${location}/collections/default_collection/engines/company_search-1`;
51
+ assert.equal(url.pathname, init.method === "POST" ? `${expected}/servingConfigs/default_serving_config:search` : expected);
52
+ assert.equal(init.redirect, "error"); assert.equal(headers.get("authorization"), `Bearer private-access-${state.count}`);
53
+ assert.equal(init.credentials, "omit"); assert.equal(url.search, "");
54
+ if (init.method === "POST") assert.ok(headers.get("content-type").includes("application/json")); else assert.equal(init.method, "GET");
55
+ if (state.hang) return new Promise((_, reject) => {
56
+ const timer = setTimeout(() => reject(new Error("Missing abort")), 1000);
57
+ const abort = () => { clearTimeout(timer); reject(init.signal.reason); };
58
+ if (init.signal.aborted) abort(); else init.signal.addEventListener("abort", abort, { once: true });
59
+ });
60
+ if (state.status === 204) return new Response(null, { status: 204 });
61
+ if (typeof state.value === "string") return new Response(state.value, { status: state.status });
62
+ return Response.json(state.value === undefined ? init.method === "POST" ? searchPage : engine : state.value, { status: state.status });
63
+ }
64
+ };
65
+ const service = createConnectionService(options);
66
+ const start = async () => {
67
+ const { authorizationUrl } = await service.beginAuthorization(args); const url = new URL(authorizationUrl);
68
+ const returned = new URL(callback); returned.searchParams.set("state", url.searchParams.get("state")); returned.searchParams.set("code", "private-code");
69
+ return { url, callbackUrl: returned.href };
70
+ };
71
+ const connect = async () => service.completeAuthorization({ ...args, callbackUrl: (await start()).callbackUrl });
72
+ return { directory, options, service, requests, state, engine, start, connect };
73
+ }
74
+
75
+ test("Gemini Enterprise uses Google PKCE consent and the configured regional engine, with encrypted restartable storage", async (t) => {
76
+ for (const location of ["global", "us", "eu"]) {
77
+ const f = await fixture(t, { location, accountMode: location === "eu" ? "assistant" : "shared" }); const start = await f.start();
78
+ assert.equal(start.url.origin + start.url.pathname, "https://accounts.google.com/o/oauth2/v2/auth");
79
+ assert.equal(start.url.searchParams.get("scope"), scope); assert.equal(start.url.searchParams.get("access_type"), "offline");
80
+ assert.equal(start.url.searchParams.get("prompt"), "consent"); assert.equal(start.url.searchParams.get("code_challenge_method"), "S256");
81
+ assert.equal(start.url.searchParams.has("client_secret"), false);
82
+ await f.service.completeAuthorization({ ...args, callbackUrl: start.callbackUrl });
83
+ const restarted = createConnectionService(f.options);
84
+ assert.equal((await restarted.status(args)).status, "connected");
85
+ assert.deepEqual(await restarted.invoke({ ...args, operation: "engine.get" }), f.engine);
86
+ for (const file of await readdir(f.directory)) {
87
+ const value = await readFile(path.join(f.directory, file), "utf8");
88
+ for (const secret of ["private-code", "private-client-secret", "private-access-1", "private-refresh-1"]) assert.equal(value.includes(secret), false);
89
+ }
90
+ }
91
+ });
92
+
93
+ test("Gemini Enterprise search sends bounded JSON pages and preserves results, opaque cursors and redirect data", async (t) => {
94
+ const f = await fixture(t); await f.connect();
95
+ assert.deepEqual(await f.service.invoke({ ...args, operation: "search", input: { query: "holiday policy & travel" } }), searchPage);
96
+ assert.deepEqual(JSON.parse(f.requests.at(-1).init.body), { query: "holiday policy & travel", pageSize: 10 });
97
+ const input = { query: "holiday policy & travel", pageSize: 25, pageToken: "next+/=&" };
98
+ await f.service.invoke({ ...args, operation: "search", input }); assert.deepEqual(JSON.parse(f.requests.at(-1).init.body), input);
99
+ for (const value of [{ attributionToken: "empty" }, { results: [], totalSize: 0, attributionToken: "empty", nextPageToken: "" },
100
+ { attributionToken: "redirect", redirectUri: "https://elsewhere.example.test/result" }]) {
101
+ f.state.value = value; assert.deepEqual(await f.service.invoke({ ...args, operation: "search", input: { query: "empty" } }), value);
102
+ }
103
+ assert.equal(f.requests.length, 7);
104
+ });
105
+
106
+ test("Gemini Enterprise rejects caller target and identity overrides, invalid page input and unauthorized scopes before transport", async (t) => {
107
+ const f = await fixture(t); await f.connect(); const count = f.requests.length;
108
+ for (const input of [{}, { query: "" }, { query: "x".repeat(4097) }, { query: "x", pageSize: 0 }, { query: "x", pageSize: 26 },
109
+ { query: "x", pageSize: 1.5 }, { query: "x", pageToken: "" }, { query: "x", pageToken: "a".repeat(16001) },
110
+ { query: "x", projectId: "other-project" }, { query: "x", userInfo: { userId: "someone" } }, { query: "x", servingConfig: "other" }]) {
111
+ await assert.rejects(f.service.invoke({ ...args, operation: "search", input }));
112
+ }
113
+ await assert.rejects(f.service.invoke({ ...args, operation: "engine.get", input: { engineId: "other" } }));
114
+ const denied = createConnectionService({ ...f.options, authorize: async () => { throw new Error("Denied by host"); } });
115
+ await assert.rejects(denied.invoke({ ...args, operation: "engine.get" }), /Denied by host/u);
116
+ assert.equal(f.requests.length, count);
117
+ const narrowed = await fixture(t); narrowed.state.tokenPatch.scope = "https://www.googleapis.com/auth/other";
118
+ await assert.rejects(narrowed.connect(), { code: "connector_scope_missing" }); assert.equal(narrowed.requests.length, 1);
119
+ });
120
+
121
+ test("Gemini Enterprise binds stored grants and pending consent to their application, owner and settings", async (t) => {
122
+ const f = await fixture(t); await f.connect(); const count = f.requests.length;
123
+ for (const owner of [{ ...context, applicationId: "other" }, { ...context, subjectId: "other" }]) {
124
+ await assert.rejects(f.service.invoke({ ...args, context: owner, operation: "engine.get" }), { code: "connector_reconnect_required" });
125
+ }
126
+ for (const [key, value] of [["projectId", "another-project"], ["location", "eu"], ["engineId", "another-engine"]]) {
127
+ const configuration = structuredClone(f.options.configuration); configuration.integrations["enterprise-search"].settings[key] = value;
128
+ const changed = createConnectionService({ ...f.options, configuration });
129
+ assert.equal((await changed.status(args)).status, "reconnect-required");
130
+ await assert.rejects(changed.invoke({ ...args, operation: "engine.get" }), { code: "connector_reconnect_required" });
131
+ const start = await f.start(); await assert.rejects(changed.completeAuthorization({ ...args, callbackUrl: start.callbackUrl }), { code: "connector_attempt_invalid" });
132
+ }
133
+ assert.equal(f.requests.length, count);
134
+ });
135
+
136
+ test("Gemini Enterprise refreshes rotated grants, keeps Google scopes bounded and reports missing or revoked refresh access", async (t) => {
137
+ const f = await fixture(t); await f.connect(); f.state.time += 121_000;
138
+ f.state.tokenPatch.scope = `${scope} https://www.googleapis.com/auth/drive`;
139
+ await f.service.invoke({ ...args, operation: "engine.get" });
140
+ assert.deepEqual((await f.service.status(args)).grantedScopes, [scope]);
141
+ f.state.time += 121_000; f.state.tokenStatus = 400;
142
+ await assert.rejects(f.service.invoke({ ...args, operation: "engine.get" }), { code: "connector_reconnect_required" });
143
+ const short = await fixture(t); short.state.tokenPatch.refresh_token = undefined; await short.connect(); short.state.time += 121_000;
144
+ await assert.rejects(short.service.invoke({ ...args, operation: "engine.get" }), { code: "connector_reconnect_required" }); assert.equal(short.requests.length, 2);
145
+ });
146
+
147
+ test("Gemini Enterprise consumes denied, cancelled and replayed attempts without destroying earlier access", async (t) => {
148
+ const f = await fixture(t); await f.connect();
149
+ const denied = await f.start(); const url = new URL(denied.callbackUrl); url.searchParams.delete("code"); url.searchParams.set("error", "access_denied");
150
+ await assert.rejects(f.service.completeAuthorization({ ...args, callbackUrl: url.href }), { code: "connector_consent_denied" });
151
+ await assert.rejects(f.service.completeAuthorization({ ...args, callbackUrl: denied.callbackUrl }), { code: "connector_attempt_invalid" });
152
+ const cancelled = await f.start(); await f.service.cancelAuthorization({ ...args, state: cancelled.url.searchParams.get("state") });
153
+ await assert.rejects(f.service.completeAuthorization({ ...args, callbackUrl: cancelled.callbackUrl }), { code: "connector_attempt_invalid" });
154
+ assert.equal((await f.service.status(args)).status, "connected"); assert.equal(f.requests.length, 2);
155
+ });
156
+
157
+ test("Gemini Enterprise rejects malformed metadata and result envelopes without treating them as verified access", async (t) => {
158
+ for (const value of [null, {}, [], { error: { message: "private" } }, { name: "engine", displayName: "Company" }, { name: "projects/123/locations/global/collections/default_collection/engines/engine" }]) {
159
+ const f = await fixture(t); f.state.value = value;
160
+ await assert.rejects(f.connect(), { code: "connector_response_invalid" }); assert.equal((await f.service.status(args)).status, "disconnected");
161
+ }
162
+ const f = await fixture(t); await f.connect();
163
+ for (const value of [null, {}, [], { attributionToken: "" }, { attributionToken: "a", results: null }, { attributionToken: "a", results: [{}] },
164
+ { attributionToken: "a", results: [{ id: "a", document: null }] }, { attributionToken: "a", totalSize: -1 }, { attributionToken: "a", nextPageToken: 1 },
165
+ { attributionToken: "a", redirectUri: {} }, { attributionToken: "a", error: {} }, { ...searchPage, results: Array(26).fill(searchPage.results[0]) }, "<html>private-error</html>"]) {
166
+ f.state.value = value; await assert.rejects(f.service.invoke({ ...args, operation: "search", input: { query: "query" } }), { code: "connector_response_invalid" });
167
+ }
168
+ });
169
+
170
+ test("Gemini Enterprise sanitizes API failures, aborts pending requests and disconnects locally", async (t) => {
171
+ const f = await fixture(t); await f.connect();
172
+ for (const [status, code] of [[403, "connector_permission_denied"], [429, "connector_rate_limited"], [500, "connector_provider_failed"]]) {
173
+ f.state.status = status; f.state.value = { error: { message: "private-provider-error" } };
174
+ await assert.rejects(f.service.invoke({ ...args, operation: "search", input: { query: "query" } }), (error) => error.code === code && !error.message.includes("private"));
175
+ }
176
+ f.state.hang = true; const controller = new AbortController();
177
+ const pending = f.service.invoke({ ...args, operation: "engine.get", signal: controller.signal }); setTimeout(() => controller.abort(), 10);
178
+ await assert.rejects(pending, { code: "connector_cancelled" });
179
+ await assert.rejects(f.service.invoke({ ...args, operation: "engine.get" }), { code: "connector_provider_timeout" });
180
+ f.state.hang = false; f.state.status = 401;
181
+ await assert.rejects(f.service.invoke({ ...args, operation: "engine.get" }), { code: "connector_reconnect_required" });
182
+ await f.service.disconnect(args); assert.equal((await f.service.status(args)).status, "disconnected");
183
+ });
184
+
185
+ test("Gemini Enterprise guide and CLI share identifier, location, permission and reference validation", async () => {
186
+ const guide = await readFile(new URL("../docs/gemini-enterprise.md", import.meta.url), "utf8");
187
+ const config = JSON.parse([...guide.matchAll(/```json\n([\s\S]*?)\n```/gu)][0][1]);
188
+ const parse = (value) => parseIntegrationConfiguration(JSON.stringify(value), { providers: [geminiEnterpriseProvider] });
189
+ assert.deepEqual(parse(config), config);
190
+ for (const [key, values] of Object.entries({ projectId: ["", "123456789012", "My-Project", "my-project/other", "my-project ", "a".repeat(31)],
191
+ engineId: ["", "-bad", "UPPER", "engine/../other", "a".repeat(64)], location: ["", "https://attacker.test", "ca", "US"] })) {
192
+ for (const value of values) {
193
+ const invalid = structuredClone(config); invalid.integrations["enterprise-search"].settings[key] = value;
194
+ assert.throws(() => parse(invalid), (error) => Boolean(error.fieldErrors[`integrations.enterprise-search.settings.${key}`]));
195
+ }
196
+ }
197
+ for (const [key, value] of [["clientSecretRef", "private-secret"], ["callbackUrlRef", callback], ["tokenEndpointAuthMethod", "none"]]) {
198
+ const invalid = structuredClone(config); invalid.registrations["google-search"][key] = value;
199
+ assert.throws(() => parse(invalid), (error) => Boolean(error.fieldErrors[`registrations.google-search.${key}`]));
200
+ }
201
+ const unsupported = structuredClone(config); unsupported.integrations["enterprise-search"].accountMode = "per-user";
202
+ assert.throws(() => parse(unsupported));
203
+ });
204
+
205
+
206
+ test("Gemini Enterprise returns source snippets and cited provider summaries only when explicitly requested", async t => {
207
+ const f = await fixture(t); await f.connect();
208
+ const value = { ...searchPage, results: [{ id: "policy", document: { id: "policy", derivedStructData: { title: "Holiday policy", link: "https://company.example/policy", snippets: [{ snippet: "Request leave a week ahead.", snippet_status: "SUCCESS" }] } } }], summary: { summaryText: "Request leave a week ahead. [1]" } };
209
+ f.state.value = value;
210
+ const contentSearchSpec = { snippetSpec: { returnSnippet: true }, summarySpec: { summaryResultCount: 3 } };
211
+ const result = await f.service.invoke({ ...args, operation: "search", input: { query: "How do I request leave?", contentSearchSpec } });
212
+ assert.equal(result.summary.summaryText, value.summary.summaryText);
213
+ assert.equal(result.results[0].document.derivedStructData.snippets[0].snippet, "Request leave a week ahead.");
214
+ assert.deepEqual(JSON.parse(f.requests.at(-1).init.body).contentSearchSpec.summarySpec, { summaryResultCount: 3, includeCitations: true, ignoreAdversarialQuery: true, ignoreNonSummarySeekingQuery: true, ignoreLowRelevantContent: true });
215
+ f.state.value = { attributionToken: "none", summary: { summarySkippedReasons: ["NO_RELEVANT_CONTENT"] } };
216
+ assert.deepEqual((await f.service.invoke({ ...args, operation: "search", input: { query: "unavailable", contentSearchSpec } })).summary.summarySkippedReasons, ["NO_RELEVANT_CONTENT"]);
217
+ await assert.rejects(f.service.invoke({ ...args, operation: "search", input: { query: "q", contentSearchSpec: { summarySpec: { summaryResultCount: 11 } } } }));
218
+ });