@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,136 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { readFile } from "node:fs/promises";
4
+ import { aiCatalogue, aiDefinition, DEFAULT_AI_MODEL, getAiModel, listAiModels } from "../src/shared/ai.js";
5
+ import { createAiConnectionResolver } from "../src/server/ai.js";
6
+ import { createEnvironmentReferenceResolver } from "../../connectors-core/src/server/environmentReferences.js";
7
+ import { parseIntegrationConfiguration, validateIntegrationConfiguration } from "../../connectors-core/src/shared/configuration.js";
8
+
9
+ const configuration = (patch = {}) => ({ schemaVersion: 1, registrations: {}, integrations: {
10
+ suggestions: { provider: "ai", accountMode: "shared", scopes: [], authentication: { method: "none" }, ...patch }
11
+ } });
12
+ const actor = { applicationId: "dogandgroom", subjectId: "customer-one" };
13
+ const request = { context: actor, integrationId: "suggestions" };
14
+ const authorize = async (context) => context?.applicationId === actor.applicationId ? context : null;
15
+ const validate = (config) => validateIntegrationConfiguration(config, { providers: [aiDefinition] });
16
+
17
+ test("AI preserves the upstream snapshot, exposes every provider and separates deprecated models", () => {
18
+ assert.equal(aiCatalogue.extractedAt, "2026-09-10");
19
+ assert.equal(aiCatalogue.providers.length, 213);
20
+ assert.equal(listAiModels({ includeDeprecated: true }).length, 7614);
21
+ assert.match(aiCatalogue.sourceSha256, /^[a-f0-9]{64}$/u);
22
+ assert.equal(getAiModel("opencode/grok-code").status, "deprecated");
23
+ assert.equal(listAiModels().some((model) => model.key === "opencode/grok-code"), false);
24
+ assert.equal(aiCatalogue.providers.find((provider) => provider.id === "amazon-bedrock").connection, "framework");
25
+ });
26
+
27
+ test("AI lists every active Zen public model first and uses Big Pickle as the explicit default", () => {
28
+ assert.equal(listAiModels()[0].key, DEFAULT_AI_MODEL);
29
+ assert.deepEqual(listAiModels({ access: "free-no-setup" }).map((model) => model.id).sort(), [
30
+ "big-pickle", "ling-3.0-flash-fin-free", "mimo-v2.5-free", "muse-spark-1.2-contributor-free",
31
+ "muse-spark-1.3-contributor-free", "nemotron-3-ultra-free", "nemotron-3.5-lightning-free"
32
+ ].sort());
33
+ assert.equal(getAiModel("zai/glm-4.7-flash").access, "free-with-connection");
34
+ assert.equal(getAiModel("zai/glm-4.7").access, "paid");
35
+ for (const model of listAiModels({ access: "free-no-setup" })) {
36
+ assert.equal(model.providerId, "opencode"); assert.equal(model.cost.input, 0); assert.equal(model.cost.output, 0);
37
+ assert.ok(Object.values(model.cost).every((value) => value === 0));
38
+ }
39
+ });
40
+
41
+ test("AI JSON defaults agree with the form and preserve explicit free model choices", () => {
42
+ const config = validate(configuration());
43
+ assert.deepEqual(config.integrations.suggestions.settings, { model: "opencode/big-pickle" });
44
+ assert.deepEqual(config.integrations.suggestions.authentication, { method: "none" });
45
+ config.integrations.suggestions.settings.model = "opencode/mimo-v2.5-free";
46
+ assert.deepEqual(parseIntegrationConfiguration(JSON.stringify(config), { providers: [aiDefinition] }), config);
47
+ assert.equal(JSON.stringify(config).includes("public"), false);
48
+ });
49
+
50
+ test("AI refuses missing models, deprecated models, coding subscriptions and unsupported cloud setup", () => {
51
+ for (const model of ["opencode/removed", "opencode/grok-code", "zai-coding-plan/glm-4.7", "amazon-bedrock/anthropic.claude-v2", "constructor"]) {
52
+ assert.throws(() => validate(configuration({ settings: { model } })), { code: "integration_configuration_invalid" });
53
+ }
54
+ for (const model of ["zai/glm-4.7-flash", "zai/glm-4.7", "openai/gpt-5.4"]) {
55
+ assert.throws(() => validate(configuration({ settings: { model } })), (error) => {
56
+ assert.ok(error.fieldErrors["integrations.suggestions.authentication.method"]); return true;
57
+ });
58
+ }
59
+ for (const authentication of [{ method: "none", secretRef: "env:KEY" }, { method: "api-key", secretRef: "raw-key" },
60
+ { method: "oauth2", registrationRef: "codex" }]) {
61
+ assert.throws(() => validate(configuration({ authentication })), { code: "integration_configuration_invalid" });
62
+ }
63
+ });
64
+
65
+ test("AI resolves no-account SDK parameters without environment, network or an OpenCode runtime", async (t) => {
66
+ t.mock.method(globalThis, "fetch", () => { throw new Error("Network is forbidden in this resolver."); });
67
+ const resolver = createAiConnectionResolver({ configuration: configuration(), authorize,
68
+ resolveReference: () => { throw new Error("Free mode must not resolve credentials."); } });
69
+ assert.deepEqual(await resolver.resolve(request), { providerId: "opencode", model: "big-pickle",
70
+ sdkPackage: "@ai-sdk/openai-compatible", baseURL: "https://opencode.ai/zen/v1", apiKey: "public", access: "free-no-setup" });
71
+ assert.equal(globalThis.fetch.mock.callCount(), 0);
72
+ assert.equal(Object.hasOwn(resolver, "invoke"), false);
73
+ });
74
+
75
+ test("AI preserves model-specific SDK protocols including Zen Responses models", async () => {
76
+ const resolver = createAiConnectionResolver({ configuration: configuration({ settings: { model: "opencode/muse-spark-1.3-contributor-free" } }), authorize });
77
+ const result = await resolver.resolve(request);
78
+ assert.equal(result.sdkPackage, "@ai-sdk/openai"); assert.equal(result.model, "muse-spark-1.3-contributor-free");
79
+ assert.equal(result.baseURL, "https://opencode.ai/zen/v1");
80
+ });
81
+
82
+ test("AI shares the administrator's Env key only after application authorization and observes rotation", async () => {
83
+ const env = { APP_AI_API_KEY: "fixture-key-one" }; const decisions = [];
84
+ const resolver = createAiConnectionResolver({ configuration: configuration({ settings: { model: "zai/glm-4.7-flash" },
85
+ authentication: { method: "api-key", secretRef: "env:APP_AI_API_KEY" } }),
86
+ authorize: async (context, operation) => { decisions.push(operation); return authorize(context); },
87
+ resolveReference: createEnvironmentReferenceResolver(env) });
88
+ assert.equal((await resolver.resolve(request)).apiKey, "fixture-key-one"); env.APP_AI_API_KEY = "fixture-key-two";
89
+ const connection = await resolver.resolve({ ...request, context: { ...actor, subjectId: "customer-two" } });
90
+ assert.equal(connection.apiKey, "fixture-key-two"); assert.equal(connection.baseURL, "https://api.z.ai/api/paas/v4");
91
+ assert.equal(connection.model, "glm-4.7-flash");
92
+ assert.deepEqual(decisions[0], { integrationId: "suggestions", operation: "ai.resolve", accountMode: "shared" });
93
+ delete env.APP_AI_API_KEY;
94
+ await assert.rejects(resolver.resolve(request), { code: "connector_binding_missing" });
95
+ });
96
+
97
+ test("AI keeps individual account resolution scoped to the authorized app user and provider", async () => {
98
+ const keys = new Map([["customer-one", "user-one-key"], ["customer-two", "user-two-key"]]);
99
+ const config = configuration({ accountMode: "per-user", settings: { model: "zai/glm-4.7-flash" },
100
+ authentication: { method: "api-key", secretRef: "account:ai-key" } });
101
+ const resolver = createAiConnectionResolver({ configuration: config, authorize, resolveReference: async (ref, owner, slot) => {
102
+ assert.equal(ref, "account:ai-key"); assert.equal(owner.applicationId, actor.applicationId);
103
+ assert.deepEqual(slot, { integrationId: "suggestions", accountMode: "per-user", providerId: "zai" });
104
+ return keys.get(owner.subjectId);
105
+ } });
106
+ assert.equal((await resolver.resolve(request)).apiKey, "user-one-key");
107
+ assert.equal((await resolver.resolve({ ...request, context: { ...actor, subjectId: "customer-two" } })).apiKey, "user-two-key");
108
+ keys.delete("customer-one");
109
+ await assert.rejects(resolver.resolve(request), { code: "connector_binding_missing" });
110
+ assert.equal((await resolver.resolve({ ...request, context: { ...actor, subjectId: "customer-two" } })).apiKey, "user-two-key");
111
+ config.integrations.suggestions.authentication.secretRef = "env:SHARED_KEY";
112
+ await assert.rejects(createAiConnectionResolver({ configuration: config, authorize }).resolve(request), { code: "connector_binding_invalid" });
113
+ });
114
+
115
+ test("AI denies unauthorized subjects before looking up credentials and redacts binding errors", async () => {
116
+ let lookups = 0;
117
+ const config = configuration({ settings: { model: "openai/gpt-5.4" }, authentication: { method: "api-key", secretRef: "env:KEY" } });
118
+ const resolver = createAiConnectionResolver({ configuration: config, authorize, resolveReference: () => {
119
+ lookups++; throw new Error("private-provider-token");
120
+ } });
121
+ for (const context of [null, {}, { ...actor, applicationId: "another-app" }]) {
122
+ await assert.rejects(resolver.resolve({ ...request, context }), { code: "connector_access_denied" });
123
+ }
124
+ assert.equal(lookups, 0);
125
+ await assert.rejects(resolver.resolve({ ...request, integrationId: "constructor" }), { code: "connector_not_found" });
126
+ await assert.rejects(resolver.resolve(request), (error) => error.code === "connector_binding_missing" && !String(error).includes("private-provider-token"));
127
+ });
128
+
129
+ test("AI can hand parameters to an app-owned SDK without making that SDK a dependency", async () => {
130
+ const result = await createAiConnectionResolver({ configuration: configuration(), authorize }).resolve(request);
131
+ // A controlled SDK factory fixture; no generated application or inference call.
132
+ const createProvider = (options) => ({ model: (id) => ({ endpoint: options.baseURL, id }) });
133
+ assert.deepEqual(createProvider(result).model(result.model), { endpoint: "https://opencode.ai/zen/v1", id: "big-pickle" });
134
+ const pkg = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
135
+ assert.equal(Object.keys(pkg.dependencies).some((name) => name.startsWith("@ai-sdk/") || name.includes("opencode")), false);
136
+ });
@@ -0,0 +1,125 @@
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 { airtableProvider } from "../src/server/airtable.js";
9
+
10
+ const context = { applicationId: "records-app", subjectId: "workspace" };
11
+ const baseId = "appFixture";
12
+ const tableId = "tblFixture";
13
+ const recordId = "recFixture";
14
+
15
+ test("Airtable discovers schema, changes tables/fields and completes a paged record lifecycle", async t => {
16
+ const directory = await mkdtemp(path.join(tmpdir(), "airtable-repair-"));
17
+ t.after(() => rm(directory, { recursive: true, force: true }));
18
+ const requests = [];
19
+ let fail = false;
20
+ let deny = false;
21
+ let status = 200;
22
+ let malformed = false;
23
+ let token = "fixture-token";
24
+ const service = createConnectionService({
25
+ configuration: { schemaVersion: 1, registrations: {}, integrations: { data: {
26
+ provider: "airtable", accountMode: "shared", scopes: [],
27
+ authentication: { method: "api-key", secretRef: "env:AIRTABLE_TOKEN" }
28
+ } } }, providers: [airtableProvider], authorize: async owner => {
29
+ if (deny) throw new Error("forbidden"); return owner;
30
+ }, resolveReference: async () => token,
31
+ store: createFileConnectionStore({ directory, protection: createCredentialProtection({
32
+ keys: { current: new Uint8Array(32).fill(9) }, activeKeyId: "current"
33
+ }) }), fetchImpl: async (url, init) => {
34
+ requests.push({ url: String(url), init });
35
+ if (fail) throw new Error("uncertain write");
36
+ if (status !== 200) return Response.json({ error: { type: "fixture" } }, { status });
37
+ if (malformed) return Response.json({});
38
+ const pathname = new URL(url).pathname;
39
+ if (pathname === "/v0/meta/bases") return Response.json(init.method === "POST" ? { id: baseId, tables: [{ id: tableId }] } : { bases: [{ id: baseId }] });
40
+ if (pathname.startsWith("/v0/meta/")) return Response.json(init.method === "GET" ?
41
+ { tables: [{ id: tableId, name: "Tasks", fields: [{ id: "fldName", type: "singleLineText", name: "Name" }] }] } :
42
+ { id: pathname.includes("/fields") ? "fldName" : tableId, name: "Tasks" });
43
+ if (pathname.endsWith("/listRecords")) return Response.json({ records: [{ id: recordId, fields: { Name: "Task" } }], offset: "opaque+/cursor" });
44
+ if (init.method === "DELETE") return Response.json({ id: recordId, deleted: true });
45
+ return Response.json({ id: recordId, fields: JSON.parse(init.body).fields });
46
+ }
47
+ });
48
+ const invoke = (operation, input) => service.invoke({ context, integrationId: "data", operation, input });
49
+ await service.connectApiKey({ context, integrationId: "data" });
50
+ assert.equal(requests.length, 1);
51
+ const newBase = await invoke("bases.create", { name: "Tasks", workspaceId: "wspFixture",
52
+ tables: [{ name: "Tasks", fields: [{ name: "Name", type: "singleLineText" }] }] });
53
+ assert.equal(newBase.id, baseId);
54
+ assert.equal(requests.at(-1).init.method, "POST");
55
+ assert.equal(JSON.parse(requests.at(-1).init.body).workspaceId, "wspFixture");
56
+ const tables = await invoke("tables.list", { baseId });
57
+ assert.equal(tables.tables[0].fields[0].type, "singleLineText");
58
+ await invoke("tables.create", { baseId, name: "Tasks", fields: [{ name: "Name", type: "singleLineText" }] });
59
+ assert.equal(requests.at(-1).init.method, "POST");
60
+ assert.equal(new URL(requests.at(-1).url).pathname, `/v0/meta/bases/${baseId}/tables`);
61
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body).fields, [{ name: "Name", type: "singleLineText" }]);
62
+ await invoke("tables.update", { baseId, tableId, name: "Renamed" });
63
+ assert.equal(requests.at(-1).init.method, "PATCH");
64
+ await invoke("fields.create", { baseId, tableId, field: { name: "Notes", type: "multilineText" } });
65
+ assert.equal(new URL(requests.at(-1).url).pathname, `/v0/meta/bases/${baseId}/tables/${tableId}/fields`);
66
+ await invoke("fields.update", { baseId, tableId, fieldId: "fldName", name: "Title" });
67
+ assert.equal(requests.at(-1).init.method, "PATCH");
68
+ const records = await invoke("records.list", { baseId, tableId, filterByFormula: '{Name}="a & b"' });
69
+ await invoke("records.list", { baseId, tableId, offset: records.offset });
70
+ assert.equal(JSON.parse(requests.at(-1).init.body).offset, "opaque+/cursor");
71
+ const created = await invoke("records.create", { baseId, tableId: "Tasks / urgent", fields: { Name: "Task", Done: false } });
72
+ assert.ok(requests.at(-1).url.endsWith("Tasks%20%2F%20urgent"));
73
+ assert.deepEqual(created.fields, { Name: "Task", Done: false });
74
+ assert.equal(JSON.parse(requests.at(-1).init.body).typecast, false);
75
+ await invoke("records.update", { baseId, tableId, recordId: created.id, fields: { Done: true } });
76
+ assert.equal(requests.at(-1).init.method, "PATCH");
77
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body).fields, { Done: true });
78
+ await invoke("records.delete", { baseId, tableId, recordId });
79
+ assert.equal(requests.at(-1).init.method, "DELETE");
80
+ assert.equal(new Headers(requests.at(-1).init.headers).get("authorization"), "Bearer fixture-token");
81
+ const before = requests.length;
82
+ for (const input of [{ baseId: "../keys", tableId, fields: { Name: "bad" } },
83
+ { baseId, tableId: "..", fields: { Name: "bad" } }, { baseId, tableId, fields: {} },
84
+ { baseId, tableId, fields: [] }]) await assert.rejects(invoke("records.create", input));
85
+ await assert.rejects(invoke("records.list", { baseId, tableId, pageSize: 101 }));
86
+ await assert.rejects(invoke("bases.create", { name: "Empty", workspaceId: "wspFixture", tables: [] }));
87
+ await assert.rejects(invoke("tables.create", { baseId, name: "Duplicates", fields: [
88
+ { name: "Name", type: "singleLineText" }, { name: "name", type: "singleLineText" }
89
+ ] }));
90
+ deny = true;
91
+ await assert.rejects(invoke("records.delete", { baseId, tableId, recordId }));
92
+ deny = false;
93
+ await assert.rejects(service.invoke({ context: { ...context, applicationId: "other" }, integrationId: "data",
94
+ operation: "records.delete", input: { baseId, tableId, recordId } }));
95
+ assert.equal(requests.length, before);
96
+ for (const errorStatus of [403, 422, 429]) {
97
+ status = errorStatus;
98
+ await assert.rejects(invoke("records.create", { baseId, tableId, fields: { Name: "Task" } }));
99
+ }
100
+ status = 200;
101
+ fail = true;
102
+ const beforeFailure = requests.length;
103
+ await assert.rejects(invoke("records.create", { baseId, tableId, fields: { Name: "Task" } }));
104
+ assert.equal(requests.length, beforeFailure + 1);
105
+ fail = false;
106
+ malformed = true;
107
+ await assert.rejects(invoke("tables.list", { baseId }), { code: "connector_response_invalid" });
108
+ malformed = false;
109
+ token = "rotated-fixture-token";
110
+ await invoke("records.list", { baseId, tableId });
111
+ assert.equal(new Headers(requests.at(-1).init.headers).get("authorization"), "Bearer rotated-fixture-token");
112
+ status = 401;
113
+ await assert.rejects(invoke("records.list", { baseId, tableId }), { code: "connector_reconnect_required" });
114
+ assert.equal((await service.status({ context, integrationId: "data" })).status, "reconnect-required");
115
+ status = 200;
116
+ await service.connectApiKey({ context, integrationId: "data" });
117
+ assert.equal((await service.status({ context, integrationId: "data" })).status, "connected");
118
+ await service.disconnect({ context, integrationId: "data" });
119
+ const afterDisconnect = requests.length;
120
+ assert.equal((await service.status({ context, integrationId: "data" })).status, "disconnected");
121
+ await assert.rejects(invoke("records.list", { baseId, tableId }));
122
+ assert.equal(requests.length, afterDisconnect);
123
+ await service.connectApiKey({ context, integrationId: "data" });
124
+ assert.equal((await service.status({ context, integrationId: "data" })).status, "connected");
125
+ });
@@ -0,0 +1,207 @@
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 path from "node:path";
5
+ import { tmpdir } from "node:os";
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 { algoliaProvider } from "../src/server/algolia.js";
10
+
11
+ const context = { applicationId: "app-one", subjectId: "team-one" };
12
+ const input = { context, integrationId: "search" };
13
+ const indices = { items: [{ name: "products", entries: 20 }], nbPages: 2 };
14
+ const hits = { hits: [{ objectID: "product-1", name: "Phone" }], nbHits: 21, nbPages: 2, page: 0, hitsPerPage: 20 };
15
+
16
+ async function fixture(t) {
17
+ const directory = await mkdtemp(path.join(tmpdir(), "algolia-connector-"));
18
+ t.after(() => rm(directory, { recursive: true, force: true }));
19
+ const protection = createCredentialProtection({ keys: { current: new Uint8Array(32).fill(7) }, activeKeyId: "current" });
20
+ const requests = [];
21
+ const references = [];
22
+ const state = { key: "backend-fixture-secret", status: 200, response: indices };
23
+ const options = {
24
+ configuration: { schemaVersion: 1, integrations: {
25
+ search: { provider: "algolia", accountMode: "shared", scopes: [],
26
+ authentication: { method: "api-key", secretRef: "env:ALGOLIA_BACKEND_KEY" },
27
+ settings: { applicationId: "APP123ABC", publicApiKeyRef: "env:ALGOLIA_PUBLIC_KEY" }, extensions: { keep: true } }
28
+ }, registrations: {}, extensions: { fromCli: true } },
29
+ providers: [algoliaProvider], authorize: async (owner) => owner,
30
+ resolveReference: async (ref) => {
31
+ references.push(ref);
32
+ assert.equal(ref, "env:ALGOLIA_BACKEND_KEY");
33
+ return state.key;
34
+ },
35
+ store: createFileConnectionStore({ directory, protection }),
36
+ fetchImpl: async (address, init) => {
37
+ requests.push({ url: new URL(String(address)), init, headers: new Headers(init.headers) });
38
+ if (state.networkFailure) throw new Error("connection lost after write");
39
+ return Response.json(state.response, { status: state.status });
40
+ }
41
+ };
42
+ return { service: createConnectionService(options), options, directory, protection, requests, references, state };
43
+ }
44
+
45
+ test("Algolia persists a file connection, uses application headers and rotates only the backend key", async (t) => {
46
+ const { service, options, directory, protection, requests, references, state } = await fixture(t);
47
+ const connected = await service.connectApiKey(input);
48
+ assert.equal(connected.status, "connected");
49
+ const first = requests[0];
50
+ assert.equal(first.url.href, "https://app123abc.algolia.net/1/indexes?page=0&hitsPerPage=100");
51
+ assert.equal(first.headers.get("x-algolia-application-id"), "APP123ABC");
52
+ assert.equal(first.headers.get("x-algolia-api-key"), state.key);
53
+ assert.equal(first.init.redirect, "error");
54
+ assert.equal(first.headers.has("authorization"), false);
55
+ for (const name of await readdir(directory)) {
56
+ assert.equal((await readFile(path.join(directory, name), "utf8")).includes(state.key), false);
57
+ }
58
+ const restarted = createConnectionService({ ...options, store: createFileConnectionStore({ directory, protection }) });
59
+ assert.deepEqual(await restarted.status(input), connected);
60
+ state.key = "rotated-fixture-secret";
61
+ assert.equal((await restarted.status(input)).status, "reconnect-required");
62
+ assert.deepEqual(await restarted.invoke({ ...input, operation: "indices.list", input: { page: 1, hitsPerPage: 25 } }), indices);
63
+ assert.equal(requests.at(-1).url.search, "?page=1&hitsPerPage=25");
64
+ assert.equal(requests.at(-1).headers.get("x-algolia-api-key"), state.key);
65
+ assert.equal((await restarted.status(input)).status, "connected");
66
+ assert.deepEqual(new Set(references), new Set(["env:ALGOLIA_BACKEND_KEY"]));
67
+ for (const owner of [{ ...context, applicationId: "other-app" }, { ...context, subjectId: "other-team" }]) {
68
+ await assert.rejects(restarted.invoke({ ...input, context: owner, operation: "indices.list" }), { code: "connector_reconnect_required" });
69
+ }
70
+ await restarted.disconnect(input);
71
+ assert.equal((await service.status(input)).status, "disconnected");
72
+ });
73
+
74
+ test("Algolia validates portable application settings and keeps optional key references separate", async (t) => {
75
+ const { options, requests } = await fixture(t);
76
+ const parse = (configuration) => parseIntegrationConfiguration(JSON.stringify(configuration), { providers: [algoliaProvider] });
77
+ assert.deepEqual(parse(options.configuration), options.configuration);
78
+ for (const applicationId of [undefined, "", "app.attacker.invalid", "app/path", "x@host", "a-b", "x\r\ny", "x".repeat(64)]) {
79
+ const config = structuredClone(options.configuration);
80
+ config.integrations.search.settings.applicationId = applicationId;
81
+ assert.throws(() => parse(config), (error) => Boolean(error.fieldErrors["integrations.search.settings.applicationId"]));
82
+ }
83
+ const config = structuredClone(options.configuration);
84
+ config.integrations.search.settings.applicationId = " APP123ABC ";
85
+ assert.equal(parse(config).integrations.search.settings.applicationId, "APP123ABC");
86
+ config.integrations.search.settings.applicationId = "APP123ABC";
87
+ config.integrations.search.settings.publicApiKeyRef = "frontend-value-is-not-a-reference";
88
+ assert.throws(() => parse(config), (error) => Boolean(error.fieldErrors["integrations.search.settings.publicApiKeyRef"]));
89
+ delete config.integrations.search.settings.publicApiKeyRef;
90
+ assert.deepEqual(parse(config), config);
91
+ assert.equal(requests.length, 0);
92
+ });
93
+
94
+ test("Algolia searches a named index with encoded paths, JSON parameters and response pagination", async (t) => {
95
+ const { service, requests, state } = await fixture(t);
96
+ await service.connectApiKey(input);
97
+ state.response = hits;
98
+ const result = await service.invoke({ ...input, operation: "index.search", input: { indexName: "products /?&", query: "phone + café", page: 1, hitsPerPage: 25 } });
99
+ assert.deepEqual(result, hits);
100
+ const request = requests.at(-1);
101
+ assert.equal(request.url.href, "https://app123abc.algolia.net/1/indexes/products%20%2F%3F%26/query");
102
+ assert.equal(request.init.method, "POST");
103
+ assert.match(request.headers.get("content-type"), /application\/json/u);
104
+ assert.deepEqual(JSON.parse(request.init.body), { query: "phone + café", page: 1, hitsPerPage: 25 });
105
+ await service.invoke({ ...input, operation: "index.search", input: { indexName: "products" } });
106
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { query: "", page: 0, hitsPerPage: 20 });
107
+ const count = requests.length;
108
+ for (const invalid of [
109
+ {}, { indexName: "" }, { indexName: "." }, { indexName: ".." }, { indexName: "x".repeat(256) },
110
+ { indexName: "products", query: "é".repeat(257) }, { indexName: "products", hitsPerPage: 1001 },
111
+ { indexName: "products", page: -1 }, { indexName: "products", apiKey: "caller-key" },
112
+ { indexName: "products", url: "https://attacker.invalid" }
113
+ ]) await assert.rejects(service.invoke({ ...input, operation: "index.search", input: invalid }), { code: "connector_input_invalid" });
114
+ for (const invalid of [{ page: -1 }, { hitsPerPage: 0 }, { hitsPerPage: 1001 }, { page: null }, { url: "https://attacker.invalid" }]) {
115
+ await assert.rejects(service.invoke({ ...input, operation: "indices.list", input: invalid }), { code: "connector_input_invalid" });
116
+ }
117
+ assert.equal(requests.length, count);
118
+ await service.invoke({ ...input, operation: "index.search", input: { indexName: "products", query: "é".repeat(256), hitsPerPage: 1000 } });
119
+ });
120
+
121
+ test("Algolia rejects cross-application destinations and requires verification after configuration changes", async (t) => {
122
+ const { service, options, requests } = await fixture(t);
123
+ await service.connectApiKey(input);
124
+ const wrongDestination = createConnectionService({ ...options, providers: [{ ...algoliaProvider, operations: {
125
+ ...algoliaProvider.operations, "indices.list": { scopes: [], request: () => ({ method: "GET", url: "https://differentapp.algolia.net/1/indexes" }) }
126
+ } }] });
127
+ await assert.rejects(wrongDestination.invoke({ ...input, operation: "indices.list" }), { code: "connector_destination_invalid" });
128
+ assert.equal(requests.length, 1);
129
+ const configuration = structuredClone(options.configuration);
130
+ configuration.integrations.search.settings.applicationId = "OTHERAPP";
131
+ const changed = createConnectionService({ ...options, configuration });
132
+ assert.equal((await changed.status(input)).status, "reconnect-required");
133
+ await assert.rejects(changed.invoke({ ...input, operation: "indices.list" }), { code: "connector_reconnect_required" });
134
+ assert.equal(requests.length, 1);
135
+ await changed.connectApiKey(input);
136
+ assert.equal(requests.at(-1).url.origin, "https://otherapp.algolia.net");
137
+ assert.equal(requests.at(-1).headers.get("x-algolia-application-id"), "OTHERAPP");
138
+ assert.equal((await service.status(input)).status, "reconnect-required");
139
+ });
140
+
141
+ test("Algolia failed verification and malformed responses never expose credentials or imply connection", async (t) => {
142
+ const { service, state } = await fixture(t);
143
+ for (const [status, code] of [[200, "connector_response_invalid"], [401, "connector_reconnect_required"], [403, "connector_permission_denied"], [429, "connector_rate_limited"], [500, "connector_provider_failed"]]) {
144
+ state.status = status;
145
+ state.response = { message: state.key };
146
+ await assert.rejects(service.connectApiKey(input), (error) => {
147
+ assert.equal(error.code, code);
148
+ assert.equal(JSON.stringify(error).includes(state.key), false);
149
+ assert.equal(error.message.includes(state.key), false);
150
+ return true;
151
+ });
152
+ assert.equal((await service.status(input)).status, "disconnected");
153
+ }
154
+ state.status = 200;
155
+ state.response = indices;
156
+ await service.connectApiKey(input);
157
+ for (const malformed of [{ items: [] }, { hits: [] }, { ...hits, page: "0" }]) {
158
+ state.response = malformed;
159
+ await assert.rejects(service.invoke({ ...input, operation: "index.search", input: { indexName: "products" } }), { code: "connector_response_invalid" });
160
+ }
161
+ state.status = 401;
162
+ await assert.rejects(service.invoke({ ...input, operation: "indices.list" }), { code: "connector_reconnect_required" });
163
+ assert.equal((await service.status(input)).status, "reconnect-required");
164
+ });
165
+
166
+
167
+ test("Algolia indexes, partially updates and deletes records with explicit asynchronous completion", async t => {
168
+ const { service, requests, state } = await fixture(t);
169
+ await service.connectApiKey(input);
170
+ state.response = { objectID: "product/1", taskID: 123, updatedAt: "2026-09-12T00:00:00Z" };
171
+ const record = { indexName: "products / live", objectID: "product/1", attributes: { name: "Phone", price: 10 } };
172
+ await service.invoke({ ...input, operation: "records.replace", input: record });
173
+ assert.equal(requests.at(-1).init.method, "PUT");
174
+ assert.equal(requests.at(-1).url.pathname, "/1/indexes/products%20%2F%20live/product%2F1");
175
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), record.attributes);
176
+ await service.invoke({ ...input, operation: "records.update", input: { ...record, attributes: { price: 9 } } });
177
+ assert.equal(requests.at(-1).init.method, "POST");
178
+ assert.equal(requests.at(-1).url.searchParams.get("createIfNotExists"), "false");
179
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { price: 9 });
180
+ const count = requests.length;
181
+ for (const invalid of [{ ...record, attributes: {} }, { ...record, objectID: ".." },
182
+ { ...record, attributes: { objectID: "other", price: 8 } }]) {
183
+ await assert.rejects(service.invoke({ ...input, operation: "records.replace", input: invalid }));
184
+ }
185
+ assert.equal(requests.length, count);
186
+ for (const status of ["notPublished", "published"]) {
187
+ state.response = { status };
188
+ assert.deepEqual(await service.invoke({ ...input, operation: "tasks.get", input: { indexName: record.indexName, taskID: 123 } }), { status });
189
+ }
190
+ assert.equal(requests.at(-1).url.pathname, "/1/indexes/products%20%2F%20live/task/123");
191
+ state.response = { taskID: 124, deletedAt: "2026-09-12T00:00:00Z" };
192
+ await service.invoke({ ...input, operation: "records.delete", input: { indexName: record.indexName, objectID: record.objectID } });
193
+ assert.equal(requests.at(-1).init.method, "DELETE");
194
+ state.status = 403;
195
+ const beforeFailure = requests.length;
196
+ await assert.rejects(service.invoke({ ...input, operation: "records.replace", input: record }));
197
+ assert.equal(requests.length, beforeFailure + 1);
198
+ state.status = 200;
199
+ state.networkFailure = true;
200
+ const beforeUncertainWrite = requests.length;
201
+ await assert.rejects(service.invoke({ ...input, operation: "records.update", input: record }));
202
+ assert.equal(requests.length, beforeUncertainWrite + 1);
203
+ state.networkFailure = false;
204
+ state.response = { status: "unknown" };
205
+ await assert.rejects(service.invoke({ ...input, operation: "tasks.get", input: { indexName: record.indexName, taskID: 123 } }),
206
+ { code: "connector_response_invalid" });
207
+ });