@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,397 @@
1
+ import { createSchema } from "json-rest-schema";
2
+ import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
3
+ import { ConnectorError } from "@jskit-ai/connectors-core/server";
4
+ import { xeroDefinition } from "../shared/xero.js";
5
+ import { jsonOperation } from "./jsonOperation.js";
6
+
7
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
8
+ const uuid = (value) => typeof value === "string" && uuidPattern.test(value);
9
+ const tenantId = { type: "string", required: true, validator: (value) => uuid(value) || "Choose an organisation ID returned by connections.list." };
10
+ const paging = {
11
+ page: { type: "integer", min: 1, max: 1_000_000, defaultTo: 1 },
12
+ pageSize: { type: "integer", min: 1, max: 200, defaultTo: 100 }
13
+ };
14
+ const connectionsUrl = "https://api.xero.com/connections";
15
+ const idempotencyField = { type: "string", required: true, minLength: 1, maxLength: 128,
16
+ validator: value => /^[\x21-\x7e]+$/u.test(value) || "Use a stable printable request key." };
17
+ const dateField = { type: "string", noTrim: true, required: true,
18
+ validator: value => /^\d{4}-\d{2}-\d{2}$/u.test(value) && Number.isFinite(Date.parse(value)) &&
19
+ new Date(value).toISOString().slice(0, 10) === value || "Use a valid YYYY-MM-DD date." };
20
+ const reportFields = {
21
+ periods: { type: "integer", min: 1, max: 12 },
22
+ timeframe: { type: "string", enum: ["MONTH", "QUARTER", "YEAR"] },
23
+ standardLayout: { type: "boolean" }, paymentsOnly: { type: "boolean" }
24
+ };
25
+ const reportRow = row => row !== null && typeof row === "object" && !Array.isArray(row) && typeof row.RowType === "string" &&
26
+ (row.Cells === undefined || Array.isArray(row.Cells) && row.Cells.every(cell => typeof cell?.Value === "string"));
27
+ const reportResult = value => Array.isArray(value?.Reports) && value.Reports.length === 1 && value.Reports.every(report =>
28
+ typeof report?.ReportID === "string" && Array.isArray(report.Rows) && report.Rows.every(row => reportRow(row) &&
29
+ (row.Rows === undefined || Array.isArray(row.Rows) && row.Rows.every(reportRow))));
30
+ const validConnections = (value) => Array.isArray(value) && value.every((entry) => uuid(entry?.id) && uuid(entry.tenantId) &&
31
+ ["ORGANISATION", "PRACTICEMANAGER", "PRACTICE"].includes(entry.tenantType) && (entry.tenantName === null || typeof entry.tenantName === "string"));
32
+
33
+ function accountingRead(resource, fields, scopes, validateResult) {
34
+ const schema = createSchema({ tenantId, ...fields });
35
+ return {
36
+ scopes, validateResult,
37
+ request(input) {
38
+ const { tenantId, ...values } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
39
+ if (values.fromDate && values.toDate && values.fromDate > values.toDate ||
40
+ values.DateFrom && values.DateTo && values.DateFrom > values.DateTo) {
41
+ throw new ConnectorError("connector_input_invalid", "The report start date must not follow its end date.", { statusCode: 422 });
42
+ }
43
+ const url = new URL(`https://api.xero.com/api.xro/2.0/${resource}`);
44
+ for (const [name, value] of Object.entries(values)) url.searchParams.set(name, String(value));
45
+ if (resource === "Contacts" || resource === "Invoices") url.searchParams.set("summaryOnly", "true");
46
+ return { method: "GET", url: url.href, headers: { "Xero-tenant-id": tenantId.toLowerCase() } };
47
+ }
48
+ };
49
+ }
50
+
51
+ function accountingRecord(resource, idName, scopes, fields = {}) {
52
+ const schema = createSchema({ tenantId, ...fields, id: { type: "string", required: true, validator: value => uuid(value) || "Use the record ID returned by Xero." } });
53
+ return {
54
+ scopes,
55
+ request(input) {
56
+ const { tenantId: tenant, id, ...values } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
57
+ if (values.DateFrom && values.DateTo && values.DateFrom > values.DateTo) {
58
+ throw new ConnectorError("connector_input_invalid", "The budget start date must not follow its end date.", { statusCode: 422 });
59
+ }
60
+ const url = new URL(`https://api.xero.com/api.xro/2.0/${resource}/${id.toLowerCase()}`);
61
+ for (const [name, value] of Object.entries(values)) url.searchParams.set(name, String(value));
62
+ return { method: "GET", url: url.href,
63
+ headers: { "Xero-tenant-id": tenant.toLowerCase() } };
64
+ },
65
+ validateResult: value => Array.isArray(value?.[resource]) && value[resource].length === 1 && uuid(value[resource][0]?.[idName])
66
+ };
67
+ }
68
+
69
+ function contactWrite(update) {
70
+ const schema = createSchema({
71
+ tenantId,
72
+ idempotencyKey: idempotencyField,
73
+ ...(update ? { id: { type: "string", required: true, validator: value => uuid(value) || "Use the contact ID returned by Xero." } } : {}),
74
+ Name: { type: "string", required: !update, minLength: 1, maxLength: 255 },
75
+ FirstName: { type: "string", maxLength: 255 }, LastName: { type: "string", maxLength: 255 },
76
+ EmailAddress: { type: "string", maxLength: 255 }, ContactNumber: { type: "string", maxLength: 50 }
77
+ });
78
+ return {
79
+ scopes: ["accounting.contacts"],
80
+ request(input) {
81
+ const { tenantId: tenant, idempotencyKey, id, ...contact } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
82
+ if (Object.keys(contact).length === 0) throw new ConnectorError("connector_input_invalid", "Supply a contact field to change.", { statusCode: 422 });
83
+ return { method: update ? "POST" : "PUT", url: `https://api.xero.com/api.xro/2.0/Contacts${update ? `/${id.toLowerCase()}` : ""}`,
84
+ headers: { "Xero-tenant-id": tenant.toLowerCase(), "Idempotency-Key": idempotencyKey }, body: { Contacts: [contact] } };
85
+ },
86
+ validateResult: value => Array.isArray(value?.Contacts) && value.Contacts.length === 1 && uuid(value.Contacts[0]?.ContactID) &&
87
+ value.Contacts[0].HasValidationErrors !== true && !value.Contacts[0].ValidationErrors?.length
88
+ };
89
+ }
90
+
91
+ const paymentSchema = createSchema({
92
+ tenantId, idempotencyKey: idempotencyField,
93
+ invoiceId: { type: "string", required: true, validator: value => uuid(value) || "Choose an invoice ID." },
94
+ accountId: { type: "string", required: true, validator: value => uuid(value) || "Choose an account ID." },
95
+ Date: dateField,
96
+ Amount: { type: "number", required: true, validator: value => Number.isFinite(value) && value > 0 && value <= Number.MAX_SAFE_INTEGER || "Use a positive finite payment amount." },
97
+ CurrencyRate: { type: "number", validator: value => Number.isFinite(value) && value > 0 || "Use a positive finite exchange rate." },
98
+ Reference: { type: "string", maxLength: 255 }
99
+ });
100
+ const finiteAmount = { type: "number", validator: value => Number.isFinite(value) && Math.abs(value) <= Number.MAX_SAFE_INTEGER || "Use a finite amount within the supported range." };
101
+ function invoiceWrite(update) {
102
+ const schema = createSchema({
103
+ tenantId, idempotencyKey: idempotencyField,
104
+ ...(update ? { id: { type: "string", required: true, validator: value => uuid(value) || "Choose the invoice ID." } } : {}),
105
+ Type: { type: "string", required: !update, enum: ["ACCREC", "ACCPAY"] },
106
+ contactId: { type: "string", required: !update, validator: value => uuid(value) || "Choose a contact ID." },
107
+ Date: { ...dateField, required: !update }, DueDate: { ...dateField, required: false },
108
+ CurrencyCode: { type: "string", required: !update, validator: value => /^[A-Z]{3}$/u.test(value) || "Use the three-letter invoice currency code." },
109
+ LineAmountTypes: { type: "string", required: !update, enum: ["Exclusive", "Inclusive", "NoTax"] },
110
+ Status: { type: "string", enum: update ? ["DRAFT", "SUBMITTED", "AUTHORISED", "VOIDED", "DELETED"] : ["DRAFT", "SUBMITTED", "AUTHORISED"], ...(!update ? { defaultTo: "DRAFT" } : {}) },
111
+ Reference: { type: "string", maxLength: 255 }, InvoiceNumber: { type: "string", maxLength: 255 },
112
+ LineItems: { type: "array", required: !update, validator: values => values.length > 0 && values.length <= 100 || "Supply 1–100 invoice lines.",
113
+ items: { type: "object", schema: createSchema({
114
+ LineItemID: { type: "string", validator: value => uuid(value) || "Use the existing line item ID." },
115
+ Description: { type: "string", required: true, minLength: 1, maxLength: 4000 },
116
+ Quantity: { ...finiteAmount, required: true, min: 0.0001 },
117
+ UnitAmount: { ...finiteAmount, required: true }, AccountCode: { type: "string", required: true, minLength: 1, maxLength: 50 },
118
+ TaxType: { type: "string", maxLength: 50 }, DiscountRate: { type: "number", min: 0, max: 100 }
119
+ }) }
120
+ }
121
+ });
122
+ return {
123
+ scopes: ["accounting.invoices"],
124
+ request(input) {
125
+ const { tenantId: tenant, idempotencyKey, id, contactId, ...invoice } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
126
+ if (!contactId && !Object.keys(invoice).length) throw new ConnectorError("connector_input_invalid", "Supply an invoice field to change.", { statusCode: 422 });
127
+ if (invoice.Type === "ACCPAY" && invoice.LineItems?.some(line => line.DiscountRate !== undefined)) {
128
+ throw new ConnectorError("connector_input_invalid", "Discount rates apply only to sales invoices.", { statusCode: 422 });
129
+ }
130
+ if (contactId) invoice.Contact = { ContactID: contactId };
131
+ return { method: update ? "POST" : "PUT", url: `https://api.xero.com/api.xro/2.0/Invoices${update ? `/${id.toLowerCase()}` : ""}?unitdp=4`,
132
+ headers: { "Xero-tenant-id": tenant.toLowerCase(), "Idempotency-Key": idempotencyKey }, body: { Invoices: [invoice] } };
133
+ },
134
+ validateResult: value => Array.isArray(value?.Invoices) && value.Invoices.length === 1 && uuid(value.Invoices[0]?.InvoiceID) &&
135
+ value.Invoices[0].HasErrors !== true && !value.Invoices[0].ValidationErrors?.length
136
+ };
137
+ }
138
+
139
+ function attachmentOperation(action) {
140
+ const writing = action === "upload" || action === "replace";
141
+ const schema = createSchema({
142
+ tenantId,
143
+ resource: { type: "string", required: true, enum: ["Contacts", "Invoices", "BankTransactions", "ManualJournals"] },
144
+ id: { type: "string", required: true, validator: value => uuid(value) || "Choose the parent record ID." },
145
+ ...(action === "download" ? { attachmentId: { type: "string", required: true, validator: value => uuid(value) || "Choose an attachment ID from attachments.list." } } : {}),
146
+ ...(writing ? {
147
+ idempotencyKey: idempotencyField,
148
+ filename: { type: "string", required: true, minLength: 1, maxLength: 255,
149
+ // eslint-disable-next-line no-control-regex -- Reject literal control characters in provider input.
150
+ validator: value => !/[\\/\x00-\x1f\x7f]/u.test(value) && ![".", ".."].includes(value) || "Use a filename without path separators or control characters." },
151
+ contentBase64: { type: "string", required: true, minLength: 4, maxLength: 4194304, noTrim: true }
152
+ } : {})
153
+ });
154
+ return {
155
+ scopes: writing ? ["accounting.attachments"] : ["accounting.attachments.read", "accounting.attachments"],
156
+ request(input) {
157
+ const { tenantId: tenant, resource, id, attachmentId, filename, contentBase64, idempotencyKey } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
158
+ const suffix = attachmentId ? `/${attachmentId.toLowerCase()}` : filename ? `/${encodeURIComponent(filename)}` : "";
159
+ const headers = { "Xero-tenant-id": tenant.toLowerCase() };
160
+ let body;
161
+ if (writing) {
162
+ body = Buffer.from(contentBase64, "base64");
163
+ if (!body.length || body.length > 3 * 1024 * 1024 || body.toString("base64") !== contentBase64) {
164
+ throw new ConnectorError("connector_input_invalid", "Supply canonical base64 for a nonempty attachment of at most 3 MiB.", { statusCode: 422 });
165
+ }
166
+ headers["Content-Type"] = "application/octet-stream";
167
+ headers["Idempotency-Key"] = idempotencyKey;
168
+ }
169
+ return { method: writing ? action === "upload" ? "PUT" : "POST" : "GET",
170
+ url: `https://api.xero.com/api.xro/2.0/${resource}/${id.toLowerCase()}/Attachments${suffix}`, headers, ...(body ? { body } : {}) };
171
+ },
172
+ validateResult: action === "download"
173
+ ? value => typeof value?.contentBase64 === "string" && Number.isInteger(value.size) && value.size > 0
174
+ : value => Array.isArray(value?.Attachments) && (!writing || value.Attachments.length === 1) && value.Attachments.every(file =>
175
+ uuid(file?.AttachmentID) && typeof file.FileName === "string" && Number.isInteger(file.ContentLength) && file.ContentLength >= 0)
176
+ };
177
+ }
178
+
179
+ function bankTransactionWrite(update) {
180
+ const schema = createSchema({
181
+ tenantId, idempotencyKey: idempotencyField,
182
+ ...(update ? { id: { type: "string", required: true, validator: value => uuid(value) || "Choose a bank transaction ID." } } : {}),
183
+ Type: { type: "string", required: !update, enum: ["SPEND", "RECEIVE"] },
184
+ contactId: { type: "string", required: !update, validator: value => uuid(value) || "Choose a contact ID." },
185
+ bankAccountId: { type: "string", required: !update, validator: value => uuid(value) || "Choose a bank account ID." },
186
+ Date: { ...dateField, required: !update }, Reference: { type: "string", maxLength: 255 },
187
+ CurrencyRate: { type: "number", validator: value => Number.isFinite(value) && value > 0 || "Use a positive finite exchange rate." },
188
+ LineAmountTypes: { type: "string", required: !update, enum: ["Exclusive", "Inclusive", "NoTax"] },
189
+ ...(update ? { Status: { type: "string", enum: ["AUTHORISED", "DELETED"] } } : {}),
190
+ LineItems: { type: "array", required: !update, validator: values => values.length > 0 && values.length <= 100 || "Supply 1–100 transaction lines.",
191
+ items: { type: "object", schema: createSchema({
192
+ LineItemID: { type: "string", validator: value => uuid(value) || "Use the existing line item ID." },
193
+ Description: { type: "string", required: true, minLength: 1, maxLength: 4000 },
194
+ Quantity: { ...finiteAmount, required: true, min: 0.0001 }, UnitAmount: { ...finiteAmount, required: true },
195
+ AccountCode: { type: "string", required: true, minLength: 1, maxLength: 50 }, TaxType: { type: "string", maxLength: 50 }
196
+ }) }
197
+ }
198
+ });
199
+ return {
200
+ scopes: ["accounting.banktransactions"],
201
+ request(input) {
202
+ const { tenantId: tenant, idempotencyKey, id, contactId, bankAccountId, ...transaction } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
203
+ if (!contactId && !bankAccountId && !Object.keys(transaction).length) throw new ConnectorError("connector_input_invalid", "Supply a bank transaction field to change.", { statusCode: 422 });
204
+ if (contactId) transaction.Contact = { ContactID: contactId };
205
+ if (bankAccountId) transaction.BankAccount = { AccountID: bankAccountId };
206
+ if (update) transaction.BankTransactionID = id.toLowerCase();
207
+ return { method: update ? "POST" : "PUT", url: `https://api.xero.com/api.xro/2.0/BankTransactions${update ? `/${id.toLowerCase()}` : ""}?unitdp=4`,
208
+ headers: { "Xero-tenant-id": tenant.toLowerCase(), "Idempotency-Key": idempotencyKey }, body: { BankTransactions: [transaction] } };
209
+ },
210
+ validateResult: value => Array.isArray(value?.BankTransactions) && value.BankTransactions.length === 1 && uuid(value.BankTransactions[0]?.BankTransactionID) &&
211
+ !value.BankTransactions[0].ValidationErrors?.length
212
+ };
213
+ }
214
+
215
+ function manualJournalWrite(update) {
216
+ const schema = createSchema({
217
+ tenantId, idempotencyKey: idempotencyField,
218
+ ...(update ? { id: { type: "string", required: true, validator: value => uuid(value) || "Choose a manual journal ID." } } : {}),
219
+ Narration: { type: "string", required: !update, minLength: 1, maxLength: 4000 },
220
+ Date: { ...dateField, required: !update },
221
+ LineAmountTypes: { type: "string", required: !update, enum: ["Exclusive", "Inclusive", "NoTax"] },
222
+ Status: { type: "string", enum: update ? ["DRAFT", "POSTED", "DELETED", "VOIDED", "ARCHIVED"] : ["DRAFT", "POSTED"], ...(!update ? { defaultTo: "DRAFT" } : {}) },
223
+ ShowOnCashBasisReports: { type: "boolean" },
224
+ JournalLines: { type: "array", required: !update, validator: values => values.length >= 2 && values.length <= 100 || "Supply 2–100 journal lines.",
225
+ items: { type: "object", schema: createSchema({
226
+ LineAmount: { ...finiteAmount, required: true },
227
+ AccountCode: { type: "string", required: true, minLength: 1, maxLength: 50 },
228
+ Description: { type: "string", maxLength: 4000 }, TaxType: { type: "string", maxLength: 50 },
229
+ Tracking: { type: "array", validator: values => values.length <= 2 || "Use at most two tracking categories.",
230
+ items: { type: "object", schema: createSchema({
231
+ Name: { type: "string", required: true, minLength: 1, maxLength: 100 },
232
+ Option: { type: "string", required: true, minLength: 1, maxLength: 100 }
233
+ }) }
234
+ }
235
+ }) }
236
+ }
237
+ });
238
+ return {
239
+ scopes: ["accounting.manualjournals"],
240
+ request(input) {
241
+ const { tenantId: tenant, idempotencyKey, id, ...journal } = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
242
+ if (!Object.keys(journal).length) throw new ConnectorError("connector_input_invalid", "Supply a journal field to change.", { statusCode: 422 });
243
+ if (update) journal.ManualJournalID = id.toLowerCase();
244
+ return { method: update ? "POST" : "PUT", url: `https://api.xero.com/api.xro/2.0/ManualJournals${update ? `/${id.toLowerCase()}` : ""}`,
245
+ headers: { "Xero-tenant-id": tenant.toLowerCase(), "Idempotency-Key": idempotencyKey }, body: { ManualJournals: [journal] } };
246
+ },
247
+ validateResult: value => Array.isArray(value?.ManualJournals) && value.ManualJournals.length === 1 && uuid(value.ManualJournals[0]?.ManualJournalID) &&
248
+ !value.ManualJournals[0].ValidationErrors?.length
249
+ };
250
+ }
251
+
252
+ const xeroProvider = Object.freeze({
253
+ ...xeroDefinition,
254
+ oauth: { issuer: "https://identity.xero.com", authorization_endpoint: "https://login.xero.com/identity/connect/authorize", token_endpoint: "https://identity.xero.com/connect/token" },
255
+ apiOrigins: ["https://api.xero.com"], checkOperation: "connections.list",
256
+ operations: {
257
+ "attachments.list": attachmentOperation("list"),
258
+ "attachments.download": attachmentOperation("download"),
259
+ "attachments.upload": attachmentOperation("upload"),
260
+ "attachments.replace": attachmentOperation("replace"),
261
+ "connections.list": jsonOperation(connectionsUrl, {}, validConnections),
262
+ "accounts.list": accountingRead("Accounts", {}, ["accounting.settings.read", "accounting.settings"],
263
+ value => Array.isArray(value?.Accounts) && value.Accounts.every(account => uuid(account?.AccountID) && typeof account.Type === "string")),
264
+ "currencies.list": accountingRead("Currencies", {}, ["accounting.settings.read", "accounting.settings"],
265
+ value => Array.isArray(value?.Currencies) && value.Currencies.every(currency => /^[A-Z]{3}$/u.test(currency?.Code) && typeof currency.Description === "string")),
266
+ "taxRates.list": accountingRead("TaxRates", {}, ["accounting.settings.read", "accounting.settings"],
267
+ value => Array.isArray(value?.TaxRates) && value.TaxRates.every(rate => typeof rate?.TaxType === "string" && typeof rate.Name === "string")),
268
+ "trackingCategories.list": accountingRead("TrackingCategories", { includeArchived: { type: "boolean", defaultTo: false } },
269
+ ["accounting.settings.read", "accounting.settings"],
270
+ value => Array.isArray(value?.TrackingCategories) && value.TrackingCategories.every(category => uuid(category?.TrackingCategoryID) && typeof category.Name === "string")),
271
+ "reports.agedReceivables": accountingRead("Reports/AgedReceivablesByContact", {
272
+ contactId: { type: "string", required: true, validator: value => uuid(value) || "Choose a contact ID." },
273
+ date: dateField, fromDate: { ...dateField, required: false }, toDate: { ...dateField, required: false }
274
+ }, ["accounting.reports.aged.read"], reportResult),
275
+ "reports.agedPayables": accountingRead("Reports/AgedPayablesByContact", {
276
+ contactId: { type: "string", required: true, validator: value => uuid(value) || "Choose a contact ID." },
277
+ date: dateField, fromDate: { ...dateField, required: false }, toDate: { ...dateField, required: false }
278
+ }, ["accounting.reports.aged.read"], reportResult),
279
+ "reports.executiveSummary": accountingRead("Reports/ExecutiveSummary", { date: dateField },
280
+ ["accounting.reports.executivesummary.read"], reportResult),
281
+ "reports.bankSummary": accountingRead("Reports/BankSummary", { fromDate: dateField, toDate: dateField },
282
+ ["accounting.reports.banksummary.read"], reportResult),
283
+ "reports.trialBalance": accountingRead("Reports/TrialBalance", { date: dateField, paymentsOnly: { type: "boolean" } },
284
+ ["accounting.reports.trialbalance.read"], reportResult),
285
+ "budgets.list": accountingRead("Budgets", {
286
+ DateFrom: { ...dateField, required: false }, DateTo: { ...dateField, required: false }
287
+ }, ["accounting.budgets.read"],
288
+ value => Array.isArray(value?.Budgets) && value.Budgets.every(budget => uuid(budget?.BudgetID) && ["OVERALL", "TRACKING"].includes(budget.Type))),
289
+ "budgets.get": accountingRecord("Budgets", "BudgetID", ["accounting.budgets.read"], {
290
+ DateFrom: { ...dateField, required: false }, DateTo: { ...dateField, required: false }
291
+ }),
292
+ "reports.budgetSummary": accountingRead("Reports/BudgetSummary", {
293
+ date: dateField, periods: { type: "integer", min: 1, max: 12 },
294
+ timeframe: { type: "integer", validator: value => [1, 3, 12].includes(value) || "Use 1 (month), 3 (quarter) or 12 (year)." }
295
+ }, ["accounting.reports.budgetsummary.read"], reportResult),
296
+ "bankTransactions.create": bankTransactionWrite(false),
297
+ "bankTransactions.update": bankTransactionWrite(true),
298
+ "bankTransactions.list": accountingRead("BankTransactions", { page: paging.page },
299
+ ["accounting.banktransactions.read", "accounting.banktransactions"],
300
+ value => Array.isArray(value?.BankTransactions) && value.BankTransactions.length <= 100 && value.BankTransactions.every(record => uuid(record?.BankTransactionID))),
301
+ "bankTransactions.get": accountingRecord("BankTransactions", "BankTransactionID", ["accounting.banktransactions.read", "accounting.banktransactions"]),
302
+ "manualJournals.create": manualJournalWrite(false),
303
+ "manualJournals.update": manualJournalWrite(true),
304
+ "manualJournals.list": accountingRead("ManualJournals", { page: paging.page },
305
+ ["accounting.manualjournals.read", "accounting.manualjournals"],
306
+ value => Array.isArray(value?.ManualJournals) && value.ManualJournals.length <= 100 && value.ManualJournals.every(record => uuid(record?.ManualJournalID))),
307
+ "manualJournals.get": accountingRecord("ManualJournals", "ManualJournalID", ["accounting.manualjournals.read", "accounting.manualjournals"]),
308
+ "payments.create": {
309
+ scopes: ["accounting.payments"],
310
+ request(input) {
311
+ const { tenantId: tenant, idempotencyKey, invoiceId, accountId, ...payment } = validateSchemaPayload({ schema: paymentSchema, mode: "replace" }, input, { statusCode: 422 });
312
+ return { method: "PUT", url: "https://api.xero.com/api.xro/2.0/Payments",
313
+ headers: { "Xero-tenant-id": tenant.toLowerCase(), "Idempotency-Key": idempotencyKey },
314
+ body: { Payments: [{ ...payment, Invoice: { InvoiceID: invoiceId }, Account: { AccountID: accountId } }] } };
315
+ },
316
+ validateResult: value => Array.isArray(value?.Payments) && value.Payments.length === 1 && uuid(value.Payments[0]?.PaymentID) &&
317
+ Number.isFinite(value.Payments[0].Amount) && value.Payments[0].HasValidationErrors !== true && !value.Payments[0].ValidationErrors?.length
318
+ },
319
+ "contacts.get": accountingRecord("Contacts", "ContactID", ["accounting.contacts.read", "accounting.contacts"]),
320
+ "contacts.create": contactWrite(false),
321
+ "contacts.update": contactWrite(true),
322
+ "invoices.get": accountingRecord("Invoices", "InvoiceID", ["accounting.invoices.read", "accounting.invoices"]),
323
+ "invoices.create": invoiceWrite(false),
324
+ "invoices.update": invoiceWrite(true),
325
+ "reports.profitAndLoss": accountingRead("Reports/ProfitAndLoss", { fromDate: dateField, toDate: dateField, ...reportFields },
326
+ ["accounting.reports.profitandloss.read"], reportResult),
327
+ "reports.balanceSheet": accountingRead("Reports/BalanceSheet", { date: dateField, ...reportFields },
328
+ ["accounting.reports.balancesheet.read"], reportResult),
329
+ "payments.list": accountingRead("Payments", { page: paging.page, pageSize: { ...paging.pageSize, max: 100 } },
330
+ ["accounting.payments.read", "accounting.payments"],
331
+ (value) => Array.isArray(value?.Payments) && value.Payments.length <= 100 && value.Payments.every((entry) =>
332
+ uuid(entry?.PaymentID) && Number.isFinite(entry.Amount) && typeof entry.Status === "string")),
333
+ "organisation.read": accountingRead("Organisation", {}, ["accounting.settings.read", "accounting.settings"],
334
+ (value) => Array.isArray(value?.Organisations) && value.Organisations.length === 1 && uuid(value.Organisations[0]?.OrganisationID)),
335
+ "contacts.list": accountingRead("Contacts", { ...paging, includeArchived: { type: "boolean", defaultTo: false }, searchTerm: { type: "string", minLength: 1, maxLength: 255 } },
336
+ ["accounting.contacts.read", "accounting.contacts"], (value) => Array.isArray(value?.Contacts) && value.Contacts.length <= 200 && value.Contacts.every((entry) => uuid(entry?.ContactID))),
337
+ "invoices.list": accountingRead("Invoices", paging, ["accounting.invoices.read", "accounting.invoices"],
338
+ (value) => Array.isArray(value?.Invoices) && value.Invoices.length <= 200 && value.Invoices.every((entry) => uuid(entry?.InvoiceID)))
339
+ },
340
+ async exchange(address, options, { request, fetchImpl }) {
341
+ if (address !== connectionsUrl) {
342
+ const tenant = options.headers["Xero-tenant-id"];
343
+ const headers = Object.fromEntries(Object.entries(options.headers).filter(([key]) => ["authorization", "accept"].includes(key.toLowerCase())));
344
+ const connections = await request(connectionsUrl, { ...options, method: "GET", body: undefined, headers });
345
+ if (!validConnections(connections)) throw new ConnectorError("connector_response_invalid", "Xero returned invalid connections.", { statusCode: 502 });
346
+ if (!connections.some((entry) => entry.tenantType === "ORGANISATION" && entry.tenantId.toLowerCase() === tenant)) {
347
+ throw new ConnectorError("connector_permission_denied", "This Xero organisation is not connected to this account.", { statusCode: 403 });
348
+ }
349
+ }
350
+ const attachmentFile = /\/Attachments\/[^/]+$/u.test(new URL(address).pathname);
351
+ if (attachmentFile) {
352
+ const downloading = options.method === "GET";
353
+ const response = await fetchImpl(address, { ...options, credentials: "omit", redirect: "error",
354
+ headers: { ...options.headers, Accept: downloading ? "application/octet-stream" : "application/json" } });
355
+ if (!response.ok) {
356
+ await response.body?.cancel();
357
+ throw Object.assign(new Error("Xero attachment request failed."), { status: response.status });
358
+ }
359
+ const reader = response.body?.getReader(); const chunks = []; let size = 0;
360
+ if (reader) try {
361
+ while (true) {
362
+ const { done, value } = await reader.read(); if (done) break;
363
+ size += value.byteLength;
364
+ if (size > 3 * 1024 * 1024) {
365
+ await reader.cancel();
366
+ throw new ConnectorError("connector_response_too_large", "Xero attachment responses are limited to 3 MiB.", { statusCode: 413 });
367
+ }
368
+ chunks.push(value);
369
+ }
370
+ } finally { reader.releaseLock(); }
371
+ const bytes = Buffer.concat(chunks);
372
+ if (downloading) return { contentBase64: bytes.toString("base64"), size,
373
+ contentType: response.headers.get("content-type")?.split(";")[0] || "application/octet-stream" };
374
+ try { return JSON.parse(bytes.toString("utf8")); }
375
+ catch { throw new ConnectorError("connector_response_invalid", "Xero returned an invalid attachment response.", { statusCode: 502 }); }
376
+ }
377
+ const result = await request(address, options);
378
+ const url = new URL(address);
379
+ const record = /^\/api\.xro\/2\.0\/(Contacts|Invoices|Budgets|BankTransactions|ManualJournals)\/([0-9a-f-]+)$/iu.exec(url.pathname);
380
+ if (record) {
381
+ const idName = { Contacts: "ContactID", Invoices: "InvoiceID", Budgets: "BudgetID", BankTransactions: "BankTransactionID", ManualJournals: "ManualJournalID" }[record[1]];
382
+ const returnedId = result?.[record[1]]?.[0]?.[idName];
383
+ if (!uuid(returnedId) || returnedId.toLowerCase() !== record[2].toLowerCase()) {
384
+ throw new ConnectorError("connector_response_invalid", "Xero returned a different record.", { statusCode: 502 });
385
+ }
386
+ }
387
+ if (url.pathname.endsWith("/Organisation") && result?.Organisations?.[0]?.OrganisationID?.toLowerCase() !== options.headers["Xero-tenant-id"]) {
388
+ throw new ConnectorError("connector_response_invalid", "Xero returned a different organisation.", { statusCode: 502 });
389
+ }
390
+ const records = url.pathname.endsWith("/Contacts") ? result?.Contacts : url.pathname.endsWith("/Invoices") ? result?.Invoices : url.pathname.endsWith("/Payments") ? result?.Payments : null;
391
+ if (url.searchParams.has("pageSize") && Array.isArray(records) && records.length > Number(url.searchParams.get("pageSize"))) {
392
+ throw new ConnectorError("connector_response_invalid", "Xero returned more records than requested.", { statusCode: 502 });
393
+ }
394
+ return result;
395
+ }
396
+ });
397
+ export { xeroProvider };
@@ -0,0 +1,90 @@
1
+ import { createSchema } from "json-rest-schema";
2
+ import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
3
+ import { ConnectorError } from "@jskit-ai/connectors-core/server";
4
+ import { zohoBooksDefinition, zohoBooksIdentifier, zohoBooksOrganizationField } from "../shared/zoho-books.js";
5
+ import { zohoRegions } from "../shared/zoho-regions.js";
6
+ import { jsonOperation } from "./jsonOperation.js";
7
+ import { zohoAuthorizationParameters, zohoOAuthMetadata, normalizeZohoTokenResponse } from "./zohoOAuth.js";
8
+
9
+ const regionFor = ({ region = "eu" } = {}) => zohoRegions.find(({ value }) => value === region);
10
+ const apiOrigin = (settings) => `https://www.zohoapis.${regionFor(settings).suffix}`;
11
+ const validOrganizations = (value) => Array.isArray(value?.organizations) && value.organizations.every((org) =>
12
+ zohoBooksIdentifier(org?.organization_id) && typeof org.name === "string" && typeof org.is_org_active === "boolean");
13
+
14
+ function booksList(resource, identifier) {
15
+ const schema = createSchema({
16
+ organization_id: zohoBooksOrganizationField,
17
+ page: { type: "integer", min: 1, max: 1_000_000, defaultTo: 1 },
18
+ per_page: { type: "integer", min: 1, max: 200, defaultTo: 100 }
19
+ });
20
+ return {
21
+ scopes: [`ZohoBooks.${resource}.READ`, `ZohoBooks.${resource}.ALL`],
22
+ request(input, settings = {}) {
23
+ const values = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
24
+ if (settings.organizationId && values.organization_id && values.organization_id !== settings.organizationId) {
25
+ throw new ConnectorError("connector_input_invalid", "This integration is configured for a different organisation.", { statusCode: 422 });
26
+ }
27
+ values.organization_id = settings.organizationId || values.organization_id;
28
+ if (!values.organization_id) {
29
+ throw new ConnectorError("connector_input_invalid", "Choose an organisation ID from organizations.list or configure one for this integration.", { statusCode: 422 });
30
+ }
31
+ const url = new URL(`${apiOrigin(settings)}/books/v3/${resource}`);
32
+ for (const [name, value] of Object.entries(values)) url.searchParams.set(name, String(value));
33
+ return { method: "GET", url: url.href };
34
+ },
35
+ validateResult: (value) => Array.isArray(value?.[resource]) && value[resource].length <= 200 &&
36
+ value[resource].every((record) => zohoBooksIdentifier(record?.[identifier])) && typeof value.page_context?.has_more_page === "boolean"
37
+ };
38
+ }
39
+
40
+ const zohoBooksProvider = Object.freeze({
41
+ ...zohoBooksDefinition,
42
+ oauth: (settings) => zohoOAuthMetadata(regionFor(settings).accounts),
43
+ authorizationParameters: zohoAuthorizationParameters, scopeSeparator: ",",
44
+ apiOrigins: (settings) => [apiOrigin(settings)], checkOperation: "organizations.list",
45
+ normalizeTokenResponse: (response, { settings }) => normalizeZohoTokenResponse(response, apiOrigin(settings)),
46
+ operations: {
47
+ "organizations.list": {
48
+ ...jsonOperation((settings) => `${apiOrigin(settings)}/books/v3/organizations`, {}, validOrganizations),
49
+ scopes: ["ZohoBooks.settings.READ"]
50
+ },
51
+ "contacts.list": booksList("contacts", "contact_id"),
52
+ "invoices.list": booksList("invoices", "invoice_id")
53
+ },
54
+ async exchange(address, options, { fetchImpl, settings }) {
55
+ const headers = new Headers(options.headers);
56
+ headers.set("Authorization", headers.get("Authorization").replace(/^Bearer /u, "Zoho-oauthtoken "));
57
+ const response = await fetchImpl(address, { ...options, headers, credentials: "omit" });
58
+ if (!response.ok) {
59
+ if (response.status === 404) throw new ConnectorError("connector_resource_not_found", "This Books resource is not available.", { statusCode: 404 });
60
+ throw Object.assign(new Error("Zoho Books request failed."), { status: response.status });
61
+ }
62
+ let value;
63
+ try { value = await response.json(); } catch { /* validated below */ }
64
+ if (response.status !== 200 || value?.code !== 0) {
65
+ throw new ConnectorError("connector_response_invalid", "Zoho Books returned an unexpected response.", { statusCode: 502 });
66
+ }
67
+ const url = new URL(address);
68
+ if (url.pathname.endsWith("/organizations")) {
69
+ if (!validOrganizations(value)) throw new ConnectorError("connector_response_invalid", "Zoho Books returned invalid organisations.", { statusCode: 502 });
70
+ if (settings.organizationId && !value.organizations.some((org) => org.organization_id === settings.organizationId && org.is_org_active)) {
71
+ throw new ConnectorError("connector_permission_denied", "The configured Books organisation is not active and accessible to this account.", { statusCode: 403 });
72
+ }
73
+ } else {
74
+ const resource = url.pathname.split("/").at(-1);
75
+ // Books documents invoice page_context as a singleton array; other lists use an object.
76
+ if (resource === "invoices" && Array.isArray(value.page_context)) {
77
+ if (value.page_context.length !== 1) throw new ConnectorError("connector_response_invalid", "Zoho Books returned ambiguous pagination.", { statusCode: 502 });
78
+ value.page_context = value.page_context[0];
79
+ }
80
+ if (value[resource]?.length > Number(url.searchParams.get("per_page")) ||
81
+ value.page_context?.per_page !== Number(url.searchParams.get("per_page")) ||
82
+ value.page_context?.page !== Number(url.searchParams.get("page"))) {
83
+ throw new ConnectorError("connector_response_invalid", "Zoho Books returned an unexpected page.", { statusCode: 502 });
84
+ }
85
+ }
86
+ return value;
87
+ }
88
+ });
89
+
90
+ export { zohoBooksProvider };
@@ -0,0 +1,89 @@
1
+ import { createSchema } from "json-rest-schema";
2
+ import { validateSchemaPayload } from "@jskit-ai/kernel/shared/validators";
3
+ import { ConnectorError } from "@jskit-ai/connectors-core/server";
4
+ import { zohoCrmDefinition, zohoCrmRegions } from "../shared/zoho-crm.js";
5
+ import { jsonOperation } from "./jsonOperation.js";
6
+ import { zohoAuthorizationParameters, zohoOAuthMetadata, normalizeZohoTokenResponse } from "./zohoOAuth.js";
7
+
8
+ const regionFor = ({ region = "us" } = {}) => zohoCrmRegions.find(({ value }) => value === region);
9
+ const apiOrigin = (settings = {}) => `https://${settings.environment === "sandbox" ? "sandbox" : settings.environment === "developer" ? "developer" : "www"}.zohoapis.${regionFor(settings).suffix}`;
10
+ const identifier = (value) => typeof value === "string" && /^[1-9][0-9]{0,29}$/u.test(value);
11
+ const pageToken = (value) => typeof value === "string" && value.length > 0 && value.length <= 4096 && !/[\s\p{Cc}]/u.test(value);
12
+
13
+ function recordsList(module, defaults) {
14
+ const schema = createSchema({
15
+ fields: { type: "string", defaultTo: defaults, validator: (value) => {
16
+ const fields = value.split(",");
17
+ return fields.length <= 50 && new Set(fields).size === fields.length && fields.every((field) => /^[A-Za-z_][A-Za-z0-9_]{0,99}$/u.test(field)) || "Use up to 50 distinct field API names, separated by commas.";
18
+ } },
19
+ page: { type: "integer", min: 1, max: 10 },
20
+ per_page: { type: "integer", min: 1, max: 200, defaultTo: 100 },
21
+ page_token: { type: "string", validator: (value) => pageToken(value) || "Use the page token returned for this user and query." }
22
+ });
23
+ return {
24
+ scopes: [`ZohoCRM.modules.${module.toLowerCase()}.READ`, `ZohoCRM.modules.${module.toLowerCase()}.ALL`, "ZohoCRM.modules.READ", "ZohoCRM.modules.ALL"],
25
+ request(input, settings) {
26
+ const values = validateSchemaPayload({ schema, mode: "replace" }, input, { statusCode: 422 });
27
+ if (values.page !== undefined && values.page_token !== undefined) {
28
+ throw new ConnectorError("connector_input_invalid", "Use either a page number or a page token.", { statusCode: 422 });
29
+ }
30
+ if (!values.page_token && values.page === undefined) values.page = 1;
31
+ const url = new URL(`${apiOrigin(settings)}/crm/v8/${module}`);
32
+ for (const [name, value] of Object.entries(values)) url.searchParams.set(name, String(value));
33
+ return { method: "GET", url: url.href };
34
+ },
35
+ validateResult: (result) => Array.isArray(result?.data) && result.data.length <= 200 && result.data.every((record) => identifier(record?.id)) &&
36
+ Number.isSafeInteger(result.info?.count) && result.info.count === result.data.length &&
37
+ Number.isSafeInteger(result.info.per_page) && result.info.per_page >= 1 && result.info.per_page <= 200 &&
38
+ typeof result.info.more_records === "boolean" &&
39
+ (result.info.page === undefined || Number.isSafeInteger(result.info.page) && result.info.page >= 1) &&
40
+ (result.info.next_page_token == null || pageToken(result.info.next_page_token)) &&
41
+ (result.info.previous_page_token == null || pageToken(result.info.previous_page_token))
42
+ };
43
+ }
44
+
45
+ const zohoCrmProvider = Object.freeze({
46
+ ...zohoCrmDefinition,
47
+ oauth: (settings) => zohoOAuthMetadata(regionFor(settings).accounts),
48
+ authorizationParameters: zohoAuthorizationParameters, scopeSeparator: ",",
49
+ apiOrigins: (settings) => [apiOrigin(settings)], checkOperation: "users.current",
50
+ normalizeTokenResponse: (response, { settings }) => normalizeZohoTokenResponse(response, apiOrigin(settings)),
51
+ operations: {
52
+ "users.current": {
53
+ ...jsonOperation((settings) => `${apiOrigin(settings)}/crm/v8/users?type=CurrentUser`, {},
54
+ (value) => Array.isArray(value?.users) && value.users.length === 1 && identifier(value.users[0]?.id)),
55
+ scopes: ["ZohoCRM.users.READ", "ZohoCRM.users.ALL"]
56
+ },
57
+ "leads.list": recordsList("Leads", "Last_Name,Email"),
58
+ "contacts.list": recordsList("Contacts", "Last_Name,Email"),
59
+ "accounts.list": recordsList("Accounts", "Account_Name"),
60
+ "deals.list": recordsList("Deals", "Deal_Name,Stage")
61
+ },
62
+ async exchange(address, options, { fetchImpl }) {
63
+ const headers = new Headers(options.headers);
64
+ headers.set("Authorization", headers.get("Authorization").replace(/^Bearer /u, "Zoho-oauthtoken "));
65
+ const response = await fetchImpl(address, { ...options, headers, credentials: "omit" });
66
+ const url = new URL(address);
67
+ const list = !url.pathname.endsWith("/users");
68
+ const perPage = Number(url.searchParams.get("per_page"));
69
+ if (response.status === 204 && list) return { data: [], info: { count: 0, per_page: perPage, more_records: false } };
70
+ let value;
71
+ try { value = await response.json(); } catch { /* checked after status mapping */ }
72
+ if (value?.code === "OAUTH_SCOPE_MISMATCH") throw new ConnectorError("connector_scope_missing", "Connect again with the CRM permissions needed for this operation.", { statusCode: 403 });
73
+ if (!response.ok) {
74
+ if (response.status === 404) throw new ConnectorError("connector_resource_not_found", "This CRM resource is not available.", { statusCode: 404 });
75
+ // The shared runtime maps HTTP status without exposing provider response text.
76
+ throw Object.assign(new Error("Zoho CRM request failed."), { status: response.status });
77
+ }
78
+ if (response.status !== 200 || !value || value.status === "error" || value.code) {
79
+ throw new ConnectorError("connector_response_invalid", "Zoho CRM returned an unexpected response.", { statusCode: 502 });
80
+ }
81
+ if (list && (value.data?.length > perPage || value.info?.per_page !== perPage ||
82
+ url.searchParams.has("page") && value.info?.page !== Number(url.searchParams.get("page")))) {
83
+ throw new ConnectorError("connector_response_invalid", "Zoho CRM returned an unexpected page.", { statusCode: 502 });
84
+ }
85
+ return value;
86
+ }
87
+ });
88
+
89
+ export { zohoCrmProvider };
@@ -0,0 +1,21 @@
1
+ import { ConnectorError } from "@jskit-ai/connectors-core/server";
2
+
3
+ const zohoAuthorizationParameters = Object.freeze({ access_type: "offline", prompt: "consent" });
4
+
5
+ function zohoOAuthMetadata(accounts) {
6
+ return { issuer: accounts, authorization_endpoint: `${accounts}/oauth/v2/auth`, token_endpoint: `${accounts}/oauth/v2/token` };
7
+ }
8
+
9
+ async function normalizeZohoTokenResponse(response, expectedApiOrigin) {
10
+ let value;
11
+ try { value = await response.clone().json(); } catch { /* handled by OAuth processing */ }
12
+ if (value?.error) {
13
+ return Response.json({ error: value.error === "invalid_code" ? "invalid_grant" : value.error }, { status: 400 });
14
+ }
15
+ if (response.ok && value?.api_domain !== expectedApiOrigin) {
16
+ throw new ConnectorError("connector_response_invalid", "Zoho returned a token for a different data center or environment.", { statusCode: 502 });
17
+ }
18
+ return response;
19
+ }
20
+
21
+ export { zohoAuthorizationParameters, zohoOAuthMetadata, normalizeZohoTokenResponse };