@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.
- package/README.md +192 -0
- package/docs/ai-upstream-notices.md +51 -0
- package/docs/ai.md +191 -0
- package/docs/airtable.md +130 -0
- package/docs/algolia.md +190 -0
- package/docs/amazon-redshift.md +229 -0
- package/docs/amplitude.md +210 -0
- package/docs/apify.md +162 -0
- package/docs/apollo-io.md +200 -0
- package/docs/asana.md +126 -0
- package/docs/ashby.md +188 -0
- package/docs/atlassian.md +224 -0
- package/docs/attention.md +162 -0
- package/docs/aws-athena.md +150 -0
- package/docs/aws-credentials.md +75 -0
- package/docs/aws-s3.md +193 -0
- package/docs/bigquery.md +146 -0
- package/docs/brevo.md +122 -0
- package/docs/calendly.md +158 -0
- package/docs/canva.md +191 -0
- package/docs/chargebee.md +192 -0
- package/docs/clay.md +194 -0
- package/docs/clickhouse.md +242 -0
- package/docs/confidence-exp.md +74 -0
- package/docs/confidence-flags.md +75 -0
- package/docs/confidence-oauth.md +137 -0
- package/docs/contentful.md +123 -0
- package/docs/databricks.md +180 -0
- package/docs/dbt-semantic-layer.md +175 -0
- package/docs/elevenlabs.md +102 -0
- package/docs/figma.md +132 -0
- package/docs/firebase-cloud-messaging.md +262 -0
- package/docs/firecrawl.md +137 -0
- package/docs/fireflies.md +95 -0
- package/docs/fireworks-ai.md +53 -0
- package/docs/gatewayapi.md +113 -0
- package/docs/gemini-enterprise.md +224 -0
- package/docs/github-api.md +133 -0
- package/docs/gitlab-api.md +138 -0
- package/docs/gmail.md +133 -0
- package/docs/gong.md +205 -0
- package/docs/google-ads.md +156 -0
- package/docs/google-analytics.md +202 -0
- package/docs/google-docs.md +105 -0
- package/docs/google-drive.md +108 -0
- package/docs/google-maps-platform.md +208 -0
- package/docs/google-oauth.md +123 -0
- package/docs/google-search-console.md +124 -0
- package/docs/google-sheets.md +94 -0
- package/docs/google-slides.md +95 -0
- package/docs/granola.md +178 -0
- package/docs/hex.md +161 -0
- package/docs/heygen.md +124 -0
- package/docs/hubspot.md +142 -0
- package/docs/incident-io.md +118 -0
- package/docs/inngest.md +178 -0
- package/docs/klipy.md +149 -0
- package/docs/lexware.md +144 -0
- package/docs/lightspeed.md +193 -0
- package/docs/linear.md +137 -0
- package/docs/linkedin.md +196 -0
- package/docs/logo-dev.md +152 -0
- package/docs/mailgun.md +153 -0
- package/docs/mapbox.md +174 -0
- package/docs/microsoft-excel.md +97 -0
- package/docs/microsoft-fabric.md +249 -0
- package/docs/microsoft-oauth.md +143 -0
- package/docs/microsoft-onedrive.md +76 -0
- package/docs/microsoft-onenote.md +75 -0
- package/docs/microsoft-outlook.md +90 -0
- package/docs/microsoft-powerpoint.md +119 -0
- package/docs/microsoft-sharepoint.md +75 -0
- package/docs/microsoft-teams.md +72 -0
- package/docs/microsoft-word.md +119 -0
- package/docs/miro.md +117 -0
- package/docs/n8n.md +169 -0
- package/docs/notion.md +137 -0
- package/docs/oura.md +161 -0
- package/docs/paddle.md +168 -0
- package/docs/perplexity.md +85 -0
- package/docs/pipedrive.md +120 -0
- package/docs/polar.md +63 -0
- package/docs/posthog.md +227 -0
- package/docs/prestashop.md +151 -0
- package/docs/replicate.md +60 -0
- package/docs/resend.md +153 -0
- package/docs/salesforce.md +234 -0
- package/docs/sanity.md +122 -0
- package/docs/semrush.md +276 -0
- package/docs/sentry.md +112 -0
- package/docs/sevdesk.md +107 -0
- package/docs/shopify.md +206 -0
- package/docs/slack.md +257 -0
- package/docs/snowflake.md +271 -0
- package/docs/storyblok.md +130 -0
- package/docs/stripe.md +77 -0
- package/docs/supabase.md +51 -0
- package/docs/tally.md +105 -0
- package/docs/telegram.md +165 -0
- package/docs/tiktok.md +201 -0
- package/docs/twilio.md +284 -0
- package/docs/twitch.md +428 -0
- package/docs/wave.md +440 -0
- package/docs/wix.md +1249 -0
- package/docs/wiz.md +177 -0
- package/docs/woocommerce.md +400 -0
- package/docs/wordpress-com.md +358 -0
- package/docs/wordpress-self-hosted.md +252 -0
- package/docs/workday.md +292 -0
- package/docs/x-twitter.md +188 -0
- package/docs/xero.md +429 -0
- package/docs/zoho-books.md +214 -0
- package/docs/zoho-crm.md +192 -0
- package/package.json +153 -0
- package/patterns/ai-connections/PATTERN.md +84 -0
- package/patterns/ai-connections/example/ai-model.js +11 -0
- package/patterns/ai-connections/example/perplexity-answer.js +29 -0
- package/patterns/api-key-connection/PATTERN.md +368 -0
- package/patterns/api-key-connection/example/connections.js +18 -0
- package/patterns/api-key-connection/example/integrations.json +19 -0
- package/patterns/assistant-mcp/PATTERN.md +110 -0
- package/patterns/assistant-mcp/example/integrations.json +13 -0
- package/patterns/assistant-mcp-oauth/PATTERN.md +235 -0
- package/patterns/assistant-mcp-oauth/example/integrations.json +102 -0
- package/patterns/aws-storage-queries/PATTERN.md +169 -0
- package/patterns/aws-storage-queries/example/formats/data-formats.js +35 -0
- package/patterns/aws-storage-queries/example/formats/package-lock.json +49 -0
- package/patterns/aws-storage-queries/example/formats/package.json +12 -0
- package/patterns/aws-storage-queries/example/formats/verify-formats.mjs +37 -0
- package/patterns/aws-storage-queries/example/integrations.json +38 -0
- package/patterns/aws-storage-queries/example/s3-transfer.js +34 -0
- package/patterns/event-delivery/PATTERN.md +151 -0
- package/patterns/event-delivery/example/integrations.json +17 -0
- package/patterns/firebase-messaging/PATTERN.md +156 -0
- package/patterns/firebase-messaging/example/server/notifications.js +23 -0
- package/patterns/google-ads-search/PATTERN.md +92 -0
- package/patterns/google-ads-search/example/ads-setup.js +25 -0
- package/patterns/oauth-connection/PATTERN.md +724 -0
- package/patterns/oauth-connection/example/integrations.json +20 -0
- package/patterns/paddle-catalogue/PATTERN.md +78 -0
- package/patterns/paddle-catalogue/example/create-products.js +38 -0
- package/patterns/public-image/PATTERN.md +84 -0
- package/patterns/public-image/example/integrations.json +13 -0
- package/patterns/public-image/example/logo-url.js +16 -0
- package/patterns/redshift-queries/PATTERN.md +150 -0
- package/patterns/redshift-queries/example/integrations.json +33 -0
- package/patterns/source-scanning/PATTERN.md +83 -0
- package/patterns/source-scanning/example/source-scanner.js +13 -0
- package/src/client/firebase-cloud-messaging.js +10 -0
- package/src/client/google-analytics.js +72 -0
- package/src/client/google-maps-platform.js +39 -0
- package/src/client/logo-dev.js +36 -0
- package/src/server/ai.js +46 -0
- package/src/server/airtable.js +67 -0
- package/src/server/algolia.js +63 -0
- package/src/server/amazon-redshift.js +96 -0
- package/src/server/amplitude.js +31 -0
- package/src/server/apify.js +68 -0
- package/src/server/apollo-io.js +85 -0
- package/src/server/asana.js +64 -0
- package/src/server/ashby.js +61 -0
- package/src/server/atlassian.js +32 -0
- package/src/server/attention.js +93 -0
- package/src/server/aws-athena.js +82 -0
- package/src/server/aws-s3.js +49 -0
- package/src/server/awsTransport.js +75 -0
- package/src/server/bigquery.js +97 -0
- package/src/server/brevo.js +97 -0
- package/src/server/calendly.js +77 -0
- package/src/server/canva.js +13 -0
- package/src/server/chargebee.js +88 -0
- package/src/server/clay.js +78 -0
- package/src/server/clickhouse.js +66 -0
- package/src/server/confidence.js +29 -0
- package/src/server/contentful.js +40 -0
- package/src/server/databricks.js +55 -0
- package/src/server/dbt-semantic-layer.js +95 -0
- package/src/server/elevenlabs.js +81 -0
- package/src/server/figma.js +24 -0
- package/src/server/firebase-cloud-messaging.js +111 -0
- package/src/server/firecrawl.js +70 -0
- package/src/server/fireflies.js +31 -0
- package/src/server/fireworks-ai.js +16 -0
- package/src/server/gatewayapi.js +48 -0
- package/src/server/gemini-enterprise.js +47 -0
- package/src/server/github-api.js +90 -0
- package/src/server/gitlab-api.js +67 -0
- package/src/server/gmail.js +60 -0
- package/src/server/gong.js +52 -0
- package/src/server/google-ads-search.js +134 -0
- package/src/server/google-ads.js +80 -0
- package/src/server/google-docs.js +36 -0
- package/src/server/google-drive.js +71 -0
- package/src/server/google-maps-platform.js +90 -0
- package/src/server/google-search-console.js +31 -0
- package/src/server/google-sheets.js +60 -0
- package/src/server/google-slides.js +36 -0
- package/src/server/google.js +61 -0
- package/src/server/granola.js +115 -0
- package/src/server/graphqlOperation.js +15 -0
- package/src/server/hex.js +29 -0
- package/src/server/heygen.js +54 -0
- package/src/server/hubspot.js +85 -0
- package/src/server/incident-io.js +68 -0
- package/src/server/inngest.js +86 -0
- package/src/server/jsonOperation.js +34 -0
- package/src/server/klipy.js +34 -0
- package/src/server/lexware.js +59 -0
- package/src/server/lightspeed.js +82 -0
- package/src/server/linear.js +52 -0
- package/src/server/linkedin.js +74 -0
- package/src/server/mailgun.js +76 -0
- package/src/server/mapbox.js +46 -0
- package/src/server/mcpRegistration.js +41 -0
- package/src/server/mcpTransport.js +60 -0
- package/src/server/microsoft-excel.js +67 -0
- package/src/server/microsoft-fabric.js +81 -0
- package/src/server/microsoft-onedrive.js +58 -0
- package/src/server/microsoft-onenote.js +52 -0
- package/src/server/microsoft-outlook.js +58 -0
- package/src/server/microsoft-powerpoint.js +5 -0
- package/src/server/microsoft-sharepoint.js +46 -0
- package/src/server/microsoft-teams.js +29 -0
- package/src/server/microsoft-word.js +5 -0
- package/src/server/microsoft.js +88 -0
- package/src/server/miro.js +29 -0
- package/src/server/n8n.js +78 -0
- package/src/server/notion.js +76 -0
- package/src/server/oura.js +48 -0
- package/src/server/paddle.js +83 -0
- package/src/server/perplexity.js +15 -0
- package/src/server/pipedrive.js +95 -0
- package/src/server/polar.js +20 -0
- package/src/server/posthog.js +44 -0
- package/src/server/prestashop.js +42 -0
- package/src/server/replicate.js +42 -0
- package/src/server/resend.js +93 -0
- package/src/server/salesforce.js +84 -0
- package/src/server/sanity.js +31 -0
- package/src/server/semrush.js +359 -0
- package/src/server/sentry.js +30 -0
- package/src/server/sevdesk.js +20 -0
- package/src/server/shopify.js +99 -0
- package/src/server/slack.js +143 -0
- package/src/server/snowflake.js +139 -0
- package/src/server/storyblok.js +49 -0
- package/src/server/stripe.js +13 -0
- package/src/server/supabase.js +13 -0
- package/src/server/tally.js +78 -0
- package/src/server/telegram.js +55 -0
- package/src/server/tiktok.js +86 -0
- package/src/server/twilio.js +90 -0
- package/src/server/twitch.js +409 -0
- package/src/server/wave.js +520 -0
- package/src/server/wix.js +1048 -0
- package/src/server/wiz.js +117 -0
- package/src/server/woocommerce.js +412 -0
- package/src/server/wordpress-com.js +236 -0
- package/src/server/wordpress-self-hosted.js +134 -0
- package/src/server/workday.js +136 -0
- package/src/server/x-twitter.js +110 -0
- package/src/server/xero.js +397 -0
- package/src/server/zoho-books.js +90 -0
- package/src/server/zoho-crm.js +89 -0
- package/src/server/zohoOAuth.js +21 -0
- package/src/shared/ai-models.json +8049 -0
- package/src/shared/ai.js +63 -0
- package/src/shared/algolia.js +29 -0
- package/src/shared/amazon-redshift.js +51 -0
- package/src/shared/amplitude.js +28 -0
- package/src/shared/atlassian.js +43 -0
- package/src/shared/aws.js +72 -0
- package/src/shared/canva.js +66 -0
- package/src/shared/chargebee.js +26 -0
- package/src/shared/clickhouse.js +38 -0
- package/src/shared/confidence.js +28 -0
- package/src/shared/databricks.js +33 -0
- package/src/shared/dbt-semantic-layer.js +32 -0
- package/src/shared/definitions.js +140 -0
- package/src/shared/figma.js +21 -0
- package/src/shared/firebase-cloud-messaging.js +45 -0
- package/src/shared/gemini-enterprise.js +33 -0
- package/src/shared/gong.js +33 -0
- package/src/shared/google-ads-search.js +31 -0
- package/src/shared/google-ads.js +28 -0
- package/src/shared/google-maps-platform.js +28 -0
- package/src/shared/google.js +133 -0
- package/src/shared/granola.js +25 -0
- package/src/shared/hex.js +28 -0
- package/src/shared/inngest.js +35 -0
- package/src/shared/lightspeed.js +75 -0
- package/src/shared/linkedin.js +27 -0
- package/src/shared/logo-dev.js +21 -0
- package/src/shared/mapbox.js +38 -0
- package/src/shared/mcp.js +92 -0
- package/src/shared/microsoft-fabric.js +47 -0
- package/src/shared/microsoft.js +231 -0
- package/src/shared/miro.js +27 -0
- package/src/shared/oura.js +23 -0
- package/src/shared/posthog.js +33 -0
- package/src/shared/prestashop.js +23 -0
- package/src/shared/salesforce.js +36 -0
- package/src/shared/semrush.js +24 -0
- package/src/shared/shopify.js +51 -0
- package/src/shared/siteUrl.js +12 -0
- package/src/shared/slack.js +88 -0
- package/src/shared/snowflake.js +55 -0
- package/src/shared/tiktok.js +22 -0
- package/src/shared/tokens.js +747 -0
- package/src/shared/twilio.js +41 -0
- package/src/shared/twitch.js +49 -0
- package/src/shared/wave.js +52 -0
- package/src/shared/wix.js +40 -0
- package/src/shared/wiz.js +41 -0
- package/src/shared/wordpress-com.js +30 -0
- package/src/shared/wordpress.js +69 -0
- package/src/shared/workday.js +50 -0
- package/src/shared/x-twitter.js +19 -0
- package/src/shared/xero.js +46 -0
- package/src/shared/zoho-books.js +39 -0
- package/src/shared/zoho-crm.js +43 -0
- package/src/shared/zoho-regions.js +12 -0
- package/test/ai.test.js +136 -0
- package/test/airtable.test.js +125 -0
- package/test/algolia.test.js +207 -0
- package/test/amazon-redshift.test.js +272 -0
- package/test/amplitude.test.js +239 -0
- package/test/apify.test.js +93 -0
- package/test/apollo-io.test.js +125 -0
- package/test/asana.test.js +99 -0
- package/test/ashby.test.js +97 -0
- package/test/atlassian.test.js +249 -0
- package/test/attention.test.js +79 -0
- package/test/aws.test.js +405 -0
- package/test/brevo.test.js +71 -0
- package/test/calendly.test.js +136 -0
- package/test/canva.test.js +258 -0
- package/test/chargebee.test.js +195 -0
- package/test/clickhouse.test.js +278 -0
- package/test/confidence.test.js +205 -0
- package/test/databricks.test.js +253 -0
- package/test/dbt-semantic-layer.test.js +221 -0
- package/test/designMcp.test.js +336 -0
- package/test/elevenlabs.test.js +52 -0
- package/test/firebase-cloud-messaging.test.js +242 -0
- package/test/firecrawl.test.js +66 -0
- package/test/gemini-enterprise.test.js +218 -0
- package/test/github-api.test.js +167 -0
- package/test/gitlab-api.test.js +160 -0
- package/test/gong.test.js +192 -0
- package/test/google-ads.test.js +350 -0
- package/test/google-maps-platform.test.js +191 -0
- package/test/google.test.js +710 -0
- package/test/granola.test.js +240 -0
- package/test/hex.test.js +253 -0
- package/test/heygen.test.js +121 -0
- package/test/hubspot.test.js +103 -0
- package/test/inngest.test.js +219 -0
- package/test/lightspeed.test.js +250 -0
- package/test/linear.test.js +146 -0
- package/test/linkedin.test.js +243 -0
- package/test/logo-dev.test.js +79 -0
- package/test/mailgun.test.js +107 -0
- package/test/mapbox.test.js +181 -0
- package/test/mcp.test.js +227 -0
- package/test/microsoft-fabric.test.js +258 -0
- package/test/microsoft.test.js +554 -0
- package/test/n8nOAuth.test.js +188 -0
- package/test/notionOAuth.test.js +183 -0
- package/test/oura.test.js +211 -0
- package/test/paddleCatalogue.test.js +51 -0
- package/test/paddlePermissions.test.js +101 -0
- package/test/perplexityComposition.test.js +39 -0
- package/test/pipedriveOAuth.test.js +140 -0
- package/test/posthog.test.js +200 -0
- package/test/prestashop.test.js +207 -0
- package/test/resend.test.js +163 -0
- package/test/salesforce.test.js +276 -0
- package/test/semrush.test.js +479 -0
- package/test/sentry.test.js +35 -0
- package/test/shopify.test.js +247 -0
- package/test/slack.test.js +320 -0
- package/test/snowflake.test.js +391 -0
- package/test/tally.test.js +75 -0
- package/test/telegram.test.js +58 -0
- package/test/tiktok.test.js +243 -0
- package/test/tokens.test.js +1079 -0
- package/test/twilio.test.js +221 -0
- package/test/twitch.test.js +573 -0
- package/test/wave.test.js +678 -0
- package/test/wix.test.js +1099 -0
- package/test/wiz.test.js +185 -0
- package/test/wordpress-com.test.js +469 -0
- package/test/wordpress.test.js +660 -0
- package/test/workday.test.js +314 -0
- package/test/x-twitter.test.js +256 -0
- package/test/xero.test.js +700 -0
- package/test/zoho-books.test.js +253 -0
- 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 };
|